プログラミング

FFmpeg.wasmの使い方:ブラウザでアップロードした動画から残像動画を作成(簡易的方法)

投稿日:2022年1月13日 更新日:

FFmpeg.wasmの使い方の一例として、

・ブラウザで動画をアップロード
・動画から音声を抽出
・動画からすべてのフレーム画像を取得
・1枚目の画像をベース画像として各フレームの残差を抽出
・残像動画を作成
・残像動画と音声を合成して表示

するコードを以下に示します。
動画のプレイヤー上で右クリックするとファイルの保存選択ができます。

コード

traceVideo.php

<?php
ini_set('mbstring.internal_encoding' , 'UTF-8');

header('Cross-Origin-Opener-Policy: same-origin');
header('Cross-Origin-Embedder-Policy: require-corp');
?>

<html>
<head>
  <script>
    if (crossOriginIsolated) {
      // Post SharedArrayBuffer
      console.log('crossOriginIsolated');
    } else {
      // Do something else
      console.log('NOT crossOriginIsolated');
    }
  </script>
  <script src="ffmpeg.min.js" ></script>
 </head>

  <body>
    <input type="file" id="fileInput">
    </input><br>

    <video id="my-video" controls="true" crossorigin="anonymous">
    </video>

    <audio id="my-audio" controls="true" crossorigin="anonymous">
    </audio>

    <img id="my-image" >
    </img><br>

    <canvas id="my-canvas" ></canvas> 

    <script src="app6.js"></script>
  </body>
</html>

app6.js

