📺

Google Cloud のライブトランスコーダー Live Stream API

2022/12/17に公開

こんにちは、メディア・エンターテイメント業界 担当の Customer Engineer の Dan です。

この記事は、Google Cloud Japan Advent Calendar 2022の 12/16 分です。
今回は YouTube Live のようなライブ動画配信サービスを提供する方向けのライブ トランスコーダーのソリューション、Live Stream API をご紹介します。Live Stream API の基本機能や言葉の概念、利用方法など分かりやすく解説してみました。

最近は動画プラットフォームだけではなく、音楽イベントや記者会見などのイベントの他、ライブ コマースといった BtoC ビジネスから工場や道路などの現場のモニタリングなどでもライブ動画ソリューションが使われるようになってきています。ライブ配信に携わっている方、これから構築しようと検討されている方は是非、今回の記事を通して理解を深め、試してみて下さい!

なお、ライブではなく VOD 配信向けの Google Cloud のソリューションには Transcoder API があります。Transcoder API の説明は下記記事・ドキュメントをご確認下さい。

Live Stream API の概要

Google Cloud Live Stream API はその名の通り、Google Cloud が提供する従量課金型のライブ配信用のストリーミング サービスで、インジェストした入力映像ソースをリアルタイムにトランス コーディングし、複数の配信形式にリアルタイムに変換します。

トランス コーディングとは、映像・音声ファイルの圧縮・符号化形式 (コーデック: codec) を再変更すること (再エンコード: encode) を行う技術のことで、具体的には最適な配信用メディア形式 (iPhone は HLS, Android は MPEG-DASH など) に変換することを意味しています。

なぜトランス コーディングする必要があるかというと、動画をインターネットを通じて配信する際に、受信デバイス側が対応している最適なメディア形式に変換するため、あるいは低速なインターネット環境でも視聴できるように低ビットレートの映像(500kbps の環境でもローディングアイコンがクルクル回らずに視聴できる容量)を含む様々なビットレートや解像度の映像を用意するためです。

それでは、Live Stream API はどのような機能があるのでしょうか。
Live Stream API。映像ソースを Live Stream API にインジェストすると配信形式に変換したものが Google Cloud Storage に出力されます

基本機能と特徴的な機能の紹介

Live Stream API の基本機能は、以下になります。

  • フルマネージド ライブ エンコーダー
  • インフラ管理不要、ジョブのサイズに応じて自動スケールアウト
  • 入力ソース:RTMP, SRT (RTMP_PUSH, SRT_PUSH)
  • 使いやすい REST API、使った分だけ従量課金、シンプルな料金体系

動画配信に詳しい方向けに、Live Stream API でサポートしている現状(2022/12 時点の)の映像条件を下記に記載します。一般的な配信ユースケースはカバーできています。

[入力]
画質の上限:HD(1920x1080, 25 Mbps, 60 fps)

[出力]
配信プロトコル:HLS, MPEG-DASH
映像:HD(1920x1080, 15 Mbps, 60 fps), h.264
音声:10Mbps, 48kHz, 6ch, AAC
字幕:CEA608, CEA708
コンテナ:fmp4(Fragmented mp4), ts (MPEG2-TS) ※ts は HLS のみ指定可能
セグメント:長さ 2 ~ 20 秒、最小数 3

昨今は動画配信ビジネスが急成長し、ライブ エンコード ソリューションも数多く出てきました。では Live Stream API の特徴的な機能は何でしょうか?
個人的には一番はもちろん、Google が開発している点だと考えています。Google は YouTube で配信ビジネスも行っていますし、Web 領域では AV1 など新しい配信用コーデックの開発にも積極的に取り組んでいます。そういったナレッジや機能開発が Live Stream API に反映されることを期待しています。

構成

次に、Live Stream API の構成要素についてご紹介します。Live Stream API ではいくつか独特のコンポーネントが存在しますので、下図の構成を交えながら解説したいと思います。主な登場人物は、Input、Channel、Event です。それぞれ見ていきます。

Input(入力エンドポイント)

参考:Input API リファレンス
インジェストされる映像ストリーム(入力映像)を受ける入力エンドポイントを Input と呼びます。入力映像に関する情報を定義する箇所です。必要最低限の設定はタイプ(type)のみで、現時点で設定可能な type は RTMP_PUSH, SRT_PUSH です。

しかし、知らない場所から映像ソースを不正にインジェストされてライブ配信がハイジャックされないように、セキュリティ ルール(securityRules)の送信元 IP アドレス指定(ipRanges)で許可する送信元を制限することをおすすめします。

その他、画質(tier)や入力映像・音声のフォーマット情報はオプションですので設定は不要です。
現在の入力映像の画質(tier)の上限は、HD(1920x1080, 25 Mbps, 60 fps)です。リファレンス上は、将来的に UHD(4096x2160)まで対応する予定のようですが、現状は対応しておりませんのでご注意下さい。

Channel(チャネル)

参考:Channel API リファレンス
入力映像に対する変換処理や変換結果(動画やマニフェスト ファイル)の出力先などの出力結果に対する情報を設定する箇所を Channel と呼びます。トランス コーディングの設定、出力先(Cloud Storage のバケット)の指定などができます。

