📹

簡単なライブ映像配信システムをCursor Agentで作って勉強する

に公開

こんにちは、Flutterで個人開発などをしているyotaroです。
今回は、簡単なライブ映像配信システムを、Cursor Agentに実装してもらいつつ、勉強した内容を記事でシェアしたいと思います。

今までFlutterを使って、簡単なメモアプリと音声会話アプリを作りました。

◼︎メモアプリ
https://zenn.dev/yotaroyotaro/articles/84834d719a202e

◼︎音声会話アプリ
https://zenn.dev/yotaroyotaro/articles/4562bb480b415a

テキストデータ・画像データ・音声データは扱ってきました。
直近のアプリで扱った音声データは、時系列データということもあり、割と新鮮で楽しさもありました。

今度は、映像データを触ってみたいと思っていました。

同時に、Cursor Agentを使ってみたかったので、ローカルで動かすライブ映像配信システムを作りながら勉強してみました。

https://youtu.be/DmGbz9wfUQ4

  • 映像の左側が、OBSという配信アプリでライブ配信画面を変化させている様子
  • 映像の右上が、Flutter(デモではWeb)でライブ映像再生している様子
  • 映像の右下が、Nginx上に動画ファイルが変化していく様子

デモ動画は、画面収録したかったのですが、画面収録すると、PCが重くなり、映像が乱れてしまいました。
そのため、PCの画面をスマホのカメラで撮影しました。
全てローカルで作りましたが、配信アプリの映像からクライアントに届くまで、15秒くらいはタイムラグがありましたね。

所要時間

ChatGPTやCursor Agentを利用し、1週間のうち、大体9時間作りました。

  • 構成検討
    • 平日1時間 x 5日 = 5時間
      • 主にChatGPT(GPT-4o)と対話して検討しました。
  • 実装
    • 土曜日 2時間
      • Cursor Agentで作ってもらいました。
      • .cursorruleで指示を書き、都度修正指示を出したら、とりあえず動きものはできました。ほんとすごい。
  • 実装後の勉強
    • 日曜日 2時間
      • Cursor Agentが書いたコードを眺めて、ごちゃごちゃ編集したり、わからないところをCursorでチャットして確認する流れで、勉強しました。

頭で構成を考えるだけではなく、実際に動くものを見て、ごちゃごちゃ触って理解を深められるのは、楽しかったですね。

構成

ざっくりとこんな構成です。
構成は、ChatGPT(GPT-4o)と対話して決めました。
この構成検討の対話によって、基本知識への理解も深まり、勉強になりました。

構成要素の詳細

今回、ライブ配信システムの構成を考えるにあたり、勉強した内容をメモします。

配信アプリ(OBS)

OBS(Open Broadcaster Software)は、映像配信側で利用します。
無料で利用でき、映像のライブ配信や録画が行える配信ソフトウェアです。

https://obsproject.com/ja

あまりライブ配信のソフトウェアには詳しくありませんが、
配信系ソフトウェアの中では、世界的に人気のようで、YouTube Liveなどでも利用できるようです。

今回は、映像配信側のソフトウェアは、既存製品のOBSをそのまま利用することにしました。
OBSの設定で、配信先のサーバを指定できるので、以下のように、nginxで建てたサーバを指定します。
今回のストリームキーは「test」としました。

その他、OBSの使い方については、以下のYouTubeなどを参考にさせていただき、基本的な操作だけ確認させていただきました。

https://www.youtube.com/watch?v=h7jNhJTv94k

少しライブ配信者の気持ちになれた気がします。

RTMP

RTMP(Real-Time Messaging Protocol)は、Adobe が開発したライブ配信や映像ストリーミングで使われてきた映像・音声データをリアルタイムに送るための通信プロトコルです。

OBSからは、RTMPで、映像を配信サーバ側に送ります。

通信はTCPポート1935を使います。
RTMPでは、TCPを使い、常に接続し続けながら、映像や音声などの「データチャンク(小さな塊)」を順番に送信していく仕組みのようです。
つまり、流しっぱなしのイメージです。

後述するHLSでは、RTMPとは異なり、流しっぱなしではなく、クライアント(映像視聴側)が自ら受け取りにいった時に映像データを受け取るようです。

配信サーバ(nginx-rtmp)

