Step Functions Express Workflows を使って動画のフレーム分割をやってみた
はじめに
AWS Step Functions Express Workflowsは、短時間(最大5分間)で終わるタスクを大量にハイパフォーマンスで高速に実行したいときに最適なワークフロー管理サービスです。
リアルタイム処理や軽量だが大量に行いたいバッチワークロードに向いています。
今回の検証題材として、動画ファイルをフレーム単位で静止画に分割する処理を選びました。
動画のフレーム分割とは?
動画ファイルは連続する静止画(フレーム)の集合で構成されています。
動画のフレーム分割とは、この静止画を一定間隔ごとに抽出して画像ファイル(jpg等)として保存する処理を指します。
用途例としては:
- 動画サムネイルの自動生成
- 動画解析・モニタリング
- 動画を静止画ベースで編集し再結合
- 機械学習用データセット作成
たとえば動画を毎回アップロードするたびにフレーム分割したいという要望の場合(あまり多くはないかもしれませんが)、ハイパフォーマンスで大量かつ高速に行う必要があるため
Step Functions Express Workflowsとの相性が良いと思いやってみました。
使用環境
今回使用した技術構成は以下のとおりです。
サービス | 用途・役割 |
---|---|
AWS SAM (Serverless Application Model) | デプロイ・リソース管理のフレームワーク |
AWS Step Functions Express Workflows | ワークフロー制御(並列・短時間処理) |
AWS Lambda (Python 3.12) | 動画メタデータ取得、フレーム抽出処理を担当 |
ffmpeg | Lambda内で subprocess 経由で呼び出す動画処理ライブラリ。Lambdaレイヤーとして提供 |
Amazon S3 | 入力動画ファイルと、出力された静止画(jpg)ファイルの保存先 |
Amazon EventBridge Rule | S3への動画アップロードをトリガーにStep Functionsを起動するイベントルール |
SAMを使い、Lambda関数・レイヤー・S3バケット・EventBridgeルール・Step Functions をすべて一括構築できる形にしています。
SAMプロジェクト構成
.
├── README.md
├── __init__.py
├── env.json
├── functions
│ ├── extract_frame # 指定秒数単位のフレーム分割関数
│ │ ├── app.py
│ └── get_video_duration # 動画の秒数とFPSを取得する関数
│ ├── app.py
├── layers # ffmpegのバイナリを含めたレイヤー
│ └── ffmpeg
│ ├── ffmpeg # ダウンロードしたバイナリ(後述)
│ └── ffprobe # ダウンロードしたバイナリ(後述)
├── samconfig.toml
├── template.yaml
ffmpegのbinをレイヤー化
python3.6環境なら「ffmpeg-lambda-layer」を使うとレイヤーを自分で準備する必要がないので楽かもしれません。
今回は以下サイトからスタティックバイナリを直接ダウンロードし、SAMプロジェクトに含めてあげることでsam build時にパッケージ化してもらいレイヤー化しました。
スタティックバイナリをダウンロードする手順は以下のブログを参考にしました。ただし今回はzip化する必要がありません(SAMで上手いことやってくれます)
ダウンロードしたスタティックバイナリ(ffmpeg, ffprobe)をlayers/ffmpeg/ に配置し以下のようにテンプレートファイルで指定します。重要なのはMetadataでビルドメソッドを指定しないとsam build時にパッケージ化してくれません。 必ずつけましょう。
## Layer
FfmpegLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: video-frame-splitter-ffmpeg-layer
ContentUri: ./layers/ffmpeg/
CompatibleArchitectures:
- x86_64
CompatibleRuntimes:
- python3.12
Metadata:
BuildMethod: python3.12
## 関数
VideoFrameSplitterGetVideoDurationFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: video-frame-splitter-get-video-duration
CodeUri: functions/get_video_duration/
Handler: app.lambda_handler
Layers:
- !Ref FfmpegLayer #レイヤーとして参照する
Policies:
- S3ReadPolicy:
BucketName: !Ref SourceBucket
これにより、各Lambdaから/opt/python/ffmpeg
等で呼び出せるようになリます。
Step Functions Express Workflows構成と全体フロー
今回の処理フローは以下のとおりです。
フローの説明
- 動画ファイルがS3にアップロードされる
- EventBridgeルールにより、S3オブジェクト作成イベントを検知
- Step Functions Express Workflowが起動する
- 最初のLambdaタスクで、ffprobeを使い動画の「総秒数(duration)」と「フレームレート(fps)」を取得する
- Passステートで秒単位リストを生成し、Inline Mapステートで並列Lambda実行を設計
- 各Lambdaタスクで対象秒間のフレームをfps枚分抽出し、jpgファイルとしてS3に格納
例: 20秒で30fpsの動画の場合、20個のLambdaが起動しそれぞれの関数で1秒間ずつ30枚のjpgとして抽出する 20秒✖︎30fps = 600枚のjpgをS3バケットに保存
Stepfunctionsについて
ワークフロー定義内で、JSONata記法を使用しています。
(ただし本記事ではJSONata自体の詳細説明は省略します)
## Step Functions Express Workflow
VideoFrameSplitterStateMachine:
Type: AWS::Serverless::StateMachine
Properties:
Name: video-frame-splitter-sm
Type: EXPRESS
Role: !GetAtt VideoFrameSplitterStateMachineRole.Arn
Definition:
StartAt: GetVideoDuration
States:
GetVideoDuration:
Type: Task
Resource: !GetAtt VideoFrameSplitterGetVideoDurationFunction.Arn
Next: GenerateFrameList
QueryLanguage: JSONata
Assign:
bucket: "{% $states.result.bucket %}"
key: "{% $states.result.key %}"
fps: "{% $states.result.fps %}"
GenerateFrameList:
Type: Pass
Next: ExtractFrames
QueryLanguage: JSONata
Output:
snapshotTimes: "{% $range(0, $states.input.duration, 1) %}"
ExtractFrames:
Type: Map
End: true
QueryLanguage: JSONata
Items: "{% $states.input.snapshotTimes %}"
ItemProcessor:
StartAt: ExtractFrame
States:
ExtractFrame:
Type: Task
Resource: !GetAtt VideoFrameSplitterExtractFrameFunction.Arn
End: true
QueryLanguage: JSONata
Arguments:
bucket: "{% $bucket %}"
key: "{% $key %}"
second: "{% $states.input %}"
fps: "{% $fps %}"
全ては解説しませんがポイントだけまとめると
-
bucket,key,fps値を変数に格納し InlineMapのLambda関数内でArgumentsとして使用
-
1つ目のLambda関数で抽出した動画のduration(秒数)を元にPassステートで配列(snapshotTimes)を作成、たとえば動画が10秒だと以下のようになる
[0,1,2,3,4,5,6,7,8,9]
-
InlineMapで配列の要素を各関数に渡していくことで1秒ずつLambda関数を起動し、fps枚数分の静止画を保存
という仕組みになっています。
8秒の動画で検証
まず8秒(正確には7.xxx秒)で30fpsのテスト用動画で検証しました。
- 約3秒以内にワークフローが終了しています。
- 8秒 × 30fps = 約240枚のフレームがS3バケットに保存されていることを確認
爆速かつスムーズな処理を体感できました。
ちなみに動画の中身はプライベートで撮影した野生のリスが柵から木にジャンプする様子ですがフレームでジャンプシーンを捉えていました!
フレーム分割した静止画
8分の動画で検証
次に8分40秒(520秒)で30fpsの動画でも検証を実施。
結果として、
- Lambda関数内部でffmpegエラー(exit 228)が発生
- エラー内容は「CalledProcessError」
という問題が起こりました。
最初に設定していた
- メモリ 1024MB
- エフェメラルストレージ: 512MB(デフォルト)
では/tmp
のエフェメラルストレージに動画と静止画を一時ダウンロードするための容量が不足していたことが原因
この問題への対応策として、
- Lambdaのエフェメラルストレージを4096MBに拡張
- Lambdaのメモリサイズも4096MB(4GB)に増加
- 約1分30秒程度でエラーなくワークフローが終了しました。
- 520秒 × 30fps = `約15600枚のフレームがS3バケットに保存されていることを確認
20分の動画で検証
次に約20分(1200秒)で30fpsの動画でも検証を実施。
結果として、状態遷移数800程度で5分を超過し、タイムアウトとなりました。この構成でフレーム分割するのは10分程度の動画が限界ですね
まとめ
今回、Step Functions Express Workflowsを使って動画フレーム分割に取り組みました。
- SAM+レイヤー構成でシンプルに環境構築可能
- Express Workflowsを使用することで10分以下の長尺ではない動画なら一瞬でフレーム分割可能
- 長尺動画になるとExpress Workflows用途向きではなくStandard Workflows向きでLambdaや他のアーキテクチャ設計が重要
という知見を得ることができました。
Step Functions Express Workflowsは今回のシチュエーションよりさらに大量に状態遷移が必要なリアルタイムデータやストリーム処理にも耐えられるはずです。今回の処理だけでもバッチ処理やデータ処理基盤にも活用できる強力なサービスであることを強く実感しました。