複数のカメラからのビデオ断片を結合し、それらを時間内に同期する

前の記事でレビューしたリモート監視システム(DLS)は、Kurentoメディアサーバーを使用してメディアストリームを制御します。これにより、各ストリームが個別のファイルであるストリームの記録が可能になります。 問題は、試験プロトコルを表示するときに、時間の流れを同期しながら3つのストリームを同時に再生する必要があることです(テスト対象のウェブカメラの音声、試験官のウェブカメラの音声および対象のデスクトップ)。いくつかの断片に。 この記事では、この問題を解決する方法と、1つのbashスクリプトだけでWebDAVサーバー上のビデオのストレージを整理する方法について説明します。



VDNビデオアーカイブの再生



Kurentoメディアサーバーは、クライアントから送信されるメディアストリームを元の形式で保存します。実際には、ストリームはwebmファイルにダンプされ、vp8およびvorbisコーデックが使用されます(mp4形式もサポートされます)。 これは、保存されたファイルのビデオ解像度とビットレートが可変であるという事実につながります。 WebRTCは、通信チャネルの品質に応じて、ビデオおよびオーディオストリームのエンコードパラメーターを動的に変更します。 各監督セッション中に、クライアントは何度も接続と切断を行うことができます。これにより、各カメラと画面に多数のファイルが表示され、これらのフラグメントがすべて接着している場合は同期が取れなくなります。



そのようなビデオを正しく再生するには、次の手順を実行する必要があります。





その結果、トランスコーディング後にのみレコードを再現することが可能になりますが、このタスクでは、これは許容可能なオプションです。 記録後、同じ秒で記録を表示できる人はいません。 さらに、遅延トランスコーディングにより、監督セッション中のサーバーの負荷が軽減されます。 トランスコーディングプロセスは、負荷が最小の夜にスケジュールできます。



VDSの各監督セッションには独自の一意の識別子があり、サブジェクトと監督の間の接続を確立するときにKurentoに送信されます。 このセッションでは、技術的な理由または監督者の主導で中断および再開できる3つのスレッドが作成されます。 「timestamp_camera-session.webm」という形式(正規表現の形式のマスク^ [0-9] + _ [a-z0-9] +-[0-9a-f] {24 } .webm $)、ここで、タイムスタンプはミリ秒単位のファイル作成のタイムスタンプです。 camera-対象のウェブカメラ(camera1)、監督者のウェブカメラ(camera2)、およびデスクトップ画像付きストリーム(スクリーン)からのストリームを区別するカメラ識別子。 session-監督セッションの識別子。 各監督セッションの後、多くのビデオフラグメントが保存されます。ビデオのフラグメント化の可能なオプションを下の図に示します。



ビデオの断片化の可能なオプション



1〜12の数字はタイムスタンプです。 太線-さまざまな長さのビデオクリップ。 破線-追加するフラグメントの欠落; 空のギャップ-ビデオクリップがない時間間隔は、最終ビデオから除外する必要があります。



出力ビデオファイルは、320x240(4:3)の解像度を持つ2台のカメラと768x480(16:10)の解像度を持つ1つの画面の3つの部分からなるブロックです。 元の画像は、指定したサイズに拡大縮小する必要があります。 アスペクト比がこの形式に対応していない場合、指定された長方形の中央に画像全体を合わせ、空の領域を黒で塗りつぶします。 その結果、カメラの場所は次の図のようになります(青と緑-Webカメラ、赤-デスクトップ)。



画面上のカメラの配置



その結果、各監督者セッションは、多くの抜粋ではなく、セッション全体の記録を含む1つのビデオファイルのみを持ちます。 さらに、出力ファイルが占有するスペースが少なくなります。 ビデオのフレームレートは、1〜5フレーム/秒の最小許容数まで減少します。 生成されたファイルはWebDAVサーバーにアップロードされ、SDNは必要なアクセス権を考慮して、適切なインターフェイスを介してこのファイルを要求します。 WebDAVプロトコルは非常に一般的です。ストレージは何でもかまいませんが、これらの目的でYandex.Diskを使用することもできます。



