【メモ】GCPで完結する動画配信サービスの設計と実装
現時点では実装できていない機能もある為詳しく記述できない箇所や実際の仕様と異なる、変更される箇所も出てきます。その点を考慮して閲覧していただけると嬉しいです。
サービスの仕様
以下の特徴を持つようなサービスを設計します。
- ユーザーがmp4形式の動画をアップロード
- アップロードされた動画をHLSにトランスコードし閲覧時に配信
- 配信されている動画はユーザー単位のアクセス制限をかける事が可能
今回は動画をメインに取り扱っていますが、動画以外にも画像や音声、テキストデータなどを扱う事が可能だと思います。
配信基盤の設計
配信にはCloudStorageを利用します。加えてCloudStorageのrulesでは表現の難しいユーザー単位でのアクセスコントロールを予定しているのでHttpLoadBalancer, CloudCDNを利用する予定です。
CloudCDNを使用した静的コンテンツのアクセスコントロールについてはこちらの記事で紹介しています。
以下は署名付きCookieを使用した場合のシークエンス図になります。
Transcodeを考慮する
ユーザーが動画をアップロードする場合はYoutubeが が殆どだと考えられます。ただ、動画をオンデマンド配信するのにあたってこれらの形式を配信するのはあまり現実的ではありません。
例えばプログレッシブダウンロードに対応していないmp4を配信場合ダウンロード完了するまで視聴する事ができず、ユーザーはダウンロード完了するまで待機する必要があります。この時配信するコンテンツがFullHDで1時間程度の動画であれば約10GB程の転送量になります。Wifiや有線接続ならまだしも、4Gや速度制限がかけられているネットワークで試聴するとなると膨大な時間が必要になるのは目に見えています。
そこで配信可能な形式、今回はHLS形式にトランスコーディングする処理が必要になります。またデバイスの通信環境を考慮するのであれば配信するコンテンツの解像度はデバイスによって切り替えられるとUX向上につながります。
以上を考慮した上で実装すると以下のような構成になりました。
ユーザーはオリジナルデータを保存するバケットに対しコンテンツをアップロードします。CloudFunctionsのonFinalizedイベントをトリガーにアップロードされたオリジナルデータを複数の解像度を持ったHLS形式にトランスコードするリクエストをTranscoderAPIに送信します。
リクエストを受け取ったTranscoderAPIはCloudFunctionsのリクエストに基づいてHLS形式にトランスコードし、変換されたデータは配信用のバケットに保存されます。
以下はNode.jsを使用したFirebase CloudFunctionsのサンプルです。
const contentTypeRegex = /^video\/mp4$/
const extRegex = /^.mp4$/
const generateURI = (bucketID: string, path: string) =>
"gs://" + bucketID + "/" + path
export const callTranscoder = functions.storage
.bucket('origin')
.object()
.onFinalize(async (metadata) => {
if (!(metadata.name && metadata.contentType)) return
const objectPath = path.parse(metadata.name)
const mimeType = metadata.contentType
if (!(mimeType.match(contentTypeRegex) && objectPath.ext.match(extRegex)))
return
const matches = objectPath.dir.match(
/^contents\/video$/
)
if (!objectPath.dir.match(
/^contents\/video$/
)) return
const inputUri = generateURI('origin', metadata.name)
const outputUri = generateURI(
'transcoded',
`contents/video/${objectPath.name}/`
)
const request = {
parent: client.locationPath(projectID, regionID),
job: {
inputUri: inputUri,
outputUri: outputUri,
config: {
pubsubDestination: {
topic: `projects/${projectID}/topics/${topicID}`,
},
elementaryStreams: [
{
key: "video-stream0",
videoStream: {
h264: {
heightPixels: 360,
widthPixels: 640,
bitrateBps: 600000,
frameRate: 60,
gopDuration: { seconds: 10 },
},
},
},
{
key: "video-stream1",
videoStream: {
h264: {
heightPixels: 720,
widthPixels: 1280,
bitrateBps: 4000000,
frameRate: 60,
gopDuration: { seconds: 6 },
},
},
},
{
key: "video-stream2",
videoStream: {
h264: {
heightPixels: 1080,
widthPixels: 1920,
bitrateBps: 8000000,
frameRate: 60,
gopDuration: { seconds: 6 },
},
},
},
{
key: "audio-stream0",
audioStream: {
codec: "aac",
bitrateBps: 64000,
},
},
],
muxStreams: [
{
key: "media-sd",
container: "ts",
elementaryStreams: ["video-stream0", "audio-stream0"],
segmentSettings: {
segmentDuration: { seconds: 10 },
individualSegments: true,
},
},
{
key: "media-hd",
container: "ts",
elementaryStreams: ["video-stream1", "audio-stream0"],
segmentSettings: {
segmentDuration: { seconds: 6 },
individualSegments: true,
},
},
{
key: "media-fullhd",
container: "ts",
elementaryStreams: ["video-stream2", "audio-stream0"],
segmentSettings: {
segmentDuration: { seconds: 6 },
individualSegments: true,
},
},
],
manifests: [
{
fileName: "master.m3u8",
type: "HLS",
muxStreams: ["media-sd", "media-hd", "media-fullhd"],
} as { fileName: string; type: "HLS"; muxStreams: string[] },
],
},
},
}
const [response] = await client.createJob(request)
console.log("start job:", response.name)
})
このサンプルではアップロードされたmp4から60fpsの360p, 720p, 1080pの動画を生成しています。jobの設定についてはこちらで詳しく確認することができます。
TranscoderAPIを利用するにあたって注意するべき点としてはサポートしている形式と課金方法はよく確認した方が良いです。GCPでは多くのサービスが無料か、非常に低いコストで利用することができますがTranscoderAPIでは無料枠はありませんし、使用の仕方によっては無視できないほどのコストになりえます。
クライアントサイドの設計
今後追加予定です。
Author And Source
この問題について(【メモ】GCPで完結する動画配信サービスの設計と実装), 我々は、より多くの情報をここで見つけました https://zenn.dev/tera_ny/articles/fe2f6da2954e76著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol