PHPを使った動画ファイルアップロードとFFMPEGを使ったMP4変換
はじめに
PHPを使ったファイルアップロードは、Web開発において必須の機能といえます。
ユーザーはこの機能を使用して、ローカルPCからWEBサーバー上にファイルをアップロードすることができるようになります。
画像も動画もアップロードの機能自体は同じで、INPUTにfile属性を定義し、フォームでPOSTメソッドを使用します。
アップロードされたファイルは、$_FILES変数に情報を格納し、一時的にサーバー上のtmpディレクトリに保存され、その後、指定したディレクトリに保存されます。
この流れを順を追って説明していきたいと思います。
今回のディレクトリ構成
- /av ディレクトリ
- ./uploads //アップロードしたファイルが保存されるディレクトリ
- ./movies //変換した動画が保存されるディレクトリ
- index.php
- upload.php
作業環境は、xamppを使用しています。htdocsの中に avというフォルダを作成している前提です。
webであれば、public_htmlやwwwの中にavディレクトリを作成します。
avの中に、index.phpとupload.php、そしてuploadsとmoviesというフォルダを作成します。
これで準備は完了です。
phpファイルにフォームを設置
まずindex.phpにアップロードするフォームを設置します。
ローカル環境であれば、セッションとか、CRSFは無視で構いません。
最低限のおまじないということで。
フォームの体裁のために、headerにtailwindcssのCDNを読み込んでいます。
<?php
/**
* @index.php
*/
session_start();
$arr_cookie_options = array(
'expires' => time() + 60 * 60,
'path' => '/',
'domain' => 'localhost',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
);
setcookie(session_name(), session_id(), $arr_cookie_options);
$token = uniqid('', true);
$_SESSION['token'] = $token;
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>動画のアップロードと変換</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<section class="text-gray-600 body-font relative">
<div class="container px-5 py-24 mx-auto">
<div class="flex flex-col text-center w-full mb-12">
<h1 class="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900">動画ファイルのアップロード</h1>
<p class="lg:w-2/3 mx-auto leading-relaxed text-base">mov.avi.wmv.mpeg.mp4のファイルがアップロードできます。</p>
</div>
<?php if (isset($_SESSION['e-message'])) : ?>
<div class="my-6 border border-red-600 rounded bg-red-50">
<?php foreach ($_SESSION['e-message'] as $message) : ?>
<p class="mb-4 text-left lg:text-center text-sm text-red-600"><?php echo $message; ?></p>
<?php endforeach; ?>
<?php unset($_SESSION['e-message']); ?>
</div>
<?php endif; ?>
<form action="./upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="upload">
<input type="hidden" name="token" value="<?php echo $token; ?>">
<div class="lg:w-1/2 md:w-2/3 mx-auto">
<div class="flex flex-wrap -m-2">
<div class="p-2 w-full">
<div class="relative">
<label for="up_file" class="leading-7 text-sm text-gray-600">動画ファイル</label>
<input type="file" id="up_file" name="up_file" accept="video/*" capture="user" class="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out">
</div>
</div>
<div class="p-2 w-full">
<button class="block w-full flex mx-auto justify-center text-white bg-indigo-500 border-0 py-2 px-8 focus:outline-none hover:bg-indigo-600 rounded text-lg tracking-widest">アップロードする</button>
</div>
<div class="p-2 w-full pt-8 mt-8 border-t border-gray-200 text-center">
<a class="text-indigo-500">アップロードテスト</a>
<p class="leading-normal my-5">369code</p>
<span class="inline-flex">
<a class="text-gray-500">
<svg fill="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-5 h-5" viewBox="0 0 24 24">
<path d="M18 2h-3a5 5 0 00-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 011-1h3z"></path>
</svg>
</a>
<a class="ml-4 text-gray-500">
<svg fill="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-5 h-5" viewBox="0 0 24 24">
<path d="M23 3a10.9 10.9 0 01-3.14 1.53 4.48 4.48 0 00-7.86 3v1A10.66 10.66 0 013 4s-4 9 5 13a11.64 11.64 0 01-7 2c9 5 20 0 20-11.5a4.5 4.5 0 00-.08-.83A7.72 7.72 0 0023 3z"></path>
</svg>
</a>
</span>
</div>
</div>
</div>
</form>
</div>
</section>
</body>
</html>
index.phpの説明
フォームでエラーが発生した時のために、エラー表示枠を設定します。
エラーは、配列にしてあるので、foreachで回して表示します。
<?php if (isset($_SESSION['e-message'])) : ?>
<div class="my-6 border border-red-600 rounded bg-red-50">
<?php foreach ($_SESSION['e-message'] as $message) : ?>
<p class="mb-4 text-left lg:text-center text-sm text-red-600"><?php echo $message; ?></p>
<?php endforeach; ?>
<?php unset($_SESSION['e-message']); ?>
</div>
<?php endif; ?>
フォームをformタグを使って設置します。ファイルをアップロードするので、enctype属性でmultipart/form-dataを指定しておきます。
このフォームですと、POST先は、upload.phpとなります。
<form action="./upload.php" method="POST" enctype="multipart/form-data">
ファイルをアップロードしたいので、inputのtype属性はfileを使用します。
accept属性でvideoを指定することで、絶対ではないですが、選択できるファイルの種類をある程度指定できます。今回なら、動画ファイルに指定できるということです。
capture属性でuserを指定することで、スマホ等のデバイスであれば、リアルに撮影したものをそのままアップロードが可能です。
<input type="file" id="up_file" name="up_file" accept="video/*" capture="user">
POST側のファイルの内容
今回のフォームでは、index.phpからupload.phpにPOSTします。
POST側でフォームの情報を受け取る必要があります。
少し長いですが、後で説明をします。
<?php
/**
* @upload.php
*/
$params = session_get_cookie_params();
session_set_cookie_params([
'lifetime' => 600,
'path' => $params["path"],
'domain' => $params["domain"],
'secure' => $params["secure"],
'httponly' => true,
'samesite' => $params["samesite"],
]);
session_cache_limiter('none');
session_start();
if (isset($_POST['token'])) {
$token = $_POST['token'];
}
if (isset($_SESSION['token'])) {
$session_token = $_SESSION['token'];
}
unset($_SESSION['token']);
if ((empty($token) || $token != $session_token)) {
$errorMessage = [];
$errorMessage[] = '不正なリクエストです。';
$_SESSION['e-message'] = $errorMessage;
header('Location: http://localhost/av/index.php');
exit;
}
$_SESSION['token'] = $token;
$action = null;
if (isset($_POST['action'])) {
$action = $_POST['action'];
}
if ($action === "upload") {
if (isset($_FILES['up_file'])) {
try {
if (!isset($_FILES['up_file']['error']) || is_array($_FILES['up_file']['error'])) {
throw new RuntimeException('パラメータが無効です。');
}
$error = (int)$_FILES['up_file']['error'];
$message = '';
switch ((int)$error) {
case 0:
$message = null;
break;
case 1:
$message = '設定のアップロードファイルサイズの制限を超えています。';
break;
case 2:
$message = 'HTMLフォームで指定されたファイルサイズの制限を超えています。';
break;
case 3:
$message = 'アップロードファイルの内容が不十分です。';
break;
case 4:
$message = 'ファイルを選択してください。';
break;
case 6:
$message = 'サーバーにTMPフォルダがありません。';
break;
case 7:
$message = 'サーバーがディスクの書き込みに失敗しました。';
break;
case 8:
$message = 'サーバーがファイルのアップロードを中止しました。';
break;
default:
$message = 'サーバーに未知のエラーが発生しました。';
}
if (!empty($message)) {
throw new RuntimeException($message);
}
//ファイルサイズ確認(50MB制限)
if ($_FILES['up_file']['size'] > 50000000) {
throw new RuntimeException('ファイルサイズの制限を超えています。');
}
//拡張子判断
$arrowExt = ['avi', 'mov', 'avi', 'wmv', 'mpeg', 'mp4'];
$fileExt = pathinfo($_FILES['up_file']['name'], PATHINFO_EXTENSION);
if (in_array($fileExt, $arrowExt) === false) {
throw new RuntimeException('ファイル形式が無効です。');
}
//移動する
if (is_uploaded_file($_FILES['up_file']['tmp_name'])) {
//名前を日時に変える
$newFilename = time() . '.' . $fileExt;
$up_dir = './uploads/';
$moveFile = $up_dir . $newFilename;
if (!move_uploaded_file($_FILES['up_file']['tmp_name'], $moveFile)) {
throw new RuntimeException('ファイルの移動に失敗しました。');
}
echo 'File is uploaded successfully.';
} else {
throw new RuntimeException('ファイルのアップロードに失敗しました。');
}
} catch (RuntimeException $e) {
$_SESSION['e-message'][] = $e->getMessage();
header('Location: http://localhost/av/index.php');
exit;
}
} else {
$_SESSION['e-message'][] = 'ファイルを選択してください';
header('Location: http://localhost/av/index.php');
exit;
}
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>動画の表示</title>
</head>
<body>
<section class="w-full mx-auto text-center">
<div class="w-1/2 mx-auto text-center p-8">
<video src="<?php echo $moveFile;?>" controls width="720"></video>
</div>
</section>
</body>
</html>
upload.phpの説明
index.phpと同じく、ローカル環境であれば、セッションとか、CRSFのあたりは無視で構いません。
最低限のおまじないということで。
フォームでPOSTの保存を行う時は、例外処理をするのが大事です。
アップロードファイルが存在していれば、その後、処理を行うためにtry~catchで囲みます。
errorがあれば、$_SESSION['e-message']にエラー内容を格納して、元のフォームに戻します。
if (isset($_FILES['up_file'])) {
try {
} catch (RuntimeException $e) {
$_SESSION['e-message'][] = $e->getMessage();
header('Location: http://localhost/av/index.php');
exit;
}
}
フォームでアップロードされたファイルに関して、バリデーションが必要になります。
不正なファイルで無いか、容量はどうか、拡張子はどうか、調べる必要があります。
$_FILEでは、アップロードされたファイルのエラーを数字で格納しています。
0がエラーなしで、それ以外は何らかのエラーが発生しています。
つまり0以外はアウトです。
$error = (int)$_FILES['up_file']['error'];
$message = '';
switch ((int)$error) {
case 0:
$message = null;
break;
case 1:
$message = '設定のアップロードファイルサイズの制限を超えています。';
break;
case 2:
$message = 'HTMLフォームで指定されたファイルサイズの制限を超えています。';
break;
case 3:
$message = 'アップロードファイルの内容が不十分です。';
break;
case 4:
$message = 'ファイルを選択してください。';
break;
case 6:
$message = 'サーバーにTMPフォルダがありません。';
break;
case 7:
$message = 'サーバーがディスクの書き込みに失敗しました。';
break;
case 8:
$message = 'サーバーがファイルのアップロードを中止しました。';
break;
default:
$message = 'サーバーに未知のエラーが発生しました。';
}
//0以外
if (!empty($message)) {
throw new RuntimeException($message);
}
上記では、サーバーに設定してあるphp.iniのmax_post_sizeの値しか見てないので、50MB以上のサイズの動画ファイルはアップしないというルールを入れたいときは下記のように記載します。
単位はbyteです。
if ($_FILES['up_file']['size'] > 50000000) {
throw new RuntimeException('ファイルサイズの制限を超えています。');
}
アップロードできる拡張子に制限を入れます。可能な拡張子を配列にして、in_arrayでチェックします。
ファイルの拡張子はpathinfoという便利な関数で抽出します。
$arrowExt = ['avi', 'mov', 'avi', 'wmv', 'mpeg', 'mp4'];
$fileExt = pathinfo($_FILES['up_file']['name'], PATHINFO_EXTENSION);
if (in_array($fileExt, $arrowExt) === false) {
throw new RuntimeException('ファイル形式が無効です。');
}
エラーがあれば、RuntimeExceptionが実行されindexに戻ります。
ここまでで、ファイルのチェックは完了です。これを通過したら、指定した場所に保存します。
今回はuploadsフォルダに保存します。
ついでに、動画ファイルの名前もアップロードした日時に変更します。//任意
if (is_uploaded_file($_FILES['up_file']['tmp_name'])) {
//名前を日時に変える
$newFilename = time() . '.' . $fileExt;
$up_dir = './uploads/';
$moveFile = $up_dir . $newFilename;
if (!move_uploaded_file($_FILES['up_file']['tmp_name'], $moveFile)) {
throw new RuntimeException('ファイルの移動に失敗しました。');
}
echo 'ファイルのアップロードに成功しました。';
}
これで、成功すれば、videoタグの$moveFileにパスが入り、アップロードが成功しましたと表示されます。
MP4変換をしないアップロードだけであれば、これで完了です。
*動画はデジカメ動画フリー素材様のものを使用しています。
FFMPEGを使った動画の変換
ファイルをアップロードするだけでなく、指定の形式に変換する作業が求められることが多くあります。サイズが大きすぎるものをリサイズしたり、wmvファイルをmp4に変換する工程は、ffmpegを使用します。
FFmpegをインストールする工程は今回、割愛します。
下記の記事を参考にしてください。
[XAMPP] PHPでFFMPEGを使って動画変換
upload.phpに以下を追加で記載します。
echo 'ファイルのアップロードに成功しました。';
//ここからファイルの変換
//mp4ファイル名の作成
$movieExt = '.mp4';
$movieName = time();
$movieDir = './movies';
$movieFile = $movieDir . '/' . $movieName . $movieExt;
//ffmpeg コマンド
define("FFMPEG_COMMAND1", ' ffmpeg -i %s -f mp4 -vcodec libx264 -acodec aac -b 2000k -y %s 2>&1');
$command1 = sprintf(FFMPEG_COMMAND1, $moveFile, $movieFile);
$log = null;
exec($command1, $log);
//var_dump($log);
//poster作成
$imageFile = $movieDir . '/' . $movieName . '.jpg';
define("FFMPEG_COMMAND2", ' ffmpeg -ss 5 -i %s -vframes 1 -f image2 %s 2>&1');
$command2 = sprintf(FFMPEG_COMMAND2, $movieFile, $imageFile);
$log = null;
exec($command2, $log);
//元のファイルを削除
if (file_exists($movieFile)) {
unlink($moveFile);
}
まず、新しく作るmp4ファイル名を作成日時+.mp4で作ります。
一旦、FFMPEGのコマンドを定数に入れ、sprintfで処理します。
今回はサイズ変更しませんが、libx264のうしろあたりに「-s 1280x720」と追加で入れることで1280×720サイズに変更できます。
1つ目の%sが入力ファイル、2つ目の%sが出力ファイルになります。
2>&1 でログをはきます。
execでFFMPEGを実行します。(Windowsなので非同期になりません)
define("FFMPEG_COMMAND1", ' ffmpeg -i %s -f mp4 -vcodec libx264 -acodec aac -b 2000k -y %s 2>&1');
$command1 = sprintf(FFMPEG_COMMAND1, $moveFile, $movieFile);
$log = null;
exec($command1, $log);
logを表示するようにしてると、エラーが分かります。
これで、アップロードした動画を変換して指定の場所に保存できました。
動画のポスターも一緒に作って同じmoviesに保存します。
-ss 5 で5秒後に、-vframes 1 で1フレームだけ 保存となります。
1つ目の%sが入力ファイル、2つ目の%sが出力ファイルになります。
2>&1 でログをはきます。
//poster作成
$imageFile = $movieDir . '/' . $movieName . '.jpg';
define("FFMPEG_COMMAND2", ' ffmpeg -ss 5 -i %s -vframes 1 -f image2 %s 2>&1');
$command2 = sprintf(FFMPEG_COMMAND2, $movieFile, $imageFile);
$log = null;
exec($command2, $log);
変換された動画ファイルの存在を確認したら、元のアップロード動画は削除します。
if (file_exists($movieFile)) {
unlink($moveFile);
}
これで、moviesにある動画をVideoタグで表示したら完了です。
upload.phpのタグを書き換えます。
<video src="<?php echo $movieFile;?>" controls width="720" poster="<?php echo $imageFile;?>"></video
最後まで読んでいただきありがとうございました。
上記のファイルはgithubでも確認できます。
Github
Discussion