2 min read

WebCodecs の Python 実装が動いた

最近は Python で AV1 や Opus が WebCodecs API に近いかたちで利用できるライブラリを開発していて、なんとか無事動いて OpenCV で取得したカメラ映像を AV1 にエンコードしてファイルに保存することができるようになりました。

経緯

もともと libwebrtc にはコーデックが含まれているのですが、他の WebRTC 実装である libdatachannel にはコーデックは含まれていないので Python から libdatacahnnel を利用する libdatachannel-py には無理矢理コーデックを組み込んでいました。

ただ、あまりしっくりきていないのと、できればコーデック部分は分けたい・・という思いがありました。

そんなとき Media over QUIC の Python ライブラリを作ろうかなとふと思ったとき、こちらも結局コーデックライブラリがブラウザ以外での利用はほとんどなく、ブラウザからの利用前提になってしまっていました。

そこでブラウザのコーデックを利用する仕組みである WebCodecs API にできるだけ寄せた Python ライブラリを自作することにしました。

これがれば気軽に FFmpegGStreamer に依存することなく、Python からコーデックを利用する事ができます。

実装方針

まずは最低限の方針としては音声は Opus 、映像は AV1 に対応するという方針を決めました。それ以外は基本的にはハードウェアアクセラレーターベースに仕様と考えました。多くの場合でハードウェアアクセラレーターを利用できる環境が圧倒的に有利な分野ということもあります。

もちろん Python での実装ではなく C++ 実装です。実装には nanobind を採用し、ビルドには scikit-build-core を採用しました。Windows への対応も必須と考えていたので CMakeList.txt をゴリゴリ書くことにしました。

結果

色々と省略していますがこんな感じで無事動いています。IVF というのは検証用の簡易コンテナフォーマットです。

既にテストはかなり十分かいたので、コードを整理してサンプルを用意して PyPI への登録と Apache-2.0 での公開に向けて進めていきます。

from webcodecs import (
    EncodedVideoChunkType,
    VideoEncoder,
    VideoEncoderConfig,
    VideoFrame,
    VideoFrameBufferInit,
    VideoPixelFormat,
)

# エンコーダーを初期化
encoded_frame_count = 0

def on_output(chunk):
    nonlocal encoded_frame_count
    # IVF ファイルに書き込み
    frame_data = chunk.get_data()
    ivf_writer.write(frame_data, encoded_frame_count)
    encoded_frame_count += 1

    # エンコードされたフレームのサイズを表示
    chunk_type = "Key" if chunk.type == EncodedVideoChunkType.KEY else "Delta"
    print(
        f"  フレーム {encoded_frame_count:4d}: {chunk_type:5s} {chunk.byte_length:6d} bytes, "
        f"timestamp={chunk.timestamp}"
    )

def on_error(error):
    print(f"エンコーダーエラー: {error}", file=sys.stderr)

encoder = VideoEncoder(on_output, on_error)

config: VideoEncoderConfig = {
    "codec": "av01.0.04M.08",
    "width": actual_width,
    "height": actual_height,
    "bitrate": args.bitrate,
    "framerate": float(args.fps),
    "bitrate_mode": "constant",
    "latency_mode": "realtime",
}

encoder.configure(config)
print("エンコーダーを初期化しました")
print(f"  コーデック: {config['codec']}")
print(f"  ビットレート: {args.bitrate} bps ({args.bitrate / 1000:.0f} kbps)")
print(f"  ビットレートモード: {config['bitrate_mode']}")
print(f"  レイテンシモード: {config['latency_mode']}")
print()

# フレームをキャプチャしてエンコード
frame_count = 0
timestamp = 0
frame_duration = 1_000_000 // args.fps  # マイクロ秒単位 (WebCodecs API 準拠)

print("フレームのキャプチャとエンコードを開始します...")
print("Ctrl+C で中断できます")
print()

start_time = time.time()
last_frame_time = start_time
try:
    while args.frames is None or frame_count < args.frames:
        ret, bgr_frame = camera.read()
        current_time = time.time()
        if not ret:
            print("エラー: フレームを読み込めませんでした", file=sys.stderr)
            break

        # フレーム間隔をログ出力(最初の10フレームのみ)
        if frame_count < 10:
            interval = (current_time - last_frame_time) * 1000  # ミリ秒
            print(f"フレーム {frame_count}: 間隔 {interval:.1f} ms")
        last_frame_time = current_time

        # BGR → I420 変換
        i420_data = bgr_to_i420(bgr_frame)

        # VideoFrame を作成
        init = VideoFrameBufferInit(
            format=VideoPixelFormat.I420,
            coded_width=actual_width,
            coded_height=actual_height,
            timestamp=timestamp,
        )
        video_frame = VideoFrame(i420_data, init)

        # エンコード(最初のフレームと 20 秒ごとにキーフレームを強制)
        # WebCodecs API ではアプリケーション側で明示的にキーフレームを制御する
        keyframe = frame_count == 0 or frame_count % (args.fps * 20) == 0
        encoder.encode(video_frame, {"keyFrame": keyframe})
        video_frame.close()

        frame_count += 1
        timestamp += frame_duration

雑感

WebCodecs の Python 実装は Claude Code と Codex を利用しています。ただし結構手を入れてさらに相当時間をかけてレビューをしているので Vibe Coding とは言いづらいかも知れません。

ただ構想から 2 週間程度で動くところまで持ってこれたのは LLM の恩恵といって間違いないでしょう。実際かなり詳細な比較ドキュメントも作ったりしています。

LLM の恩恵

Python の C++ 拡張を色々と作って言っていますが、かなりの速度で開発が進みます。ただレビューやら細かい修正やら仕様検討は相当疲れます。結局は人間がボトルネックでもっと強くならねば ... と実感する日々です。