Opus変換と永続ストレージ
概要
internal/transcodeは、TUSアップロード完了後にWorkerが行う処理を実装する。実際のffmpeg/ffprobe実行は、ネットワークも認証情報も持たない隔離コンテナcmd/transcoderが担う(詳細設計はtranscode-sandboxを参照)。処理はinternal/transcode/worker.goのWorker.Workが起点で、次の順に進む。
db.Queries.GetUploadでアップロード行(original_keyを含む)を取得する(読み取りのみ、トランザクション不要)。job.Args.Formatがtranscodeproto.IsAllowedFormatの許可リストに含まれることを再検証する(appのマジックバイト判定を信用しきらない多層防御。境界を越えてジョブとして積まれた値の再チェック)。transcodeAudio(audio.go)が、DBトランザクションの外側で、Garageから元ファイルをダウンロード→共有spoolボリューム上に新規ジョブディレクトリを作成し隔離コンテナへハンドオフ→result.jsonを待機→戻り値を実測値と突き合わせて検証(validateAndReadOutput、handoff.go)→検証を通ったoutput.opusを永続ストレージへアップロード、を行う。finishUpload(service.go)がトランザクション内でuploadsテーブルを更新し(converted_key・duration_seconds・converted_size・status=done)、workがpublish_pendingならpublishedに進める。- DBコミット成功後、一時ストレージ上の元ファイルを削除する(失敗してもログのみでジョブは成功扱い)。変換自体が失敗した場合(フォーマット再検証失敗・隔離コンテナ側の失敗・戻り値検証失敗を含む)は、
FailUploadで即座にuploads.status=failedを記録し、Riverによる再試行はしない。詳しい設計はupload-hardeningを参照。
なぜダウンロード/変換/アップロードをDBトランザクションの外に出したか
隔離コンテナとのやり取りやS3への読み書きは数秒〜数十分(最大6時間分の音声)かかりうる外部IOである。これをPostgresのトランザクション内で行うと、その間ずっとトランザクションが開いたままになり、ロックやコネクションプールを圧迫する。そのため、外部IOを伴うtranscodeAudioはトランザクションの外で完結させ、DB更新(finishUpload)だけを短いトランザクションにまとめている。
ビットレートとコーデック
固定128kbpsのOpus(internal/transcode/handoff.goのopusBitrateKbps、internal/encoderの同名定数と同じ値)。バイノーラル録音を含む高品質なASMR音声を想定した値(DEV.mdの「固定ビットレート」要件に対する具体化)。実際のffmpeg呼び出しコマンドとセキュリティ関連フラグの詳細はtranscode-sandboxを参照。
高域が削れすぎる等のエンコード品質そのものの調整にはlab/ffmpeg-lab/(使い捨てのffmpeg実験用Docker環境)を使う。
ffmpeg依存はworkerから隔離コンテナへ移した
Workerはffmpeg/ffprobeを一切実行しない(backend/cmd/worker/Dockerfileにffmpegを含めない)。実行するのは隔離コンテナcmd/transcoderのみ。経緯・脅威モデル・共有ボリュームプロトコルの詳細はtranscode-sandboxを参照。
永続ストレージ(source / dest の2系統)
internal/transcode.ObjectStoreは{Client *s3.Client, Bucket string}のシンプルな組。Workerは2つ持つ。
- source: 一時ストレージ(Garage、
TMP_AUDIO_*環境変数)。TUSアップロードされた元ファイルの取得元。 - dest: 永続ストレージ(Cocot Storage、
COCOT_STORAGE_*環境変数)。本番はCloudflare R2、開発中はR2の認証情報が不要な代わりにGarageの別バケット(audio-permanent)を使う。
どちらもS3互換なので、internal/s3.InitS3Client(ctx, accessKey, secretKey, region, endpoint)という同じコンストラクタを使い回している(region/endpointを引数化する一般化を実施済み)。本番でdestをR2に切り替える際は、COCOT_STORAGE_*環境変数を変えるだけでよい設計。
開発環境でのバケット準備
開発中は、Garageにaudio-permanentという別バケットを作成し、既存のTUSアップロード用アクセスキーに読み書き権限を付与している。
garage bucket create audio-permanentgarage bucket allow --read --write audio-permanent --key <既存のアクセスキーID>テスト方針
buildRequest・waitForResult・validateAndReadOutput・sweepOrphanedJobs(internal/transcode/handoff.go)は純粋関数/ファイルI/Oのみに切り出し、ユニットテスト済み(実際のffmpeg/ffprobeバイナリや隔離コンテナ不要、t.TempDir()で代替)。finishUpload(DB更新の意思決定ロジック)も狭いインターフェース越しにfakeでテスト済み。一方transcodeAudio・downloadObject・uploadObject・deleteObjectは実際のS3 IOと隔離コンテナとのやり取りを伴うため、ユニットテストの対象外とし、e2e-smoke-testing(scripts/e2e-smoke.sh)で実際に音声ファイルをアップロードし、uploads.status=done・duration_seconds・converted_sizeが正しく記録されること、メタデータが除去されていることを検証している。アップロードファイルの検証(マジックバイトチェック・フォーマット強制)についてはupload-hardening、隔離コンテナ自体の設計はtranscode-sandboxを参照。
この検証で見つかったバグ
このマイルストーンの実装・検証で見つかった実バグ(001_river.sqlのスキーマ順序、TUS複合IDのDB照会不整合が2箇所)はknown-findingsに記録している。