(async () => {
  console.log('app.js');
  const { createFFmpeg, fetchFile } = FFmpeg

  const ffmpeg = createFFmpeg({
    corePath: 'ffmpeg-core.js',
    log: true
  });

  await ffmpeg.load()

  const sizeLimit = 1024 * 1024 * 10; // 制限サイズ
  const fileInput = document.getElementById('fileInput'); // input要素
  let mDuration = 0;
  
  const handleFileSelect = async () => {
    const files = fileInput.files;
    for (let i = 0; i < files.length; i++) {
      if (files[i].size > sizeLimit) {
        alert('ファイルサイズは10MB以下にしてください'); 
        fileInput.value = ''; // inputの中身をリセット
        return; 
      }//if
    }//for

    const t_start = performance.now();

    console.log('size:'+files[0].size );
    console.log('name:'+files[0].name );
    console.log('URL:'+URL.createObjectURL(files[0]) );

    //var mVideo = document.createElement("video");
    var mVideo = document.getElementById("my-video");
    mVideo.src = URL.createObjectURL(files[0]);
    mVideo.addEventListener('loadedmetadata', function() {
      console.log('幅:', mVideo.videoWidth); 
      console.log('高さ:', mVideo.videoHeight); 
      console.log('長さ:', mVideo.duration); 
      mDuration = mVideo.duration;
    });
    mVideo.addEventListener("play", function() {

    }, false);

    const { name } = files[0];
    console.log('name:'+name);

    await ffmpeg.write(name, files[0] );
    var strCmd = '-i '+ name + ' -f mp3 -ab 192000 -vn output.mp3';
    console.log('strCmd:'+strCmd);
    await ffmpeg.run(strCmd);

    const data = await ffmpeg.read('output.mp3')
    //var mAudio = new Audio( URL.createObjectURL(new Blob([data.buffer], { type: 'audio/mp3' })) );
    var mAudio = document.getElementById("my-audio");
    mAudio.src = URL.createObjectURL(new Blob([data.buffer], { type: 'audio/mp3' }));
    //mAudio.play();  

    //strCmd = '-i '+ name + ' -vcodec png image_%03d.png';
    strCmd = '-i '+ name + ' image_%03d.jpg';
    console.log('strCmd:'+strCmd);
    await ffmpeg.run(strCmd);

    let mArray = [];
    let imgData = [];
    let mImage = [];

    let c_ = 1;
    let e_ = 1;
    while(e_==1){
      let str_num = '00' + c_;
      str_num = str_num.substring(str_num.length-3,str_num.length);
      //let str_image = 'image_' + str_num + '.png';
      let str_image = 'image_' + str_num + '.jpg';
        console.log('str_image:'+str_image);

      try {
        imgData[c_] = await ffmpeg.read(str_image); 
        console.log('imgData:'+imgData[c_].length);
        c_ += 1;
      }
      catch (e) {
        c_ -= 1;
        e_ = 0;
        break;
      }
      
    }//
    console.log('c_:'+c_);

    let mFPS = c_ / mDuration;
    console.log('mFPS:'+mFPS);

    let m = 0;
    var ks = 1;
    var ke = c_;
    for (let k = ks; k <= ke; k++) 
    {
      mImage[k]  = new Image();
      //mImage[k].src = URL.createObjectURL(new Blob([imgData[k].buffer], { type: 'image/png' }));
      mImage[k].src = URL.createObjectURL(new Blob([imgData[k].buffer], { type: 'image/jpg' }));

      mImage[k].onload = function() {
        console.log('mImage['+k+']:'+mImage[k].width+', '+mImage[k].height);

        m += 1;
        
      }// onload 

    }// for k
    
    
    var mTimer;
    mTimer = setInterval(() => {
        console.log('mArray:'+m);

        if(m==c_){
            clearInterval(mTimer);
            console.log('Timer cleared.');
            
            mMakeTraceVideo(mImage, mFPS)
        }//
        
    }, 1000);

    const t_end = performance.now();
    console.log('time:'+ (t_end - t_start) +'[ms]' );

  }//

  fileInput.addEventListener('change', handleFileSelect);


  async function mMakeTraceVideo(mImage, mFPS){
    let images = await generateTraceImages(mImage)
    const video = await generateVideo(images, mFPS)
    const objectUrl = createObjectUrl(, { type: 'video/mp4' })
    insertVideo(objectUrl)
  }//

  async function generateTraceImages(mImage) {
    console.log('generateTraceImages');

    let mArray = [];

    let w_ = mImage[1].width;
    let h_ = mImage[1].height;
    const canvas_base = document.createElement('canvas')
    canvas_base.width = w_;
    canvas_base.height = h_;
    let ctx_base = canvas_base.getContext("2d");
    ctx_base.clearRect(0, 0, canvas_base.width, canvas_base.height)
    ctx_base.drawImage(mImage[1], 0, 0, w_, h_)
    let src_base = ctx_base.getImageData(0, 0, w_, h_)
    let dst = ctx_base.createImageData(w_, h_)

    for (let i = 0; i < src_base.data.length; i += 4) {
      dst.data[i+0] = src_base.data[i+0]
      dst.data[i+1] = src_base.data[i+1]
      dst.data[i+2] = src_base.data[i+2]
      dst.data[i+3] = src_base.data[i+3]
    }//


    let ks = 1;
    let ke = mImage.length
    console.log('ke:'+ke);
    for (let k = ks; k < ke; k++) 
    {

        const canvas_k = document.createElement('canvas')
        canvas_k.width = w_;
        canvas_k.height = h_;
        let ctx_k = canvas_k.getContext("2d");
        ctx_k.clearRect(0, 0, canvas_k.width, canvas_k.height)
        ctx_k.drawImage(mImage[k], 0, 0, w_, h_)

        let src_k = ctx_k.getImageData(0, 0, w_, h_)
        let dst_k = ctx_k.createImageData(w_, h_)
        
        if(k%3==0){
          for (let i = 0; i < src_k.data.length; i += 4) {

            var e = Math.abs(src_k.data[i+0] - src_base.data[i+0])
                   +Math.abs(src_k.data[i+1] - src_base.data[i+1])
                   +Math.abs(src_k.data[i+2] - src_base.data[i+2])
  
            if( e >= 40){
              dst.data[i+0] = src_k.data[i+0]
              dst.data[i+1] = src_k.data[i+1]
              dst.data[i+2] = src_k.data[i+2]
            }
            
          }//  
        }//
        //ctx_k.putImageData(dst, 0, 0)

        for (let i = 0; i < src_k.data.length; i ++) {
            dst_k.data[i] = dst.data[i];
        }
        for (let i = 0; i < src_k.data.length; i += 4) {

          var e = Math.abs(src_k.data[i+0] - src_base.data[i+0])
                 +Math.abs(src_k.data[i+1] - src_base.data[i+1])
                 +Math.abs(src_k.data[i+2] - src_base.data[i+2])

          if( e >= 40){
            dst_k.data[i+0] = src_k.data[i+0]
            dst_k.data[i+1] = src_k.data[i+1]
            dst_k.data[i+2] = src_k.data[i+2]
          }         
        }//  
        ctx_k.putImageData(dst_k, 0, 0)

        const dataUrl = canvas_k.toDataURL()
        mArray[k] = dataUrl
    }

    return mArray;
  }

  async function generateVideo(images, mFPS) {
    console.log('generateVideo');

    images.forEach(async (image, i) => {
      await ffmpeg.write(`gimage${i}.png`, image)
    })
    

    let str_cmd = '-r ' + mFPS + ' -i gimage%d.png -pix_fmt yuv420p output.mp4'
    await ffmpeg.run(str_cmd)

    let str_cmd2 = '-i output.mp4 -i output.mp3 output2.mp4'
    await ffmpeg.run(str_cmd2)

    const data = ffmpeg.read('output2.mp4')
    return data
  }

  function createObjectUrl(array, options) {
    console.log('createObjectUrl');
    const blob = new Blob(array, options)
    const objectUrl = URL.createObjectURL(blob)
    return objectUrl
  }

  function insertVideo(src) {
    console.log('insertVideo');
    const video = document.createElement('video')
    video.controls = true

    video.onloadedmetadata = () => {
      document.body.appendChild(video)
    }

    video.src = src
  }
  
})()

