こんにちは、テリーです。もうすぐオリンピックが始まりますね。オリンピックの試合をリモート観戦するのが待ち遠しいです。できるだけ大画面で見たいし、手に汗を握りながら、家族で声援も送りたいところです。オリンピックの試合はYouTubeライブで見られるのでしょうか?

さて、これまでライブ映像配信といえば2つのユースケースがありました。1つは、オリンピックの試合中継のような、テレビ的な大衆放送。もう1つは、数人規模のビデオ会議です。カメラの映像をそのまま送り出し、視聴者に最高品質の映像を届ける、という仕組みです。最近は、ケース1,2の複合型とも言える画面共有がよく使われています。

この数年、「第三のユースケース」として、コラボプレイの事例が増えてきました。2人の配信者がビデオ会議でつながりつつ、ゲーム・楽器演奏・カラオケを同時にプレイ・演奏・歌唱し、その様子を多数の人が視聴するという使い方です。例えばクラウドゲームでは、同一のシーンに複数のキャラクターが登場し、他のプレイヤーに対して銃を撃ったり、体当たりしたり、敵の攻撃をよけます。リモートカラオケで2人でハモったりしたいです。楽器演奏もリモートでしたい、というニーズは昔からあります。

リモートで楽器演奏ができない理由の一つに、「配信の遅延」があります。人間同士の声の会話では、数百ミリ秒の遅延は許容範囲内でしたが、楽器演奏やカラオケのケースでは百ミリ秒ズレただけでとても違和感を感じるアウトプットになります。ゲームの場合では、自分の画面上では敵の攻撃を回避したはずなのに、なぜか当たり判定が下され、突如ゲームオーバーになることがあります。

このように、配信の遅延に百ミリ秒以下を要求するケースが増えてきています。これまでの映像配信技術では実現できない、遅延数十ミリ秒の新しい配信技術開発に、様々な企業が取り組んでいます。今回は、映像伝送処理全般を表す言葉「トランスポート」に注目し、その中でも特に低遅延を売りにして広く使用されつつある「SRT」を取り上げます。どのくらい低遅延なのか、従来技術と何が違うのかの比較・評価方法について紹介したいと思います。

配信と伝送って何が違うの?

「映像配信」という言葉を「映像伝送」という言葉と区別して使用する場合、「配信」は「カメラの撮影から視聴者のディスプレイ表示」までの一連の流れすべてを指します。一方「伝送」は「エンコード(圧縮)済みのデータを視聴者に届けデコード(復号)する直前」までの一連の流れを指します。映像配信にかかる数百ミリ秒の戦いの中で、映像伝送の占める割合はかなり高く、技術開発の成果を出しやすい箇所と言えそうです。

SRTの特徴

GitHubのドキュメントにSRTがなぜ開発されたのかについて書かれています。それによると、従来の伝送技術には下記の短所があり、一長一短とのことです。

  • 前方誤り訂正(FEC)がない、あってもオーバーヘッドが大きすぎる
  • TCPはネットワーク混雑時やパケットロスのリカバリーが遅すぎる
  • UDPはパケットロス、ゆらぎ、到着順が変わることがあるため、待ち時間を多くしなければならない
  • UDTはライブ映像伝送用に作られていない

インターネットはベストエフォートであるため、パケットが到着する時間のゆらぎがとても大きく、都度変わります。ゆらぎを無視して受信したものを即再生すると、プチップチッと頻繁に一瞬停止が発生します。YouTubeでプチっとなることはめったにありませんね。それは遅延を犠牲にして、受信バッファを長く確保しているからです。ゆらぎを吸収するためには、できるだけ大きい秒数の期間、プレイヤー側でパケットを寝かしてから再生します。秒数を事前に定義しているため、回線が十分安定して速かったとしても、バッファ時間を後から短くすることができません。高速で安定した回線を前提にバッファ時間を短くしすぎた場合、視聴に違和感を覚える視聴者がたくさん発生します。

バッファが短いとビデオ再生が頻繁に止まる(出典:GitHubのドキュメント)

SRTは、受信側の状態を送信側に伝えることで、バッファ時間を最適化する仕組みを導入しています。そのため、高速で安定した回線では低遅延に、やや不安定な状態を検出した場合にはバッファを自動的に調整して安定した再生ができるようにしています。

SRTを導入するとバッファ時間が最適化され安定した再生ができる(出典:GitHubのドキュメント)

