Google Cloud Transcoder API を使ってみた記録

Google Cloud Transcoder API を使ってみた記録

January 15, 2022

例えば MP4 形式の動画を HLS 形式に変換する場合、AWS ならば Elemental MediaConvert があります。

では、GCP 上にサービスを構築している場合はどうしましょう?

GCP では 2020 年 11 月に Transcoder API というサービスの提供を始めています。これが GCP 版の Elemental MediaConvert にあたるものです。しかしベータ版という扱いだったこともあり、なかなか本格的に使用を開始するには躊躇われる状態でした。

そして 2021 年の 7 月、ついにベータ版を脱していたようで、試しに使ってみました。

以下、トランスコードを実行するための API の実行例を記載していきます。

ジョブテンプレートの登録/削除、ジョブ作成のコード例 #

Elemental MediaConvert と違い Transcoder API は今のところ GUI で操作することができません。Transcoder API に対する全ての操作は CLI や、以下に示すような SDK を介して行います。

ここでは Node.js 版の Transcoder API パッケージを利用してジョブテンプレートの登録や削除、ジョブの作成を行う例を示します。

これから説明に用いる関数を一通り先に定義しておきます。

import { v1 } from '@google-cloud/video-transcoder';

const projectId = 'example-project';
const location = 'asia-east1';

const client = new v1.TranscoderServiceClient();
const parent = client.locationPath(projectId, location);

const jobTemplate = {
  config: {
    elementaryStreams: [
      {
        key: 'video-stream0',
        videoStream: {
          h264: { heightPixels: 360, widthPixels: 640, bitrateBps: 550000, frameRate: 30 },
        },
      },
      {
        key: 'video-stream1',
        videoStream: {
          h264: { heightPixels: 720, widthPixels: 1280, bitrateBps: 2500000, frameRate: 30 },
        },
      },
      {
        key: 'audio-stream0',
        audioStream: { codec: 'aac', bitrateBps: 64000 },
      },
    ],
    muxStreams: [
      {
        key: 'sd',
        fileName: 'sd.ts',
        container: 'ts',
        elementaryStreams: ['video-stream0', 'audio-stream0'],
      },
      {
        key: 'hd',
        fileName: 'hd.ts',
        container: 'ts',
        elementaryStreams: ['video-stream1', 'audio-stream0'],
      },
    ],
    manifests: [
      {
        fileName: 'manifest.m3u8',
        type: 'HLS' as const,
        muxStreams: ['sd', 'hd'],
      },
    ],
  },
};

const createJobTemplate = () => {
  const jobTemplateId = 'hello-world';
  return client.createJobTemplate({ parent, jobTemplate, jobTemplateId });
};

const listJobTemplates = () => {
  return client.listJobTemplates({ parent });
};

const deleteJobTemplate = () => {
  const jobTemplateId = 'hello-world';
  const name = client.jobTemplatePath(projectId, location, jobTemplateId);
  return client.deleteJobTemplate({ name });
};

const createJob = (templateId: string, inputUri: string, outputUri: string) => {
  return client.createJob({ parent, job: { templateId, inputUri, outputUri } });
};

const getJob = (name: string) => {
  return client.getJob({ name });
};

ジョブテンプレートの作成・確認・削除 #

AWS Elemental MediaConvert でもそうですが、予めジョブのテンプレート(雛形)を作成しておき、実際にジョブを実行する際にはそのテンプレートを指定する場合が通常と思います。

createJobTemplate() #

これを実行すると、ジョブテンプレートを登録することができます。

上記のコード例では jobTemplate で定義している内容をもとに hello-world というジョブテンプレートが作成されます。

listJobTemplates() #

これを実行すると、現在定義されているジョブテンプレートの一覧を取得できます。

自分が指定した hello-world の名称でジョブテンプレートが登録されており、内容も自分が定義したものと一致していることがわかります。