こちらで動作確認できます。
https://g-llc.co.jp/grayVideo.php

残像動画の例

実写動画のサンプルがなかったので自作ゲームのキャプチャで一例を。
右から左へ走りながらショットガンを撃つキャラの残像動画です。
アイデア次第で面白い動画が作れると思います。

実写動画がありました。
ゴルフのパットです。


ボールの壁当て

-プログラミング

執筆者:

関連記事

no image

ショーモナイノ/ ソースコード(クライアントサイド)

サーバサイドのソースコードを公開したところ、結構ビュー数が伸びているようです。なのでクライアントサイドも公開しておきます。何かの役に立てればと思います。 内容はとんでもないジャンクコードとなっています …

iPhoneアプリ公開でAppStoreにて言語が英語になる場合の対処 | Xcode

日本語にしか対応していないアプリを作成してAppStoreConnectでも言語を日本語しか選択していないのに、公開したらAppStoreでの言語表記が「英語」に。日本語にしたい場合の対処法です。[対 …

no image

ショーモナイノ/ ソースコード(サーバーサイド)

ショーモナイノのコードを公開していないかとのお問い合わせを頂きました。GitHubでの公開を検討しましたが、書き散らかした粗末なコードをGitHubに置くべきではないと判断しました。代わりに自分のブロ …

FFmpeg.wasmの使い方。クロスオリジンアイソレーション(COOP,COEP)って何ですの?

今さらですがJavascriptでFFmpegが使えるようになっているらしいとの情報を得て早速試してみました。FFmpeg.wasmというらしいです。公開された当初は容易に使用できたらしいですが、現在 …

FFmpeg.wasm使い方: 動画をアップロードして音声を抽出する

FFmpeg.wasmの使い方の一例として、動画をアップロードしてその音声を抽出したmp3を出力してみます。処理が終わると音声が自動で再生されます。音声のプレイヤー上で右クリックするとファイルの保存選 …

スポンサーリンク