work.log

元エンジニアの備忘録的ブログ

Web経由でFFmpegのエンコード処理を行い進捗状況を表示するメモ

投稿:

Web 経由で FFmpeg のエンコード処理を走らせながら進捗状況をブラウザに表示する方法についてのメモです。

動画アップロードサイトではユーザーに動画を Web 経由でアップさせ、エンコードしたりキャプチャ画像を作成したりする機能を管理画面内に設けていたりしますが、そのようなものを自前で実装してみようというお話です。

Web 経由で重い処理を走らせる場合は、

ffmpeg -i in.mp4 -vcodec libx264 -b:v 3000k out.mp4 &

みたいにコマンドの最後に “&” を付けてバックグラウンドで走らせるのが簡単ですが、いつ終わったとかエラーが出ていないかとかの実行後のステータスがわからなくなるので、この進捗までしっかり確認しようというのが今回の目的です。

全体の流れとしては、

  1. 動画をアップロード
  2. FFmpeg で進捗を表示しつつバックグラウンド処理
  3. 終わったら動画とキャプチャ画像をブラウザへ表示

となります。

タイトルにエンコードと書きましたが、サンプルプログラムでは動画のキャプチャ画像作成のみを実装してます。(理由は後ほど…)

動作デモとサンプル

動作デモのスクリーンショットはこんな感じです。

アップロードが始まるとその時の処理内容と進捗状況をプログレスバーで表示するようにしています。

プログレスバーが 100% の状態になるとアップロードした動画と作成したキャプチャ画像が表示され再生可能な状態となります。

ページをリロードする、もしくは「リセット」ボタンを押すとアップロードされたファイルは全て削除されます。

サンプルの動かし方

今回作成したサンプル一式は下記ファイルにまとめてあります。

Perl 製のプログラムなので Web サーバの設定と下記モジュールをインストールすれば動作します。(あと ffmpeg も)

# yum -y install perl-JSON ImageMagick ImageMagick-devel ImageMagick-perl

Web サーバへサンプルをアップロードしたら動画を保存するディレクトリに書き込み権限を追加しておきます。

$ chmod 777 progress_demo/upload

全体のファイル構成はこのようになっています。

progress_demo/
│
├── index.pl
├── embed.pl
├── ffmpeg.pl
├── progress.pl
├── up.php
├── UploadHandler.php
│
├── css/
│   ├── embed.css
│   ├── jquery.fileupload.css
│   └── style.css
│
├── js/
│   ├── jQuery-File-Upload/
│   ├── upload.js
│   ├── video-js/
│   ├── videojs-contrib-hls.min.js
│   ├── videojs-contrib-media-sources.min.js
│   └── videojs-func.js
│
└── upload/ (* Web 経由からの書き込み権限が必要)

進捗状況を表示する部分は ffmpeg.pl と progress.pl になるので次の項ではこの点について書きます。

アップロードの進捗や動画の表示周り(Video.js)については下記記事にまとめてありますので今回は割愛します。

FFmpegの進捗を取得する方法

まず始めは Web 経由でバックグラウンド実行された FFmpeg のステータスをどうやって取得するかについてです。

プログレスバーを動かすには何らかの数値が必要なのですが、今回は FFmpeg が処理中に吐き出すログを利用する事にしました。

FFmpeg は処理中等に下記のようなログを表示しますがここの frame の値と drop の値を取れば進捗が算出できます。

frame=   24 fps=5.3 q=24.8 size=N/A time=00:00:24.00 bitrate=N/A dup=0 drop=647 speed=5.29x

全体の frame 値は下記コマンドで調べる事が出来るので、事前に取得しておいて定期的にこの frame 値を計算させてブラウザに応答を返す感じです。(nb_frames の値)

ffprobe -show_streams -print_format json in.mp4