[
  {
    name: 'projects/123456789abc/locations/asia-east1/jobTemplates/hello-world',
    config: {
      inputs: [{ key: 'input0', uri: '', preprocessingConfig: null }],
      editList: [
        {
          inputs: ['input0'],
          key: 'atom0',
          endTimeOffset: null,
          startTimeOffset: { seconds: '0', nanos: 0 },
        },
      ],
      elementaryStreams: [
        {
          key: 'video-stream0',
          videoStream: {
            h264: {
              widthPixels: 640,
              heightPixels: 360,
              frameRate: 30,
              bitrateBps: 550000,
              pixelFormat: 'yuv420p',
              rateControlMode: 'vbr',
              crfLevel: 21,
              allowOpenGop: false,
              enableTwoPass: false,
              vbvSizeBits: 550000,
              vbvFullnessBits: 495000,
              entropyCoder: 'cabac',
              bPyramid: false,
              bFrameCount: 0,
              aqStrength: 0,
              profile: 'high',
              tune: '',
              preset: 'veryfast',
              gopDuration: { seconds: '3', nanos: 0 },
              gopMode: 'gopDuration',
            },
            codecSettings: 'h264',
          },
          elementaryStream: 'videoStream',
        },
        {
          key: 'video-stream1',
          videoStream: {
            h264: {
              widthPixels: 1280,
              heightPixels: 720,
              frameRate: 30,
              bitrateBps: 2500000,
              pixelFormat: 'yuv420p',
              rateControlMode: 'vbr',
              crfLevel: 21,
              allowOpenGop: false,
              enableTwoPass: false,
              vbvSizeBits: 2500000,
              vbvFullnessBits: 2250000,
              entropyCoder: 'cabac',
              bPyramid: false,
              bFrameCount: 0,
              aqStrength: 0,
              profile: 'high',
              tune: '',
              preset: 'veryfast',
              gopDuration: { seconds: '3', nanos: 0 },
              gopMode: 'gopDuration',
            },
            codecSettings: 'h264',
          },
          elementaryStream: 'videoStream',
        },
        {
          key: 'audio-stream0',
          audioStream: {
            channelLayout: ['fl', 'fr'],
            mapping: [],
            codec: 'aac',
            bitrateBps: 64000,
            channelCount: 2,
            sampleRateHertz: 48000,
          },
          elementaryStream: 'audioStream',
        },
      ],
      muxStreams: [
        {
          elementaryStreams: ['video-stream0', 'audio-stream0'],
          key: 'sd',
          fileName: 'sd.ts',
          container: 'ts',
          segmentSettings: null,
        },
        {
          elementaryStreams: ['video-stream1', 'audio-stream0'],
          key: 'hd',
          fileName: 'hd.ts',
          container: 'ts',
          segmentSettings: null,
        },
      ],
      manifests: [{ muxStreams: ['sd', 'hd'], fileName: 'manifest.m3u8', type: 'HLS' }],
      adBreaks: [],
      spriteSheets: [],
      overlays: [],
      output: { uri: '' },
      pubsubDestination: null,
    },
  },
],

deleteJobTemplate() #

これを実行すると、指定したジョブテンプレートを削除することができます。

上記のコード例では hello-world というジョブテンプレートが削除されます。

ジョブの作成・確認 #

createJob() #

これを実行すると動画のトランスコードを行うジョブを発行できます。

  • jobTemplateId には、ジョブの実行に使用するジョブテンプレートを指定します。
  • inputUri には、変換元となる動画の Cloud Storage 上のファイルパスを指定します。
  • outputUri には、変換後のファイルの出力先となる Cloud Storage のディレクトリを指定します。
const templateId = 'hello-world'; // hello-world テンプレートを使用してジョブを実行するように指定
const inputUri = 'gs://videos/flight.mp4'; // videos バケット直下にある flight.mp4 を入力として指定
const outputUri = 'gs://streamings/flight/'; // streamings バケット直下の flight ディレクトリを出力先に指定
createJob(templateId, inputUri, outputUri);

実行結果として以下のレスポンスが返されます。

{
  "name": "projects//123456789abc//locations/asia-east1/jobs/f0806ec6-c4a4-4aa0-8f04-2da7273138fc",
  "inputUri": "",
  "outputUri": "",
  "state": "PENDING",
  "createTime": { "seconds": "1638015081", "nanos": 764452348 },
  "startTime": null,
  "endTime": null,
  "ttlAfterCompletionDays": 30,
  "error": null
  // (以降のプロパティは省略)
}

