👋

PHPを使った動画ファイルアップロードとFFMPEGを使ったMP4変換

2023/04/13に公開

はじめに

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を読み込んでいます。

index.php
<?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側でフォームの情報を受け取る必要があります。
少し長いですが、後で説明をします。

upload.php
<?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に以下を追加で記載します。

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