この Channel の中で重要な項目として、elementaryStreams があります。elementaryStreams は、映像・音声・字幕テキストといった映像ファイル内のトラックのエンコード情報を設定する項目です。Channel では最終的な出力フォーマットとして複数の配信フォーマットをマニフェスト(manifests)として指定可能であり、異なる出力フォーマット間(HLS, MPEG-DASH)で elementaryStreams を共有し効率的にパッケージ化できるように映像・音声・字幕テキストのフォーマットを指定できるようになっています。

最終的な出力フォーマット(Manifest : マニフェスト)を作成するために必要な構成要素として、ElementaryStream (VideoStream, AudioStream), Mux がありますので、図を用いて説明します(説明のため設定項目は一部簡略化しています)。

例えば、一つの入力映像に対して、以下のような 2 つの配信フォーマット、2 つの解像度/プロトコル/コーデックの組み合わせで設定したいとします。

  1. HLS
    1-1. 映像:1280x720, 3Mbps, 30fps, h.264、音声:160kbps, aac
    1-2. 映像:640x360, 500kbps, 30fps, h.264、音声:64kbps, aac
  2. MPEG-DASH
    2-1. 映像:1280x720, 3Mbps, 30fps, h.264、音声:160kbps, aac
    2-2. 映像:640x360, 500kbps, 30fps, h.264、音声:64kbps, aac

この場合、一つずつのパターンを用意するとすると合計 4 パターンのトランスコーディング・Mux が必要になり、そのためのコンピュート リソースが必要と考えられるかと思います。

しかしこの内、映像・音声の設定に着目するとそれぞれ 2 パターンしか存在しません(1-1, 2-1 は同じ設定。1-2, 2-2 も同様)。そのため、以下のような形で映像と音声の組み合わせ(ElementaryStream)を定義することができます(図左端の VideoStream, AudioStream)。
VideoStream1(es_video_1) : 1280x720, 3Mbps, 30fps, h.264
VideoStream2(es_video_2) : 640x360, 500kbps, 30fps, h.264
AudioStream1(es_audio_1) : 160kbps, aac
AudioStream2(es_audio_2) : 64kbps, aac

この、映像と音声を組み合わせることを Mux(重畳)と呼ぶのですが、Mux の定義を書くと下記のようになります(図中央の Mux)。
mux_video_fmp4_1 : ElementaryStream [es_video_1], fmp4, 2s
mux_video_fmp4_2 : ElementaryStream [es_video_2], fmp4, 2s
mux_audio_fmp4_1 : ElementaryStream [es_audio_1], fmp4, 2s
mux_audio_fmp4_2 : ElementaryStream [es_audio_2], fmp4, 2s

この定義をベースに配信フォーマット(HLS, MPEG-DASH)の定義を行うと、下記のようになります(図右端の manifests)。

  • HLS [mux_video_fmp4_1, mux_video_fmp4_2, mux_audio_fmp4_1, mux_audio_fmp4_2]
  • MPEG-DASH [mux_video_fmp4_1, mux_video_fmp4_2, mux_audio_fmp4_1, mux_audio_fmp4_2]

このようにして出力設定を行っていくことになります。特に HLS/MPEG-DASH 両方の出力をしたい場合、トランスコーディング/Mux で共通化できる部分を意識して設定を行うことが大事です。少し分かりづらいですが、出力したいフォーマットから逆算して ElementaryStream[VideoStream, AudioStream], Mux, Manifests を定義していくと良いかと思います。

なお、HLS の場合はコンテナ フォーマットとして ts を選択可能です。ただし、HLS (ts) と MPEG-DASH (fmp4) 両方のマニフェストを作成したい場合、下記のように必要となる mux 処理およびファイル数・容量が多くなってしまいます。そのコストを低減するため、そのようなケースでは fmp4 を利用する方がメリットがあります。

[HLS 用 mux]
mux_video_ts_1 : ElementaryStream [es_video_1, es_audio_1], ts, 2s
mux_video_ts_2 : ElementaryStream [es_video_2, es_audio_2], ts, 2s

[MPEG-DASH 用 mux]
mux_video_fmp4_1 : ElementaryStream [es_video_1], fmp4, 2s
mux_video_fmp4_2 : ElementaryStream [es_video_2], fmp4, 2s
mux_audio_fmp4_1 : ElementaryStream [es_audio_1], fmp4, 2s
mux_audio_fmp4_2 : ElementaryStream [es_audio_2], fmp4, 2s

Event(チャンネル イベント)

参考:Event API リファレンス

作成した Channel に対して、操作を行いたいイベント内容を定義します。
現時点では、広告挿入イベント(AdBreakTask)のみサポートしています。広告挿入イベントは、ライブ配信の映像内に動画広告を流したいときに利用するイベントで、実行するイベントを即時実行(executeNow)またはスケジュール実行(executionTime)することが可能です。

使い方

ではここからは、Live Stream API の使い方について説明していきます。
現時点(2022/12)では Google Cloud コンソール上からは設定・利用できません(コンソール上では API の有効化機能のみ提供)。REST API での操作の他、C#, Golang, Java, Node.js, Python, Ruby のクライアント ライブラリを利用して操作する必要があります。

また、今回は説明用なので、CDN を使わずに環境をセットアップしたいと思います。