ジョブを発行した直後は statePENDING となっています。これは処理が実行待ち状態であることを示します。やがて自動的に処理が開始されます。

処理が成功すると、指定したディレクトリにトランスコード後のファイルが作成されています。今回の例で言えば、streamings バケットの flight ディレクトリ直下に作成されます。

name にはそのジョブの ID 記載されています。これを次の getJob() で指定することでジョブの状態を確認することができます。

getJob() #

これを実行するとジョブの状態を確認できます。

const name = 'projects/123456789abc/locations/asia-east1/jobs/f0806ec6-c4a4-4aa0-8f04-2da7273138fc';
getJob(name);

ジョブが成功した場合の例 #

stateSUCCEEDED になっています。

{
  "name": "projects/123456789abc/locations/asia-east1/jobs/f0806ec6-c4a4-4aa0-8f04-2da7273138fc",
  "inputUri": "",
  "outputUri": "",
  "state": "SUCCEEDED",
  "createTime": { "seconds": "1638015081", "nanos": 764452348 },
  "startTime": { "seconds": "1638015086", "nanos": 466000000 },
  "endTime": { "seconds": "1638015093", "nanos": 96921332 },
  "ttlAfterCompletionDays": 30,
  "error": null
  // (以降のプロパティは省略)
}

ジョブが失敗した場合の例 #

stateFAILED になっており、error にその理由が書かれています。

{
  "name": "projects/123456789abc/locations/asia-east1/jobs/f0806ec6-c4a4-4aa0-8f04-2da7273138fc",
  "inputUri": "",
  "outputUri": "",
  "state": "FAILED",
  "createTime": { "seconds": "1638015081", "nanos": 764452348 },
  "startTime": { "seconds": "1638015086", "nanos": 466000000 },
  "endTime": { "seconds": "1638015093", "nanos": 96921332 },
  "ttlAfterCompletionDays": 30,
  "error": {
    "details": [
      {
        "type_url": "type.googleapis.com/google.rpc.BadRequest",
        "value": {
          "type": "Buffer",
          "data": [
            10, 38, 10, 22, 99, 111, 110, 102, 105, 103, 46, 101, 100, 105, 116, 76, 105, 115, 116,
            115, 46, 97, 116, 111, 109, 48, 18, 12, 65, 117, 100, 105, 111, 77, 105, 115, 115, 105,
            110, 103
          ]
        }
      }
    ],
    "code": 3,
    "message": "atom atom0 does not have any inputs (input0) with an audio track"
  }
  // (以降のプロパティは省略)
}

例えば上記のジョブは以下の理由により失敗しています。

音声がありません

次の問題は、editList で音声トラックが指定されていない場合、または入力ファイルで音声トラックが検出されない場合に発生します。

https://cloud.google.com/transcoder/docs/troubleshooting

参考資料 #

上記では基本的な部分の機能のみご紹介しました。その他の機能は公式のドキュメントを参考にしてみてください。参考となりそうな資料をまとめました。

他の方の紹介記事

メディアにオススメ!Google 印の Transcoder API 爆誕! | Medium

上記記事はベータ版時点の仕様に基づいて書かれています。設定の定義方法など、この時点から微妙に変わっている仕様があります。変更された点は以下のドキュメントを参考ください。

v1 API への移行 | Transcoder API のドキュメント | Google Cloud

公式ドキュメント

Node.js ライブラリ関連

その他

本記事記事執筆時点では Transcoder API は日本リージョンをサポートしていません。(本記事のサンプルコードでは asia-east1(台湾) を使用しています。)

Cloud Storage と Transcoder API のリージョンが一致している必要はないため、日本リージョンの Cloud Storage に対して異なるリージョンの Transcoder API を使っても問題なく機能します。

将来的にはリージョンが追加されている可能性もあります。みなさんが利用される際は以下のページから改めて対応リージョンをご確認ください。

ロケーション - リージョンとゾーン | Google Cloud