~ 省略 ~
            "bit_rate": "128000",
            "nb_frames": "50594",
            "disposition": {
~ 省略 ~

これらを組み合わせると、

  1. ffprobe コマンドを実行して nb_frames の値を取得
  2. ffmpeg コマンドを実行しながら nb_frames, frame, drop の値で進捗率を計算

となります。

nb_frames の取得は ffmpeg.pl で行い取得結果は JSON 形式でログを保存。その後、処理を progress.pl に引き継ぎ、3秒置きに進捗状況を計算させる感じです。

殆ど、コマンドを実行しているだけですが progress.pl の進捗計算はこのような処理です。

sub get_progress {

    ## ffprobe のコマンド実行結果を読み込み (JSON)
    open(IN, "< $conf->{json}");
    my @read_data = <IN>;
    close(IN);
    chomp(@read_data);

    my $json = from_json($read_data[0]);

    ## JSON ファイルから nb_frames の値を取得
    foreach (@{$json->{streams}}) {

        if ($_->{codec_type} =~ /video/) {
            $progress->{nb_frames} = $_->{nb_frames};
            last;
        }

    }

    sleep(3);

    ## ffmpeg の出力するログを読み込み
    open(IN, "< $conf->{log}");
    my @in = <IN>;
    close(IN);
    chomp(@in);

    my @tmp = ();

    foreach (@in) {

        if ($_ =~ /(error|fail|Invalid|No such file or directory)/) {

            ## skip error
            if ($_ =~ /decode_slice_header error/) { next; }

            $progress->{err} = 1;
            $progress->{msg} = "Error: check $conf->{log}";
            return;
        }

        if ($_ =~ /frame/) {
            $_ =~ s/\r/::/g;
            push(@tmp, split('::', $_));
        }
    }

    ## ログから frame, drop の値を取得
    my $cur = pop(@tmp);
    $cur =~ /(frame=(\s+)?(\d+)).+(drop=(\s+)?(\d+))/;

    if ($3) { $progress->{frame} = $3; }
    if ($6) { $progress->{drop} = $6; }

    ## 進捗率を計算
    $progress->{cur} = int(($progress->{frame} + $progress->{drop}) / $progress->{nb_frames} * 100);

    return;

}

エラー処理がしっかりしていませんがこんな感じにシンプルな実装です。

FFmpegの進捗率をプログレスバーで表示する

上記で ffmpeg の進捗率が取れるようになったので、これをブラウザに表示させます。

ざっくりと言えば、ffmpeg.pl を Web から叩いてバックグラウンド処理を開始、進捗率を返す URL(progress.pl)が返って来るのでそれを jQuery で定期的にアクセスさせるという流れです。

この処理は upload.js でこんな風に実装しています。

function reqFFmpeg() {

	$.ajax({
		type: 'GET',
		url : 'ffmpeg.pl'
	}).done(function(callback){
		$('.msg').empty().text('サムネイル画像を作成しています...');
		/* progress.pl の URL が返って来るので、reqFFmpegProgress() にそれを渡す */
		reqFFmpegProgress(callback.url);
	});

	return;

}

function reqFFmpegProgress(url) {

	var i = 0;

	$.ajax({
		type : 'GET',
		url  : url
	}).done(function(data){

		/* エラーが起きたら処理中止 */
		if (data.err) {
			i = 100;
			$('.progress-bar').addClass('progress-bar-danger').text('error');
			console.log(data);
		} else {
			/* プログレスバーを進める */
			i = data.cur;
			var progress = Math.round(parseInt(50, 10) + parseInt(data.cur, 10) / 2);
			$('.progress-bar').css('width', progress + '%').text(progress + '%');

			/* 完了しない場合は reqFFmpegProgress() を再度実行 */
			if (i != 100) {
				reqFFmpegProgress(url);

			/* 完了後の処理 */
			} else {

				$('.msg').empty().text('アップロードが完了しました。');

				$('.capture').show();
				$('.video').append('<iframe src="embed.pl" frameborder="0" scrolling="no" allowfullscreen></iframe>');

				var ts = new Date().getTime();
				for (var i=1; i<=data.file; i++) {
					var ahref = '<a href="upload/capture/' + i + '.jpg?' + ts + '" target="_blank">';
					var img = '<img src="upload/capture/' + i + '.jpg?' + ts + '">';
					$('.capture .preview').append('<div class="col-md-2 thumbnail">' + ahref + img + '</a></div>');
				}

			}

		}

	}).fail(function(err){
		i = 100;
	});

	return i;

}

$(function () {

	'use strict';

	$('.reset').hide();
	var url = 'up.php?upload=upload&filename=demo';

	$('#fileupload').fileupload({
		url: url,
		dataType: 'json',
		maxChunkSize: 1047552,
		maxFileSize: 2147483648,
		acceptFileTypes: /\.mp4$/i,
	})
	.on('fileuploadstart', function (e) {
		$('.fileinput-button').hide();
		$('.reset').show();
		$('.msg').empty().text('アップロード中です...');
	})
	.on('fileuploadprogressall', function (e, data) {
		var progress = parseInt(data.loaded / data.total * 100 / 2, 10);
		$('.progress-bar').css('width', progress + '%');
		$('.progress-bar').text(progress + '%');
	})
	.on('fileuploaddone', function (e, data) {
		var file = data.result.files[0];
		/* mp4 がアップロードされたら reqFFmpeg() を実行 */
		if (file.name.match(/\.mp4/)) {
			reqFFmpeg()
		}
	})
	.on('fileuploadchunksend', function (e, data) {})
	.on('fileuploadchunkdone', function (e, data) {})
	.on('fileuploadchunkalways', function (e, data) {});

});

これで progress.pl へ定期アクセスし ffmpeg の処理が完了するまで進捗を管理しています。

力技な部分もありますがこれで Web 経由でも ffmpeg の進捗状況がわかるようになりました。

FFmpegでn秒毎にキャプチャ画像を作成する処理

progress.pl の最後でキャプチャ画像に関する処理を行っていますが、キャプチャ画像はn秒毎というようなオプション指定が出来なかったのでそれを実現する処理と、再生バーにマウスオーバーした時のプレビュー画像を作る処理を ImageMagick で行っています。

キャプチャ画像は5秒間隔で作るようにしていて、progress.pl の “capture_sec => 5”、videojs-custom.js の “var capture_sec = 5;” に固定値を設定してあります。

Video.js の話になってしまいますが、再生バーにプレビュー画像を表示する為の画像は下記のようなもので一枚に連結させ、スライド位置で一緒に写真もずらしている形となります。

サーバでエンコードをする際の注意

サンプルではエンコードではなく画像のキャプチャ処理のみを実装していますが、この部分をエンコードに置き換えても上手く動作すると思います。

ただし、エンコード自体が非常に重い処理ですのでサーバスペックが高くないと実運用は厳しいかなという所感です。

エンコードはエンコード専用マシンを使うか、キューを利用した完全なバックグラウンド処理。今回のような進捗を確認するような処理はキャプチャ画像の作成か、トランスコード等の軽作業がいい所じゃないかなと思います。

関連記事

コメント

コメントを残す

よく読まれている記事

  • 本日
  • 週間
  • 月間