[비고] GCP가 완성한 영상 배달 서비스의 설계와 설치

지금 할 수 없는 기능이 있기 때문에 자세히 기술하지 못하는 부분과 실제 규격과 다른 부분, 변경된 부분도 있다.그런 점을 감안하여 열람해 주시면 기쁘겠습니다.

서비스 사양


다음과 같은 특징을 가진 서비스를 설계하다.
  • 사용자가 mp4 형식의 애니메이션을 업로드
  • 업로드된 애니메이션을 HLS로 변환하여 열람 시 배포
  • 발송된 애니메이션은 사용자 단위의 접근을 제한할 수 있다
    이번에는 애니메이션을 주로 처리하는데 애니메이션 이외에도 이미지, 소리, 텍스트 데이터 등을 처리할 수 있다.
  • 배신 기초의 설계


    전송 시 CloudStorage를 사용합니다.게다가 클라우드 스토어의rules는 어려운 사용자를 단위로 접근 제어를 할 계획이어서 Http Load Balancer, 클라우드 CDN을 사용할 계획이다.
    클라우드 CDN을 사용하는 정적 내용에 대한 접근 제어는 여기.에 소개되어 있다.
    다음은 서명이 있는 쿠키를 사용할 때의 시퀀스입니다.
    sequenceDiagram
        participant 閲覧者
        participant ApplicationServer
        participant CloudCDN
        participant CloudStorage
        閲覧者->>ApplicationServer: send AuthorizationToken
        ApplicationServer->>閲覧者: Set-Cookie: Cloud-CDN-Cookie
        閲覧者->>CloudCDN: Cookie: Cloud-CDN-Cookie
        alt incorrect cookie value
            CloudCDN-->>閲覧者: 403 Error
        else correct cookie value and cached resource
            CloudCDN->>閲覧者: return resource
        else correct cookie value and not cached resource
            CloudStorage->>閲覧者: return resource
        end

    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의 360p720p1080p의 영상을 생성했다.잡에 대한 설정여기.은 자세히 확인할 수 있습니다.
    Transcoder API를 사용할 때 주의해야 할 점은 지원되는 형식요금 방법 확인해 보는 것이 좋다.GCP에서는 많은 서비스를 무료로 사용하거나 매우 낮은 비용으로 사용할 수 있지만 Transcoder API는 무료 프레임워크가 없어 사용 방법에 따라 무시할 수 없는 비용이 발생한다.

    클라이언트 설계


    향후 추가될 계획이다.

    좋은 웹페이지 즐겨찾기