これらのすべての機能の実装は、ffmpegおよびcurlユーティリティが追加で必要な小さなbashスクリプトに適合させることができました。 まず、ビデオファイルを動的解像度とビットレートでトランスコードし、各カメラに必要なパラメーターを設定する必要があります。 ソースビデオファイルを特定の解像度と1秒あたりのフレームレートでトランスコードする機能は次のようになります。



scale_video_file() { local in_file="$1" local out_file="$2" local width="$3" local height="$4" ffmpeg -i "$in_file" -c:v vp8 -r:v ${FRAME_RATE} -filter:v scale="'if(gte(a,4/3),${width},-1)':'if(gt(a,4/3),-1,${height})'",pad="${width}:${height}:(${width}-iw)/2:(${height}-ih)/2" -c:a libvorbis -q:a 0 "${out_file}" }
      
      





ffmpegスケールフィルターには特に注意を払う必要があります。アスペクト比が異なる場合でも、結果の空のスペースを黒で塗りつぶして、特定の解像度に画像を調整できます。 FRAME_RATE-フレームレートが設定されるグローバル変数。



次に、ビデオファイル間のギャップを埋めるためのスタブファイルを作成する関数が必要です。



 write_blank_file() { local out_file="$1" [ -e "${out_file}" ] && return; local duration=$(echo $2 | LC_NUMERIC="C" awk '{printf("%.3f", $1 / 1000)}') local width="$3" local height="$4" ffmpeg -f lavfi -i "color=c=black:s=${width}x${height}:d=${duration}" -c:v vp8 -r:v ${FRAME_RATE} -f lavfi -i "aevalsrc=0|0:d=${duration}:s=48k" -c:a libvorbis -q:a 0 "${out_file}" }
      
      





これにより、特定の解像度、継続時間(ミリ秒単位)、フレームレートのビデオトラックと、無音のオーディオトラックが作成されます。 これらはすべて、メインビデオクリップと同じコーデックでエンコードされます。



結果として各カメラのビデオフラグメントを結合する必要があります。これには、次の関数を使用します(OUTPUT_DIR-ビデオフラグメントのあるディレクトリへのパスを含むグローバル変数)。



 concat_video_group() { local video_group="$1" ffmpeg -f concat -i <(ls "${OUTPUT_DIR}" | grep -oe "^[0-9]\+_${video_group}$" | xargs -I FILE echo "file ${OUTPUT_DIR%/}/FILE") -c copy "${OUTPUT_DIR}/${video_group}" ls "${OUTPUT_DIR}" | grep -oe "^[0-9]\+_${video_group}$" | xargs -I FILE rm "${OUTPUT_DIR%/}/FILE" }
      
      