環境の準備

ここからは、Linux(Debian) 環境で gcloud コマンドのインストール済み、という前提で進めます。
下記コマンドを実行し、<PROJECT_ID>, <PROJECT_NUMBER>はご自身の環境のものに差し替えて下さい。

export PROJECT_ID=<PROJECT_ID>
export PROJECT_NUMBER=<PROJECT_NUMBER>
export LOCATION=asia-northeast1
export INPUT_ID=test-live-input
export CHANNEL_ID=test-live-channel

Google Cloud コンソールにアクセスせずに PROJECT_NUMBER を確認したい場合は、PROJECT_ID を環境変数にセットした後、下記コマンドを実行することで確認が可能です。

gcloud projects list --filter="${PROJECT_ID}" --format='value(PROJECT_NUMBER)'

次に Live Stream API を有効化します。

gcloud services enable livestream.googleapis.com

Live Stream API の変換結果を出力・保管するバケットを作成します。

gsutil mb gs://live-streaming-storage-$PROJECT_ID

バケットを公開します。今回は説明簡略化のために一般公開化していますが、必要あれば公開範囲を限定するようにしてください。

gsutil iam ch allUsers:objectViewer gs://live-streaming-storage-$PROJECT_ID

なお、組織ポリシーの「ドメイン別の ID の制限:constraints/iam.allowedPolicyMemberDomains」でリソースにアクセスできるユーザーのドメインが制限されている場合は、上記コマンドで公開バケットを作成する際にエラー(PreconditionException: 412 One or more users named in the policy do not belong to a permitted customer.)が発生します。ご注意下さい。

Input の作成

まずは入力映像を受ける Input を作成します。最初に Input を定義する理由は、Input を定義しないと Channel を作成できない(Channel 作成時に Channel に紐づく Input [inputAttachments] として Input を指定する必要がある)ためです。

今回は入力に RTMP を利用します。最初に下記設定を含めた input.json というファイルを作成します。
前述の通り、セキュリティ上、映像をインジェストできる場所を制限するために、IP アドレスを設定しています。PUBLIC_IP_ADDRESS 部分を、許可するグローバル IP アドレスを CIDR 形式で置き換えて下さい。
また、今回は tier を SD (Resolution < 1280x720. Bitrate <= 6 Mbps. FPS <= 60.) と指定しました。指定しない場合は HD (Resolution <= 1920x1080. Bitrate <= 25 Mbps. FPS <= 60.) になります。

input.json
{
  "type": "RTMP_PUSH",
  "securityRules": {
    "ipRanges": [
      PUBLIC_IP_ADDRESS
    ]
  },
  "tier": "SD"
}

次に下記コマンドで input を作成します。

curl -X POST \
-H "Authorization: Bearer "$(gcloud auth application-default print-access-token) \
-H "Content-Type: application/json; charset=utf-8" \
-d @input.json \
"https://livestream.googleapis.com/v1/projects/$PROJECT_NUMBER/locations/$LOCATION/inputs?inputId=$INPUT_ID"

成功すると下記のような結果が返却されます。
ここで注目いただきたいのが、最後の"done": falseです。

{
  "name": "projects/PROJECT_NUMBER/locations/asia-northeast1/operations/operation-1661405972853-5e70a38d6f27f-79100d00-310671b4",
  "metadata": {
    "@type": "type.googleapis.com/google.cloud.video.livestream.v1.OperationMetadata",
    "createTime": "2022-08-25T05:39:32.884030164Z",
    "target": "projects/PROJECT_NUMBER/locations/asia-northeast1/inputs/lab-live-input",
    "verb": "create",
    "requestedCancellation": false,
    "apiVersion": "v1"
  },
  "done": false
}

これは、Input の作成の受付は行われたが、まだ Input の作成が完了していないことを意味しています。
"done": trueとなるまで、オペレーションの完了を待つ必要があります。利用しているプロジェクトで、初めて Live Stream API を利用するリージョンの場合は、作成完了まで 10 分程度かかることがありますので、ご了承下さい。

上記レスポンス内の name 項目の operations 以下の operation- から始まる値が、オペレーション ID です(上記例でいうと、operation-1661405972853-5e70a38d6f27f-79100d00-310671b4)。
オペレーションが完了するまで何度か API を叩くこともありますので、この値を下記のように環境変数に設定しておきます。<OPERATION> の値は実際のオペレーション ID を登録して下さい。

export OPERATION_ID_1=<OPERATION>

その後、下記のコマンドを実行すると、オペレーションの状況を確認することができます。

curl -X GET \
-H "Authorization: Bearer "$(gcloud auth application-default print-access-token) \
"https://livestream.googleapis.com/v1/projects/$PROJECT_NUMBER/locations/$LOCATION/operations/$OPERATION_ID_1"