SRTがどのくらいスゴイのか?を調べる

「SRTはスゴイ」とSRTの開発会社が言うのは当たり前なので、ユーザー目線でどのくらいスゴイのか評価してみたいと思います。そのためには、ネットワークのゆらぎを作る必要がありますが、インターネットでゆらぎ・パケットロスを意図的に発生させるのは難しいです。ローカル環境でゆらぎを作る方法をまとめてみました。

Macの場合、Apple純正の「Network Link Conditioner」という設定ツールがあります。これはGUIなのでとても気軽に便利に設定を変えられます。問題点は、特定のポート番号やIPアドレスだけにパケット制限することができないところです。例えばVPN+SSHで接続しながら不安定なネットワークの配信試験を行うことができません。安定ネットワークと、不安定ネットワークを同時受信し、長時間再生比較するような使い方ができません。NICを指定する機能がないため、ローカルDockerコンテナに対して制限をかけることができません。これらのどれにも当てはまらないテストの場合には最も使い勝手の良いツールと言えるでしょう。

linuxの場合、tcコマンドがあります。これはDocker内でも使用できるため、Docker環境でテスト対象のホスト構成をすれば、インターネットを使わずに、再現性の高い不安定ネットワークのテストをすることができます。IP制限とポート制限ができるため、特定のDockerコンテナのみ、または特定のサービスポートのみ制限をかけることができます。MacのNetwork Link Conditionerで述べた欠点はすべてありません。唯一の欠点と言えるのは、コマンドラインなのに機能が満載すぎて、すぐには使いこなせないところです。

環境

本記事の実行環境は下記を使用しています。

  • Mac macOS Big Sur 11.4
  • Visual Studio Code 1.57.1
  • Docker Desktop 3.5.1

Dockerコンテナでtcコマンドを使用するには?

ローカルDockerコンテナを複数立て、tcコマンドによる不安定ネットワークを構築する手順を紹介します。

Dockerにマウントしたいフォルダを作成し、VSCodeで開きます。例として~/srt1とします。

mkdir -p ~/srt1
cd ~/srt1
code .

次にVSCodeウインドウ左下の緑枠「><」をクリックし、出てきたメニューの中から「Add Development Container Configuration Files…」をクリックします。

出てきた候補の中から「Ubuntu」を選び、さらにその次のメニューで「focal(20.04)」「bionic(18.04)」のどちらかを選びます。

すると「.devcontainer」というフォルダと、「devcontainer.json」「Dockerfile」が生成されます。devcontainer.jsonを下記のように書き加えます。変更点は3箇所です。

  • nameを”Ubuntu”から”srt1″に変更
  • “runArgs”:[“–privileged”]を追加
  • “remoteUser”: “vscode”を削除
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.183.0/containers/ubuntu
{
    "name": "srt1",
    "build": {
        "dockerfile": "Dockerfile",
        // Update 'VARIANT' to pick an Ubuntu version: focal, bionic
        "args": { "VARIANT": "focal" }
    },
    "runArgs": [
        "--privileged",
    ],

    // Set *default* container specific settings.json values on container create.
    "settings": {},


    // Add the IDs of extensions you want installed when the container is created.
    "extensions": [],

    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    // "forwardPorts": [],

    // Use 'postCreateCommand' to run commands after the container is created.
    // "postCreateCommand": "uname -a",

    // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
    // "remoteUser": "vscode"
}

保存したら、もう一度左下の緑色ボタンをクリックし、出てきたメニューから「Reopen in Container」をクリックしてください。

同様に~/srt2にもう一つコンテナを立ち上げます。上記手順で「srt1」としているところをすべて「srt2」に置き換えてください。

mkdir -p ~/srt2
cd ~/srt2
code .
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.183.0/containers/ubuntu
{
    "name": "srt2",
    "build": {
        "dockerfile": "Dockerfile",
        // Update 'VARIANT' to pick an Ubuntu version: focal, bionic
        "args": { "VARIANT": "focal" }
    },
    "runArgs": [
        "--privileged",
    ],

    // Set *default* container specific settings.json values on container create.
    "settings": {},


    // Add the IDs of extensions you want installed when the container is created.
    "extensions": [],

    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    // "forwardPorts": [],

    // Use 'postCreateCommand' to run commands after the container is created.
    // "postCreateCommand": "uname -a",

    // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
    // "remoteUser": "vscode"
}