ビデオファイルの継続時間をミリ秒単位で決定する関数も必要です。ffmpegパッケージのffprobeユーティリティがここで使用されます。



 get_video_duration() { local in_file="$1" ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${in_file}" | LC_NUMERIC="C" awk '{printf("%.0f", $1 * 1000)}' }
      
      





トランスコーディング機能、特定の長さの欠落フラグメントを作成する機能、およびこれらすべてのフラグメントを接着する機能があるため、異なるカメラからのビデオフラグメントの同期機能が必要になります。これは、どのフラグメントとどのくらいの時間を再作成するかを決定します。 アルゴリズムは次のとおりです。



  1. ファイル名の最初の部分であるタイムスタンプでソートされたビデオクリップを含むファイルのリストを取得します。
  2. リストを上から下に表示し、同時に「timestamp:flag:file_name」という形式の別のリストを作成します。 このリストの本質は、各ビデオファイルのすべての開始点と終了点をマークすることです(ビデオの断片化の図解を参照してください)。 この例では、これは次のリストになります。

     1:1:camera1-session.webm 3:-1:camera1-session.webm 7:1:camera1-session.webm 10:-1:camera1-session.webm 2:1:camera2-session.webm 5:-1:camera2-session.webm 8:1:camera2-session.webm 10:-1:camera2-session.webm 3:1:screen-session.webm 6:-1:screen-session.webm 8:1:screen-session.webm 12:-1:screen-session.webm
          
          



  3. 結果のリストには、ビデオクリップの最初のリストの最初と最後のファイルの期間がゼロのレコード(同一のタイムスタンプ)を追加する必要があります。 これは、欠落している中間ビデオクリップを計算する段階で必要になります。
  4. どのカメラからもビデオがない場合、フラグメントの先頭と末尾に対応するエントリをリストに追加します。 この例では、エントリは「6:1:...」および「7:-1:...」になります。
  5. 結果のリストは3つの部分に分割され、各カメラのリストを取得します。 各リストを調べて、それを逆にします。 既存のフラグメントのリストではなく、欠落しているフラグメントのリストを取得する必要があります。
  6. 結果のリストを「timestamp:duration:file_name」の形式に変換して、不足しているビデオクリップの作成に使用できるようにします。


このアルゴリズムは、次の一連の関数によって実装されます。



 #   # input: timestamp:flag:filename # output: timestamp:duration:filename find_spaces() { local state=0 prev=0 sort -n | while read item do arr=(${item//:/ }) timestamp=${arr[0]} flag=${arr[1]} let state=state+flag if [ ${state} -eq 0 ] then let prev=timestamp elif [ ${prev} -gt 0 ] then let duration=timestamp-prev if [ ${duration} -gt 0 ] then echo ${prev}:${duration}:${arr[2]} fi prev=0 fi done } #         zero_marks() { sort -n | sed '1!{$!d}' | while read item do arr=(${item//:/ }) timestamp=${arr[0]} for video_group in ${VIDEO_GROUPS} do echo ${timestamp}:1:${video_group} echo ${timestamp}:-1:${video_group} done done } #  ,         blank_marks() { find_spaces | while read item do arr=(${item//:/ }) first_time=${arr[0]} duration=${arr[1]} let last_time=first_time+duration for video_group in ${VIDEO_GROUPS} do echo ${first_time}:1:${video_group} echo ${last_time}:-1:${video_group} done done } #    : timestamp:duration:filename generate_marks() { ls "${OUTPUT_DIR}" | grep "^[0-9]\+_" | sort -n | while read video_file do filename=${video_file#*_} timestamp=${video_file%%_*} duration=$(get_video_duration "${OUTPUT_DIR%/}/${video_file}") echo ${timestamp}:1:${filename} echo $((timestamp+duration)):-1:${filename} done | tee >(zero_marks) >(blank_marks) } #     ,     fragments_by_groups() { local cmd="tee" for video_group in ${VIDEO_GROUPS} do cmd="${cmd} >(grep :${video_group}$ | find_spaces)" done eval "${cmd} >/dev/null" } #    write_fragments() { while read item do arr=(${item//:/ }) timestamp=${arr[0]} duration=${arr[1]} video_file=${arr[2]} write_blank_file "${OUTPUT_DIR%/}/${timestamp}_${video_file}" "${duration}" $(get_video_resolution "${video_file}") done } #    generate_marks | fragments_by_groups | write_fragments
      
      





不足しているビデオクリップを再作成した後、それらのマージを開始できます。 これを行うには、1つのグループのすべてのビデオファイルを結合する(つまり、1つのカメラIDを持つ)次の関数が必要です。



 concat_video_group() { local video_group="$1" ffmpeg -f concat -i <(ls "${OUTPUT_DIR}" | grep -oe "^[0-9]\+_${video_group}$" | sort -n | xargs -I FILE echo "file ${OUTPUT_DIR%/}/FILE") -c copy "${OUTPUT_DIR}/${video_group}" }
      
      





3つのビデオファイルすべてが時間的に同期されているため、これらのファイルを統合画面の必要な部分に配置して、1つの統合画面に結合する必要があります。



 encode_video_complex() { local video_file="$1" local camera1="$2" local camera2="$3" local camera3="$4" ffmpeg \ -i "${OUTPUT_DIR%/}/${camera1}" \ -i "${OUTPUT_DIR%/}/${camera2}" \ -i "${OUTPUT_DIR%/}/${camera3}" \ -threads ${NCPU} -c:v vp8 -r:v ${FRAME_RATE} -c:a libvorbis -q:a 0 \ -filter_complex " pad=1088:480 [base]; [0:v] setpts=PTS-STARTPTS, scale=320:240 [camera1]; [1:v] setpts=PTS-STARTPTS, scale=320:240 [camera2]; [2:v] setpts=PTS-STARTPTS, scale=768:480 [camera3]; [base][camera1] overlay=x=0:y=0 [tmp1]; [tmp1][camera2] overlay=x=0:y=240 [tmp2]; [tmp2][camera3] overlay=x=320:y=0; [0:a][1:a] amix" "${OUTPUT_DIR%/}/${video_file}" }
      
      





ここでは、ffmpegフィルターを使用して、空の黒い領域(パッド)が作成され、指定された順序でカメラが配置されます。 最初の2台のカメラからの音がミックスされます。



ビデオを処理して出力ファイルを受け取ったら、サーバーにアップロードします(グローバル変数STORAGE_URL、STORAGE_USER、およびSTORAGE_PASSには、それぞれWebDAVサーバーのアドレス、ユーザー名、パスワードが含まれます)。



 upload() { local video_file="$1" [ -n "${video_file}" ] || return 1 [ -z "${STORAGE_URL}" ] && return 0 local http_code=$(curl -o /dev/null -w "%{http_code}" --digest --user ${STORAGE_USER}:${STORAGE_PASS} -T "${OUTPUT_DIR%/}/${video_file}" "${STORAGE_URL%/}/${video_file}") #   ,    201,   - 204 test "${http_code}" = "201" -o "${http_code}" = "204" }
      
      





考慮されるシナリオの完全なコードはGitHubに投稿されています

アルゴリズムの動作を確認するには、次のジェネレーターを使用できます。このジェネレーターは、考慮された例からビデオクリップを作成します。



 #!/bin/bash STORAGE_DIR="./storage" write_blank_video() { local width="$1" local height="$2" local color="$3" local duration="$4" local frequency="$5" local out_file="$6-56a8a7e3f9adc29c4dd74295.webm" ffmpeg -y -f lavfi -i "color=c=${color}:s=${width}x${height}:d=${duration}" -f lavfi -i "sine=frequency=${frequency}:duration=${duration}:sample_rate=48000,pan=stereo|c0=c0|c1=c0" -c:a libvorbis -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: timecode='00\:00\:00\:00': r=30: x=10: y=10: fontsize=24: fontcolor=black: box=1: boxcolor=white@0.7" -c:v vp8 -r:v 30 "${STORAGE_DIR%/}/${out_file}" </dev/null >/dev/null } # camera1 write_blank_video 320 200 blue 2 1000 1000_camera1 write_blank_video 320 200 blue 3 1000 7000_camera1 # camera2 write_blank_video 320 240 green 3 2000 2000_camera2 write_blank_video 320 240 green 2 2000 8000_camera2 # screen write_blank_video 800 480 red 3 3000 3000_screen write_blank_video 800 480 red 4 3000 8000_screen
      
      







その結果、問題は解決され、結果のスクリプトをKurentoサーバーに配置して、スケジュールどおりに実行できます。 作成したビデオファイルをWebDAVサーバーに正常にアップロードしたら、ソースファイルを削除して、後で読みやすい形式で表示するためにビデオをアーカイブできます。




All Articles