無事に Input が作成できた場合は、下記のように "done": true となり、reponse 項目で Input の情報が返却されます。
input.json で指定した通り、画質(tier)は SD に、セキュリティルールとして映像をインジェストする環境の IP アドレスがセットされています。
また、この response 内で重要なのは、uri です(下記例では、rtmp://34.94.97.220/live/4b7846a1-4a67-44ed-bfd0-d98281b6464a)。
これが映像を RTMP でインジェストするための送信先 URI になりますのでコピーしておきます。

{
  "name": "projects/PROJECT_NUMBER/locations/asia-northeast1/operations/operation-1661408816982-5e70ae25cea49-617844f0-8fdcb4a1",
  "metadata": {
    "@type": "type.googleapis.com/google.cloud.video.livestream.v1.OperationMetadata",
    "createTime": "2022-08-25T06:26:57.001530499Z",
    "endTime": "2022-08-25T06:26:57.043623522Z",
    "target": "projects/PROJECT_NUMBER/locations/asia-northeast1/inputs/test-live-input",
    "verb": "create",
    "requestedCancellation": false,
    "apiVersion": "v1"
  },
  "done": true,
  "response": {
    "@type": "type.googleapis.com/google.cloud.video.livestream.v1.Input",
    "name": "projects/PROJECT_ID/locations/asia-northeast1/inputs/test-live-input",
    "createTime": "2022-08-25T06:26:56.997623672Z",
    "updateTime": "2022-08-25T06:26:56.997623672Z",
    "type": "RTMP_PUSH",
    "uri": "rtmp://34.94.97.220/live/4b7846a1-4a67-44ed-bfd0-d98281b6464a",
    "securityRules": {
      "ipRanges": [
        PUBLIC_IP_ADDRESS
      ]
    },
    "tier": "SD"
  }
}

下記のように環境変数に設定しておきます。<uri> は実際の値を設定して下さい。

export URI=<uri>

続いて Channel の作成に移ります。

Channel の作成

最初に下記設定を含めた channel.json というファイルを作成します。前述の HLS を 2 ファイル、MPEG-DASH を 2 ファイル作成する構成で記載しています。
また、エラーが発生した際にログから確認できるように、ログ出力レベル(logSeverity)を INFO としています。ログ出力(Cloud Logging)には料金がかかりますので、問題のデバッグとトラブルシューティングが必要な場合にのみプラットフォーム ログを有効にし、不要になった場合はプラットフォーム ログを無効にするようにしてください。
下記ファイル内の $PROJECT_NUMBER, $PROJECT_ID, $LOCATION, $INPUT_ID はご自身の環境の値に変更して下さい。

channel.json
{
 "inputAttachments": [
   {
     "key": "my-input",
     "input": "projects/$PROJECT_NUMBER/locations/$LOCATION/inputs/$INPUT_ID"
   }
 ],
 "output": {
   "uri": "gs://live-streaming-storage-$PROJECT_ID"
 },
 "elementaryStreams": [
   {
     "key": "es_video_1",
     "videoStream": {
       "h264": {
         "profile": "high",
         "widthPixels": 1280,
         "heightPixels": 720,
         "bitrateBps": 3000000,
         "frameRate": 30
       }
     }
   },
   {
     "key": "es_video_2",
     "videoStream": {
       "h264": {
         "profile": "high",
         "widthPixels": 640,
         "heightPixels": 360,
         "bitrateBps": 500000,
         "frameRate": 30
       }
     }
   },
   {
     "key": "es_audio_1",
     "audioStream": {
       "codec": "aac",
       "channelCount": 2,
       "bitrateBps": 160000
     }
   },
   {
     "key": "es_audio_2",
     "audioStream": {
       "codec": "aac",
       "channelCount": 2,
       "bitrateBps": 64000
     }
   }
 ],
 "muxStreams": [
   {
     "key": "mux_video_fmp4_1",
     "container": "fmp4",
     "elementaryStreams": ["es_video_1"],
     "segmentSettings": { "segmentDuration": "2s" }
   },
   {
     "key": "mux_video_fmp4_2",
     "container": "fmp4",
     "elementaryStreams": ["es_video_2"],
     "segmentSettings": { "segmentDuration": "2s" }
   },
   {
     "key": "mux_audio_fmp4_1",
     "container": "fmp4",
     "elementaryStreams": ["es_audio_1"],
     "segmentSettings": { "segmentDuration": "2s" }
   },
   {
     "key": "mux_audio_fmp4_2",
     "container": "fmp4",
     "elementaryStreams": ["es_audio_2"],
     "segmentSettings": { "segmentDuration": "2s" }
   }
 ],
 "manifests": [
   {
     "fileName": "hls_index.m3u8",
     "type": "HLS",
     "muxStreams": [
       "mux_video_fmp4_1",
       "mux_audio_fmp4_1",
       "mux_video_fmp4_2",
       "mux_audio_fmp4_2"
     ],
     "maxSegmentCount": 5,
     "segmentKeepDuration": "60s"
   },
   {
     "fileName": "dash_index.mpd",
     "type": "DASH",
     "muxStreams": [
       "mux_video_fmp4_1",
       "mux_audio_fmp4_1",
       "mux_video_fmp4_2",
       "mux_audio_fmp4_2"
     ],
     "maxSegmentCount": 5,
     "segmentKeepDuration": "60s"
   }
 ],
 "logConfig": 
   { 
     "logSeverity": "INFO" 
   }
}

下記コマンドを実行して Channel を作成します。

curl -X POST \
-H "Authorization: Bearer "$(gcloud auth application-default print-access-token) \
-H "Content-Type: application/json; charset=utf-8" \
-d @channel.json \
"https://livestream.googleapis.com/v1/projects/$PROJECT_NUMBER/locations/$LOCATION/channels?channelId=$CHANNEL_ID"

レスポンスは下記のようなものが返却されます。Input のときと同じく、 "done": false が返却されています。 "done": true になるまで確認する必要があるので、name 項目の operations 以下の operation- から始まる値が、オペレーション ID です。下記の例では operation-1661405972853-5e70a38d6f27f-79100d00-310671b4 の部分です。

{
  "name": "projects/PROJECT_NUMBER/locations/asia-northeast1/operations/operation-1661405972853-5e70a38d6f27f-79100d00-310671b4",
  "metadata": {
    "@type": "type.googleapis.com/google.cloud.video.livestream.v1.OperationMetadata",
    "createTime": "2022-08-25T05:39:32.884030164Z",
    "target": "projects/PROJECT_NUMBER/locations/asia-northeast1/channels/test-live-channel",
    "verb": "create",
    "requestedCancellation": false,
    "apiVersion": "v1"
  },
  "done": false
}

この operation id を下記のコマンドで環境変数にセットします。

export OPERATION_ID_2=<OPERATION>

その後、Channel 作成状況が "done": true になるまで下記コマンドを実行します。

curl -s -X GET \
-H "Authorization: Bearer "$(gcloud auth application-default print-access-token) \
"https://livestream.googleapis.com/v1/projects/$PROJECT_NUMBER/locations/$LOCATION/operations/$OPERATION_ID_2"

Channel 作成状況が "done": true になると、下記のようなレスポンスが返却されます。
無事 Channel は作成済みですが、"streamingState": "STOPPED" とストリーミングのステータスが停止しています。この状態では入力映像を受け付けず、また当然ストリーミング出力もされません。
次のステップで Channel のストリーミング ステータスを有効化します。

{
  "name": "projects/PROJECT_NUMBER/locations/asia-northeast1/operations/operation-1671151922599-5efe760b654da-c7bdeea4-201ce678",
  "metadata": {
    "@type": "type.googleapis.com/google.cloud.video.livestream.v1.OperationMetadata",
    "createTime": "2022-12-16T00:52:02.629649021Z",
    "endTime": "2022-12-16T00:52:02.659716342Z",
    "target": "projects/PROJECT_NUMBER/locations/asia-northeast1/channels/test-live-channel",
    "verb": "create",
    "requestedCancellation": false,
    "apiVersion": "v1"
  },
  "done": true,
  "response": {
    "@type": "type.googleapis.com/google.cloud.video.livestream.v1.Channel",
    "name": "projects/PROJECT_ID/locations/asia-northeast1/channels/test-live-channel",
    "createTime": "2022-12-16T00:52:02.626799432Z",
    "updateTime": "2022-12-16T00:52:02.626799432Z",
    "activeInput": "my-input",
    "output": {
      "uri": "gs://live-streaming-storage-PROJECT_ID"
    },
    "elementaryStreams": [
      {
        "videoStream": {
          "h264": {
            "widthPixels": 1280,
            "heightPixels": 720,
            "frameRate": 30,
            "bitrateBps": 3000000,
            "gopDuration": "2s",
            "vbvSizeBits": 3000000,
            "vbvFullnessBits": 2700000,
            "entropyCoder": "cabac",
            "profile": "high"
          }
        },
        "key": "es_video_1"
      },
      {
        "videoStream": {
          "h264": {
            "widthPixels": 640,
            "heightPixels": 360,
            "frameRate": 30,
            "bitrateBps": 500000,
            "gopDuration": "2s",
            "vbvSizeBits": 500000,
            "vbvFullnessBits": 450000,
            "entropyCoder": "cabac",
            "profile": "high"
          }
        },
        "key": "es_video_2"
      },
      {
        "audioStream": {
          "codec": "aac",
          "bitrateBps": 160000,
          "channelCount": 2,
          "sampleRateHertz": 48000
        },
        "key": "es_audio_1"
      },
      {
        "audioStream": {
          "codec": "aac",
          "bitrateBps": 64000,
          "channelCount": 2,
          "sampleRateHertz": 48000
        },
        "key": "es_audio_2"
      }
    ],
    "muxStreams": [
      {
        "key": "mux_video_fmp4_1",
        "container": "fmp4",
        "elementaryStreams": [
          "es_video_1"
        ],
        "segmentSettings": {
          "segmentDuration": "2s"
        }
      },
      {
        "key": "mux_video_fmp4_2",
        "container": "fmp4",
        "elementaryStreams": [
          "es_video_2"
        ],
        "segmentSettings": {
          "segmentDuration": "2s"
        }
      },
      {
        "key": "mux_audio_fmp4_1",
        "container": "fmp4",
        "elementaryStreams": [
          "es_audio_1"
        ],
        "segmentSettings": {
          "segmentDuration": "2s"
        }
      },
      {
        "key": "mux_audio_fmp4_2",
        "container": "fmp4",
        "elementaryStreams": [
          "es_audio_2"
        ],
        "segmentSettings": {
          "segmentDuration": "2s"
        }
      }
    ],
    "manifests": [
      {
        "fileName": "main_1.m3u8",
        "type": "HLS",
        "muxStreams": [
          "mux_video_fmp4_1",
          "mux_audio_fmp4_1"
        ],
        "maxSegmentCount": 5,
        "segmentKeepDuration": "60s"
      },
      {
        "fileName": "main_2.m3u8",
        "type": "HLS",
        "muxStreams": [
          "mux_video_fmp4_2",
          "mux_audio_fmp4_2"
        ],
        "maxSegmentCount": 5,
        "segmentKeepDuration": "60s"
      },
      {
        "fileName": "main_1.mpd",
        "type": "DASH",
        "muxStreams": [
          "mux_video_fmp4_1",
          "mux_audio_fmp4_1"
        ],
        "maxSegmentCount": 5,
        "segmentKeepDuration": "60s"
      },
      {
        "fileName": "main_2.mpd",
        "type": "DASH",
        "muxStreams": [
          "mux_video_fmp4_2",
          "mux_audio_fmp4_2"
        ],
        "maxSegmentCount": 5,
        "segmentKeepDuration": "60s"
      }
    ],
    "streamingState": "STOPPED",
    "inputAttachments": [
      {
        "key": "my-input",
        "input": "projects/PROJECT_NUMBER/locations/asia-northeast1/inputs/test-live-input"
      }
    ],
    "logConfig": {
      "logSeverity": "INFO"
    }
  }
}

ストリーミングの有効化は下記のコマンドを実行します。

curl -X POST \
-H "Authorization: Bearer "$(gcloud auth application-default print-access-token) \
-H "Content-Type: application/json; charset=utf-8" \
-d "" \
"https://livestream.googleapis.com/v1/projects/$PROJECT_NUMBER/locations/$LOCATION/channels/$CHANNEL_ID:start"

レスポンスは下記のようなものが返されます。

{
  "name": "projects/PROJECT_NUMBER/locations/LOCATION/operations/operation-1661405972853-5e70a38d6f27f-79100d00-310671b4",
  "metadata": {
    "@type": "type.googleapis.com/google.cloud.video.livestream.v1.OperationMetadata",
    "createTime": "2022-08-25T05:39:32.884030164Z",
    "target": "projects/PROJECT_NUMBER/locations/asia-northeast1/channels/lab-live-channel",
    "verb": "start",
    "requestedCancellation": false,
    "apiVersion": "v1"
  },
  "done": false
}

下記コマンドを実行し、Channel のステータスを確認します。"streamingState": "STARTING" が表示されることがあります。ステータスが "streamingState": "AWAITING_INPUT" になるまでコマンドの実行を続けます(ご利用のプロジェクトで Channel を初めて有効化する場合は、初回のみ 10 分程度かかることがあります)。

curl -s -X GET -H "Authorization: Bearer "$(gcloud auth application-default print-access-token) "https://livestream.googleapis.com/v1/projects/$PROJECT_NUMBER/locations/$LOCATION/channels/$CHANNEL_ID" | grep "streamingState"

ステータスが "streamingState": "AWAITING_INPUT" になると Channel が作成できています。このステータスは Input として入力映像を待っている状態になります。次のステップで、映像を入力してみます。

映像のインジェスト

映像のインジェストには、配信用ソフトウェア(OBS や vMix 等)を利用することもできますが、ffmpeg でテスト映像をインジェストしてみます。
次のコマンドを実行して ffmpeg をインストールします(今回の環境は Debian)。

sudo apt install ffmpeg -y

インストールが無事に完了した後、下記のコマンドにてテストストリームを送信します。

ffmpeg -re -f lavfi -i "testsrc=size=1280x720 [out0]; sine=frequency=500 [out1]" -acodec aac -vcodec h264 -f flv $URI

その後、下記コマンドで Cloud Storage 上にファイルが生成されていることを確認します。

gcloud storage ls --recursive gs://live-streaming-storage-$PROJECT_ID/**

上記の結果、このようにバケット内にマニフェスト ファイルと動画セグメント ファイルが生成されていることがわかります。

gs://live-streaming-storage-PROJECT_ID/dash_index.m3u8
gs://live-streaming-storage-PROJECT_ID/hls_index.mpd
gs://live-streaming-storage-PROJECT_ID/mux_audio_fmp4_1/index-1.m3u8
gs://live-streaming-storage-PROJECT_ID/mux_audio_fmp4_1/segment-0000000406.m4s
gs://live-streaming-storage-PROJECT_ID/mux_audio_fmp4_1/segment-0000000407.m4s
gs://live-streaming-storage-PROJECT_ID/mux_audio_fmp4_1/segment-0000000408.m4s
gs://live-streaming-storage-PROJECT_ID/mux_audio_fmp4_1/segment-0000000409.m4s

コンソールで確認すると、下記のようにファイルが出力されています。

指定した通り、

  • HLS 用マニフェスト(hls_index.m3u8)
  • MPEG-DASH 用マニフェスト(dash_index.mpd)

が生成されていることが分かります。これらは Master Playlist (Multivariant Playlist) と呼ばれ、再生環境の通信速度や処理できるファイル形式/ビットレートごとに、どのマニフェスト ファイルを参照して動画を再生すればよいかをクライアント デバイス側で判断できるようになっています。

また、動画・音声用の HLS マニフェストとメディアファイル (fmp4) は Mux (muxStreams) で指定した名称のディレクトリ配下に保存されています。

参考までに、HLS マニフェスト ファイル(hls_index.m3u8)の中身を覗くと、下記のような情報が記載されています。それぞれ 動画と音声用のマニフェストのパスが、複数の解像度・ビットレートとともに定義されていることが分かります。

#EXTM3U
#EXT-X-VERSION:7
#EXT-X-INDEPENDENT-SEGMENTS

#EXT-X-STREAM-INF:BANDWIDTH=3160000,AVERAGE-BANDWIDTH=3160000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2",AUDIO="es_audio_1"
mux_video_fmp4_1/index-1.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=660000,AVERAGE-BANDWIDTH=660000,RESOLUTION=640x360,CODECS="avc1.64001e,mp4a.40.2",AUDIO="es_audio_1"
mux_video_fmp4_2/index-1.m3u8

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="es_audio_1",NAME="es_audio_1",AUTOSELECT=YES,DEFAULT=YES,URI="mux_audio_fmp4_1/index-1.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="es_audio_1",NAME="es_audio_2",AUTOSELECT=YES,URI="mux_audio_fmp4_2/index-1.m3u8"

上記 HLS マニフェスト ファイルで指定されている動画用マニフェスト(mux_video_fmp4_1/index-1.m3u8)の中身はこのような形で、実際のメディア ファイルのパスが指定されています。

#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:642
#EXT-X-DISCONTINUITY-SEQUENCE:1
#EXT-X-MAP:URI="segment-initialization_segment_0000000001.m4s"
#EXT-X-PROGRAM-DATE-TIME:2022-12-16T02:10:49.701Z
#EXTINF:2.000000
segment-0000000642.m4s
#EXT-X-PROGRAM-DATE-TIME:2022-12-16T02:10:51.701Z
#EXTINF:2.000000
segment-0000000643.m4s
#EXT-X-PROGRAM-DATE-TIME:2022-12-16T02:10:53.701Z
#EXTINF:2.000000
segment-0000000644.m4s
#EXT-X-PROGRAM-DATE-TIME:2022-12-16T02:10:55.701Z
#EXTINF:2.000000
segment-0000000645.m4s
#EXT-X-PROGRAM-DATE-TIME:2022-12-16T02:10:57.701Z
#EXTINF:2.000000
segment-0000000646.m4s

音声用マニフェスト(mux_audio_fmp4_1/index-1.m3u8)の中身はこのような形です。

#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:934
#EXT-X-DISCONTINUITY-SEQUENCE:1
#EXT-X-MAP:URI="segment-initialization_segment_0000000001.m4s"
#EXT-X-PROGRAM-DATE-TIME:2022-12-16T02:20:33.701Z
#EXTINF:2.000000
segment-0000000934.m4s
#EXT-X-PROGRAM-DATE-TIME:2022-12-16T02:20:35.701Z
#EXTINF:2.000000
segment-0000000935.m4s
#EXT-X-PROGRAM-DATE-TIME:2022-12-16T02:20:37.701Z
#EXTINF:2.000000
segment-0000000936.m4s
#EXT-X-PROGRAM-DATE-TIME:2022-12-16T02:20:39.701Z
#EXTINF:2.000000
segment-0000000937.m4s
#EXT-X-PROGRAM-DATE-TIME:2022-12-16T02:20:41.701Z
#EXTINF:2.000000
segment-0000000938.m4s

また、MPEG-DASH 用のマニフェスト ファイル(dash_index.mpd)の中身を覗くと、このような形が確認できます。動画・音声それぞれの定義はありますが、HLS と異なり、子マニフェスト ファイルの指定ではなく、メディアファイルへのパスが指定されています。また、今回の想定通り、HLS, MPEG-DASH で同じメディア ファイル(fmp4)が共通化できていることも分かります。

<?xml version="1.0" encoding="utf-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" profiles="urn:mpeg:dash:profile:isoff-live:2011" type="dynamic" minimumUpdatePeriod="PT1S" minBufferTime="PT4S" timeShiftBufferDepth="PT10.000S" availabilityStartTime="2022-12-18T11:08:01.024Z" publishTime="2022-12-18T11:09:27.487Z" availabilityEndTime="2022-12-18T11:17:27.487Z">
  <Period id="Dash-0.000" start="PT0.000S">
    <AdaptationSet segmentAlignment="true" maxWidth="1280" maxHeight="720">
      <Representation mimeType="video/mp4" id="mux_video_fmp4_1" codecs="avc1.64001f" width="1280" height="720" startWithSAP="1" bandwidth="3000000">
        <SegmentTemplate timescale="1000000" initialization="mux_video_fmp4_1/segment-initialization_segment_0000000000.m4s" media="mux_video_fmp4_1/segment-$Number%010d$.m4s" startNumber="38" presentationTimeOffset="80000">
          <SegmentTimeline>
            <S t="76000000" d="2000000" r="4"/>
          </SegmentTimeline>
        </SegmentTemplate>
      </Representation>
      <Representation mimeType="video/mp4" id="mux_video_fmp4_2" codecs="avc1.64001e" width="640" height="360" startWithSAP="1" bandwidth="500000">
        <SegmentTemplate timescale="1000000" initialization="mux_video_fmp4_2/segment-initialization_segment_0000000000.m4s" media="mux_video_fmp4_2/segment-$Number%010d$.m4s" startNumber="38" presentationTimeOffset="80000">
          <SegmentTimeline>
            <S t="76000000" d="2000000" r="4"/>
          </SegmentTimeline>
        </SegmentTemplate>
      </Representation>
    </AdaptationSet>
    <AdaptationSet segmentAlignment="true" mimeType="audio/mp4" id="1" label="mux_audio_fmp4_1">
      <Representation id="mux_audio_fmp4_1" codecs="mp4a.40.2" audioSamplingRate="48000" startWithSAP="1" bandwidth="160000">
        <SegmentTemplate timescale="1000000" initialization="mux_audio_fmp4_1/segment-initialization_segment_0000000000.m4s" media="mux_audio_fmp4_1/segment-$Number%010d$.m4s" startNumber="38" presentationTimeOffset="80000">
          <SegmentTimeline>
            <S t="76000000" d="2000000" r="4"/>
          </SegmentTimeline>
        </SegmentTemplate>
      </Representation>
    </AdaptationSet>
    <AdaptationSet segmentAlignment="true" mimeType="audio/mp4" id="2" label="mux_audio_fmp4_2">
      <Representation id="mux_audio_fmp4_2" codecs="mp4a.40.2" audioSamplingRate="48000" startWithSAP="1" bandwidth="64000">
        <SegmentTemplate timescale="1000000" initialization="mux_audio_fmp4_2/segment-initialization_segment_0000000000.m4s" media="mux_audio_fmp4_2/segment-$Number%010d$.m4s" startNumber="38" presentationTimeOffset="80000">
          <SegmentTimeline>
            <S t="76000000" d="2000000" r="4"/>
          </SegmentTimeline>
        </SegmentTemplate>
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>

これらのマニフェスト ファイル(hls_index.m3u8, dash_index.mpd)を HLS/MPEG-DASH それぞれに対応した動画プレイヤーで読み込むと、ライブ映像が再生されます。次のステップで確認してみます。

配信結果の確認

今回は HLS 対応動画プレイヤーとして、Mac の QuickTime Player で確認してみます。
※ブラウザ上で HLS/MPEG-DASH ストリームを確認するサイトとして Shaka Player のサイトなどいくつかありますが、今回は Cloud Storage 上のマニフェスト ファイルを直接参照している構成のため、それらのサイトから確認するためには Cloud Storage のバケットに対して CORS の構成 を行う必要がありますのでご注意下さい。

今回生成された HLS の一般公開 URL は下記のような構成です。PROJECT_ID はご自身の環境のものと置き換えて下さい。

https://storage.googleapis.com/live-streaming-storage-PROJECT_ID/hls_index.m3u8

QuickTime Player で、「ファイル」>「場所を開く」にて上記一般公開 URL を貼り付けて再生すると、下図のようにテスト ストリームが再生されることを確認できます。遅延量を図りたいときは、元映像にタイムコードを焼き込んで End-to-End の遅延量を確認するようにして下さい。

また、本番利用時には Cloud Storage 直接配信ではなく、Cloud CDN/Media CDN 等の CDN を利用することを推奨します。CDN を利用することで、多くの視聴者にコストを抑えつつパフォーマンス良く、より良い視聴体験を提供することができます。

Clean up

最後に、今回利用した環境を削除しておきます。

まずは ffmpeg を実行しているコンソールで、<CTRL+C> を送信し、ffmpeg を停止します。

次に、下記コマンドを実行し、Channel を停止します。

curl -X POST \
-H "Authorization: Bearer "$(gcloud auth application-default print-access-token) \
-H "Content-Type: application/json; charset=utf-8" \
-d "" \
"https://livestream.googleapis.com/v1/projects/$PROJECT_NUMBER/locations/$LOCATION/channels/$CHANNEL_ID:stop"

下記コマンドを実行し、Channel を削除します。

curl -X DELETE -H "Authorization: Bearer "$(gcloud auth application-default print-access-token) "https://livestream.googleapis.com/v1/projects/$PROJECT_NUMBER/locations/$LOCATION/channels/$CHANNEL_ID"

下記コマンドを実行し、Input を削除します。

curl -X DELETE \
-H "Authorization: Bearer "$(gcloud auth application-default print-access-token) \
"https://livestream.googleapis.com/v1/projects/$PROJECT_NUMBER/locations/$LOCATION/inputs/$INPUT_ID"

最後に、下記コマンドを実行し、Cloud Storage バケットを削除します。

gsutil rm -r gs://live-streaming-storage-$PROJECT_ID

まとめ

今回は Live Stream API の特徴や使い方について解説しました。
使いやすい API ベース、従量課金、高パフォーマンスの他、動画配信ビジネスに活用しやすい機能も充実しており、まさに OTT 事業者向けのクラウド ライブ トランスコーダーだということがご理解いただけたかと思います。
Google Cloud はメディア領域に注力していますので、新しい機能も鋭意開発中です。Live Stream API に機能追加された際には、その詳細について説明する記事をまた書きたいと思います。ご期待下さい!

それでは、引き続き Google Cloud Japan Advent Calendar 2022 をお楽しみください!

Google Cloud Japan

Discussion