配信サーバとしては、Nginxを利用しました。
今回、勉強したかった目玉部分なので、少し長く書きます。

nginx-rtmpは、nginxに、RTMP(リアルタイム映像配信)機能を追加するモジュールです。
今回、nginx-rtmpを利用し、簡易的な配信サーバを作りました。

nginx-rtmpで、 RTMPで映像を受信し、HLS形式に変換後、クライアントアプリに配信します。

https://github.com/yotaro-code/sample_live_video_system/blob/main/media_server/nginx.conf

nginx.confの内容を一部抜粋してみます。
まずは、RTMPモジュールの読み込みます。

load_module /usr/lib/nginx/modules/ngx_rtmp_module.so;  # RTMPモジュールの読み込み

次に、RTMPサーバの設定を書きます。

rtmp {
    server {
        listen 1935;  # RTMPのデフォルトポート
        chunk_size 4096;  # チャンクサイズの設定

        # ステータス確認用アプリケーション
        application stat {
            live on;
            record off;
        }

        # ライブストリーミング用アプリケーション
        application live {
            live on;  # ライブストリーミングを有効化
            record off;  # 録画機能を無効化

            hls on;  # HLS配信を有効化
            hls_path /var/www/hls;  # HLSファイルの保存先
            hls_fragment 3;  # 各フラグメントの長さ(秒)
            hls_playlist_length 60;  # プレイリストの長さ(秒)
        }
    }
}

RTMPプロトコルの標準ポートは、1935。
チャンクというデータを小分けにした形で、映像を扱います。そのチャンクのサイズが、4096Byte。

ちなみに、OBS側からは、少し小さいチャンクサイズ(128byte等)で、映像が送られてくるようです。
例えば、OBS側からNginxへ、128byteのチャンクで送られた場合、Nginxでは、128byteのチャンクを蓄積し、4096byte(128byte x 32個)まで貯めて、1つのチャンクとして扱うようです。
多くのシステム上では、メモリを管理する最小単位(ページサイズ)が4KBなので、4096byte(4KB)で扱うと、メモリ管理の効率が良いんだとか。

liveをonにして、ライブ映像を受信できるよう設定します。
また、後述するHLSという形式に変換をする設定も、ここで行います。
3秒間の長さに映像ファイルを分割し、プレイリストには60秒間(3秒/ファイル x 20ファイル)のデータを保持する設定です。

今回、録画機能は特に使わず、offにしています。

続いて、クライアント側に配信する側のWebサーバーとしてのnginxの設定です。