Dockerコンテナが2つ立ち上がったら、それぞれのVSCodeウインドウ内でターミナルを開き、下記のコマンドを実行してください。IPアドレスが表示されます。

apt update
apt-get install -y iputils-ping net-tools iperf3 iftop
ifconfig

ここでは例として、srt1のIPアドレスが172.17.0.2、srt2のIPアドレスが172.17.0.3とします。srt1からsrt2にpingをして応答時間を確認します。

ping -c 10 172.17.0.3

0.1〜0.2ms(ミリ秒)の応答速度です。同じコンピュータ内なので、このくらいの数値になります。外部のサーバにpingを打つと、東京のサーバの場合10〜30msです。海を越えるサーバの場合は100〜200msになることが多いです。

疎通が確認できたところで、今度は大量のパケットを送って通信速度(ビットレート)を図ってみましょう。iperf3というコマンドを使います。このコマンドは特定のポート番号(未指定の場合は5201)に10秒間パケットを送り続け、その送信速度、受信速度を1秒ごとに集計します。

srt2のターミナルで下記のコマンドを打ちます。

iperf3 -s

srt1のターミナルで下記のコマンドを打ちます。

iperf3 -c 172.17.0.3

すると、srt1からsrt2にパケットが送られ、1秒ごとの転送量とビットレートが表示されます。私の環境では、15Gbps出ているようです。

制限なしの状態でDockerコンテナ間の応答速度とビットレートを把握することができました。

  • ping応答速度 0.138ミリ秒
  • ビットレート 15.2Gbps

通信速度を遅くする

では、この応答速度をtcコマンドを使って変えてみましょう。応答速度を200msにしてみます。srt1で下記のコマンドを打ちます。

tc qdisc add dev eth0 root netem delay 200ms
ping -c 10 172.17.0.3
iperf3 -c 172.17.0.3

応答速度が200ms台で安定していること、ビットレートもおおよそ安定して遅くなっていることが確認できます。安定して遅いネットワークのテストをしたい場合はこの設定です。

通信速度にゆらぎを加える

実運用のネットワークでは固定のビットレートはほぼありえないので、もう少し不安定にしたいというケースもあります。その場合は下記のコマンドを試してみてください。ping応答速度が200±50ミリ秒の範囲でランダムに変わります。パケットロスが1%なので、100パケットに1回程度ランダムにリトライが発生します。

tc qdisc change dev eth0 root netem delay 200ms 50ms loss 1%
ping -c 10 172.17.0.3
iperf3 -c 172.17.0.3

特定のポートだけ通信速度を遅くする

上のコマンドの場合、srt1から送信するすべてのパケットが遅くなります。計測対象のポートだけ遅くする、パケットロスを与えるようにするには下記のコマンドを打ちます。

# 設定をすべて削除
tc qdisc del dev eth0 root
# パケットを3種類(1:1, 1:2, 1:3)に分類する仕組みを導入
tc qdisc add dev eth0 root handle 1: prio
# IP 172.17.0.3 TCPポート5201 宛てのパケットを1:1に分類
tc filter add dev eth0 protocol ip parent 1: prio 1 u32 match ip dst 172.17.0.3/32 match ip dport 5201 0xffff flowid 1:1
# 1:1に分類したパケットに遅延・帯域制限等を設定
tc qdisc add dev eth0 parent 1:1 handle 10: netem delay 200ms 50ms loss 1%
ping -c 10 172.17.0.3
iperf3 -c 172.17.0.3

pingの応答速度が制限なしの0.1ミリ秒付近になったことと、iperf3のビットレートは一つ前のテスト結果に近い数値が出ていることが確認できます。

通信速度設定を確認する

現在の設定を確認するには下記のコマンドを打ちます。

tc qdisc show dev eth0 && tc filter show dev eth0

通信速度制限をすべて削除する

設定が不要になったので、すべて削除したい場合は下記のコマンドを打ちます。

tc qdisc del dev eth0 root

まとめ

このようにtcコマンドを使うと、Docker間で様々な通信速度制限を加えて確認することができます。tcコマンドは他にもたくさんの機能があり、もっと複雑な制限も設定できますので、ぜひトライしてください。次回の後編では、制限を加えた状態でSRTを使って、どのような挙動を示すか、生のパケット送信と比較して、どのような差が出るかを示します。