http {
    # 基本設定
    include       /etc/nginx/mime.types;    # MIMEタイプの定義ファイル
    default_type  application/octet-stream; # デフォルトのMIMEタイプ

映像データを扱うために、MINE対応の設定をします。

    map $http_origin $cors_origin {
        default "*";
        "~^https?://localhost(:[0-9]+)?$" "$http_origin";
        "~^https?://127.0.0.1(:[0-9]+)?$" "$http_origin";
    }

CORS(Cross-Origin Resource Sharing)の設定です。
アクセス元(Origin)を確認して、それに応じて許可するドメインを設定します。

デフォルトはすべてのドメインからのアクセスを許可しています(*)
また、今回は、ローカルで試すので、http://localhosthttp://localhost:3000 などが来たときの許可。127.0.0.1(localhostのIP版)も同様に許可の設定を入れました。

    server {
        listen 8080;

8080ポートで、httpリクエストを受け付けます。
「location /hls」の説明します。
ちなみに、locationは、どのURLにアクセスされたかによって、どう処理するかを指定する場所を指すようです。

        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /var/www;
            add_header Cache-Control no-cache;
            
            # HLS特有のCORS設定
            add_header 'Access-Control-Allow-Origin' $cors_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' '*' always;
            add_header 'Access-Control-Expose-Headers' '*' always;
            
            # プリフライトリクエストの処理
            if ($request_method = 'OPTIONS') {
                add_header 'Access-Control-Allow-Origin' $cors_origin;
                add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
                add_header 'Access-Control-Allow-Headers' '*';
                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Content-Type' 'text/plain charset=UTF-8';
                add_header 'Content-Length' 0;
                return 204;
            }
        }

typesで、HLS配信用の .m3u8(プレイリスト)や .ts(動画の分割ファイル)を指定します。
(ファイルの説明は後述のHLSに書きます。)
これらのファイルは、 /var/www/hls/ に保存されます。
ブラウザにキャッシュさせない設定を入れてます。

CORS設定を、locationブロックでも書く必要があるようですね。
ちなみに、各ブロックの設定は、以下のように効果があるらしいです。

  • httpブロックに書いた設定 → 全体に効く「デフォルト」
  • serverブロックに書いた設定 → そのサーバー(ポートなど)に対して有効
  • locationブロックに書いた設定 → そのURLのみに有効(優先度高い)

最後に、OPTIONSで、nginx側で事前に204を返す設定をすることで、プリフライトリクエストという事前確認用のリクエスト対応ができるようです。

今回は、ローカルでnginxを利用して作りましたが、実際には、以下のようなマネージドサービスを使ったりするのだろうなと勝手に思ってます。

HLS

HLS(HTTP Live Streaming)は、Appleが開発した動画配信の方式で、
動画を小さなファイル(セグメント)に分けて、HTTPで順番に配信します。
HTTPで配信できるので、普通のWebサーバ(nginxなど)で、ストリーミング配信ができるようですね。

「RTMPのチャンク」と「HLSのセグメント」、どちらも分割された映像データを表現しますね。
使われ方が違うようです。詳しくは理解できてないですが、ざっくりと以下の認識です。

  • 「RTMPのチャンク」は、TCPベースで連続的にデータを受信し続ける。
  • 「HLSのセグメント」は、HTTPベースでクライアント側のタイミングで取得する。

HLSでは、大きく「.m3u8」と「.ts」の2種類のファイルが登場します。
hlsフィルダ内のファイル構成は、以下のようになります。

📂 /tmp/hls/           ← HLS出力先(nginx.confで指定)
├── test.m3u8         ← HLSのプレイリストファイル(動画の目次)
├── test0.ts          ← 映像の最初の3秒間(セグメント)
├── test1.ts
├── test2.ts
├── ...

「.m3u8」ファイルは、プレイリストファイルです。
「.ts」ファイルのリストになっており、目次ファイルのようなものである認識です。

中身は以下のようなイメージです。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:3
#EXT-X-MEDIA-SEQUENCE:0

#EXTINF:3.000,
test0.ts
#EXTINF:3.000,
test1.ts
#EXTINF:3.000,
test2.ts
...

「.ts」ファイルは、元の映像をを数秒ごとに分割した短い映像ファイルです。
nginx.confで設定したように、3秒ごとにデータが分割されていきます。

ファイル名 内容 時間
test0.ts 最初の3秒 0〜3秒
test1.ts 次の3秒 3〜6秒
test2.ts 次の3秒 6〜9秒
... ... ...

映像視聴アプリ(Flutter)

クライアント側の映像視聴アプリは、Flutterで作成しました。
ネイティブとWebでは、映像再生の作り方が異なるようです。

今回は、ネイティブアプリ向けに「video_player」パッケージを利用した
映像再生部分を見ていきます。
(iPhoneのシミュレータで再生を試しました。)
(Webの場合は、hls.jsなどを使います。)

まずは、以下のパッケージを追加します。

video_player: ^2.7.0

映像再生部分だけ見ていきます。

https://github.com/yotaro-code/sample_live_video_system/blob/main/client_app/play_app/lib/ios_video_player_page.dart

「VideoPlayerController」が、video_playerパッケージが提供するコントローラで、再生・停止等を管理します。
「_controller」に、「networkUrl」を利用して、ローカルの「hls/test.m3u8」のURLを指定し、HLSで映像再生をしています。

video_playerパッケージを使えば、m3u8ファイルを指定するだけで、簡単にストリーミング再生ができました。すごい楽です。

おわりに

今回は、ライブ映像配信システムをサクッと作って勉強してみました。

あまり聞いたことのない「RTMP」や「HLS」のプロトコルを勉強し、配信側と視聴側での実現方式の差分も少し見えてきた気がします。
実際に動いたものを見て、nginx.confを眺めてたりするだけで、設定値の意味合いなども頭に入りやすかった印象です。

AI Agentが出てきて、ちょっとしたアイディアを試すレベルのシステムやアプリであれば、実装の時間はAI Agentに任せて短縮できそうです。
とりあえず作っちゃって、動かしながら勉強してみる、という勉強方法も良いかもしれませんね。

Discussion