diff --git a/.github/workflows/media-transcoder.yaml b/.github/workflows/media-transcoder.yaml new file mode 100644 index 0000000000..476f434de1 --- /dev/null +++ b/.github/workflows/media-transcoder.yaml @@ -0,0 +1,71 @@ +name: media-transcoder +on: + push: + branches: + - main + paths: + - 'media/transcoder/**' + - '.github/workflows/media-transcoder.yaml' + pull_request: + paths: + - 'media/transcoder/**' + - '.github/workflows/media-transcoder.yaml' + pull_request_target: + types: [labeled] + paths: + - 'media/transcoder/**' + - '.github/workflows/media-transcoder.yaml' + schedule: + - cron: '0 0 * * 0' +jobs: + test: + if: ${{ github.event.action != 'labeled' || github.event.label.name == 'actions:force-run' }} + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: 'write' + pull-requests: 'write' + id-token: 'write' + steps: + - uses: actions/checkout@v3.1.0 + with: + ref: ${{github.event.pull_request.head.sha}} + - uses: 'google-github-actions/auth@v1.0.0' + with: + workload_identity_provider: 'projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider' + service_account: 'kokoro-system-test@long-door-651.iam.gserviceaccount.com' + create_credentials_file: 'true' + access_token_lifetime: 600s + - uses: actions/setup-node@v3.5.1 + with: + node-version: 16 + - run: npm install + working-directory: media/transcoder + - run: npm test + working-directory: media/transcoder + env: + MOCHA_REPORTER_SUITENAME: media_transcoder + MOCHA_REPORTER_OUTPUT: media_transcoder_sponge_log.xml + MOCHA_REPORTER: xunit + - if: ${{ github.event.action == 'labeled' && github.event.label.name == 'actions:force-run' }} + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + try { + await github.rest.issues.removeLabel({ + name: 'actions:force-run', + owner: 'GoogleCloudPlatform', + repo: 'nodejs-docs-samples', + issue_number: context.payload.pull_request.number + }); + } catch (e) { + if (!e.message.includes('Label does not exist')) { + throw e; + } + } + - if: ${{ github.event_name == 'schedule' && always() }} + run: | + curl https://github.com/googleapis/repo-automation-bots/releases/download/flakybot-1.1.0/flakybot -o flakybot -s -L + chmod +x ./flakybot + ./flakybot --repo GoogleCloudPlatform/nodejs-docs-samples --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} diff --git a/.github/workflows/workflows.json b/.github/workflows/workflows.json index 6f5ca40618..a9457d68c3 100644 --- a/.github/workflows/workflows.json +++ b/.github/workflows/workflows.json @@ -69,6 +69,7 @@ "healthcare/hl7v2", "iam", "kms", + "media/transcoder", "media/video-stitcher", "mediatranslation", "monitoring/opencensus", diff --git a/media/transcoder/createJobFromAdHoc.js b/media/transcoder/createJobFromAdHoc.js new file mode 100644 index 0000000000..10eaa3c10c --- /dev/null +++ b/media/transcoder/createJobFromAdHoc.js @@ -0,0 +1,104 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, inputUri, outputUri) { + // [START transcoder_create_job_from_ad_hoc] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // inputUri = 'gs://my-bucket/my-video-file'; + // outputUri = 'gs://my-bucket/my-output-folder/'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobFromAdHoc() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + job: { + inputUri: inputUri, + outputUri: outputUri, + config: { + elementaryStreams: [ + { + key: 'video-stream0', + videoStream: { + h264: { + heightPixels: 360, + widthPixels: 640, + bitrateBps: 550000, + frameRate: 60, + }, + }, + }, + { + key: 'video-stream1', + videoStream: { + h264: { + heightPixels: 720, + widthPixels: 1280, + bitrateBps: 2500000, + frameRate: 60, + }, + }, + }, + { + key: 'audio-stream0', + audioStream: { + codec: 'aac', + bitrateBps: 64000, + }, + }, + ], + muxStreams: [ + { + key: 'sd', + container: 'mp4', + elementaryStreams: ['video-stream0', 'audio-stream0'], + }, + { + key: 'hd', + container: 'mp4', + elementaryStreams: ['video-stream1', 'audio-stream0'], + }, + ], + }, + }, + }; + + // Run request + const [response] = await transcoderServiceClient.createJob(request); + console.log(`Job: ${response.name}`); + } + + createJobFromAdHoc(); + // [END transcoder_create_job_from_ad_hoc] +} + +// node createJobFromAdHoc.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/createJobFromPreset.js b/media/transcoder/createJobFromPreset.js new file mode 100644 index 0000000000..814970d601 --- /dev/null +++ b/media/transcoder/createJobFromPreset.js @@ -0,0 +1,61 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, inputUri, outputUri, preset) { + // [START transcoder_create_job_from_preset] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // inputUri = 'gs://my-bucket/my-video-file'; + // outputUri = 'gs://my-bucket/my-output-folder/'; + // preset = 'preset/web-hd'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobFromPreset() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + job: { + inputUri: inputUri, + outputUri: outputUri, + templateId: preset, + }, + }; + + // Run request + const [response] = await transcoderServiceClient.createJob(request); + console.log(`Job: ${response.name}`); + } + + createJobFromPreset(); + // [END transcoder_create_job_from_preset] +} + +// node createJobFromPreset.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/createJobFromTemplate.js b/media/transcoder/createJobFromTemplate.js new file mode 100644 index 0000000000..dad57f3ff8 --- /dev/null +++ b/media/transcoder/createJobFromTemplate.js @@ -0,0 +1,61 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, inputUri, outputUri, templateId) { + // [START transcoder_create_job_from_template] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // inputUri = 'gs://my-bucket/my-video-file'; + // outputUri = 'gs://my-bucket/my-output-folder/'; + // templateId = 'my-job-template'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobFromTemplate() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + job: { + inputUri: inputUri, + outputUri: outputUri, + templateId: templateId, + }, + }; + + // Run request + const [response] = await transcoderServiceClient.createJob(request); + console.log(`Job: ${response.name}`); + } + + createJobFromTemplate(); + // [END transcoder_create_job_from_template] +} + +// node createJobFromTemplate.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/createJobTemplate.js b/media/transcoder/createJobTemplate.js new file mode 100644 index 0000000000..959518ee7c --- /dev/null +++ b/media/transcoder/createJobTemplate.js @@ -0,0 +1,104 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, templateId) { + // [START transcoder_create_job_template] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // templateId = 'my-job-template'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobTemplate() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + jobTemplateId: templateId, + jobTemplate: { + config: { + elementaryStreams: [ + { + key: 'video-stream0', + videoStream: { + h264: { + heightPixels: 360, + widthPixels: 640, + bitrateBps: 550000, + frameRate: 60, + }, + }, + }, + { + key: 'video-stream1', + videoStream: { + h264: { + heightPixels: 720, + widthPixels: 1280, + bitrateBps: 2500000, + frameRate: 60, + }, + }, + }, + { + key: 'audio-stream0', + audioStream: { + codec: 'aac', + bitrateBps: 64000, + }, + }, + ], + muxStreams: [ + { + key: 'sd', + container: 'mp4', + elementaryStreams: ['video-stream0', 'audio-stream0'], + }, + { + key: 'hd', + container: 'mp4', + elementaryStreams: ['video-stream1', 'audio-stream0'], + }, + ], + }, + }, + }; + + // Run request + const [jobTemplate] = await transcoderServiceClient.createJobTemplate( + request + ); + console.log(`Job template: ${jobTemplate.name}`); + } + + createJobTemplate(); + // [END transcoder_create_job_template] +} + +// node createJobTemplate.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/createJobWithAnimatedOverlay.js b/media/transcoder/createJobWithAnimatedOverlay.js new file mode 100644 index 0000000000..f8350fe216 --- /dev/null +++ b/media/transcoder/createJobWithAnimatedOverlay.js @@ -0,0 +1,133 @@ +/** + * Copyright 2021, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, inputUri, overlayImageUri, outputUri) { + // [START transcoder_create_job_with_animated_overlay] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // inputUri = 'gs://my-bucket/my-video-file'; + // overlayImageUri = 'gs://my-bucket/my-overlay-image-file'; // Must be a JPEG + // outputUri = 'gs://my-bucket/my-output-folder/'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobFromAnimatedOverlay() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + job: { + inputUri: inputUri, + outputUri: outputUri, + config: { + elementaryStreams: [ + { + key: 'video-stream0', + videoStream: { + h264: { + heightPixels: 360, + widthPixels: 640, + bitrateBps: 550000, + frameRate: 60, + }, + }, + }, + { + key: 'audio-stream0', + audioStream: { + codec: 'aac', + bitrateBps: 64000, + }, + }, + ], + muxStreams: [ + { + key: 'sd', + container: 'mp4', + elementaryStreams: ['video-stream0', 'audio-stream0'], + }, + ], + overlays: [ + { + image: { + uri: overlayImageUri, + resolution: { + x: 0, + y: 0, + }, + alpha: 1.0, + }, + animations: [ + { + animationFade: { + fadeType: 'FADE_IN', + xy: { + x: 0.5, + y: 0.5, + }, + startTimeOffset: { + seconds: 5, + }, + endTimeOffset: { + seconds: 10, + }, + }, + }, + { + animationFade: { + fadeType: 'FADE_OUT', + xy: { + x: 0.5, + y: 0.5, + }, + startTimeOffset: { + seconds: 12, + }, + endTimeOffset: { + seconds: 15, + }, + }, + }, + ], + }, + ], + }, + }, + }; + + // Run request + const [response] = await transcoderServiceClient.createJob(request); + console.log(`Job: ${response.name}`); + } + + createJobFromAnimatedOverlay(); + // [END transcoder_create_job_with_animated_overlay] +} + +// node createJobFromAnimatedOverlay.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/createJobWithConcatenatedInputs.js b/media/transcoder/createJobWithConcatenatedInputs.js new file mode 100644 index 0000000000..523aed7e3f --- /dev/null +++ b/media/transcoder/createJobWithConcatenatedInputs.js @@ -0,0 +1,158 @@ +/** + * Copyright 2021, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main( + projectId, + location, + inputUri1, + startTimeOffset1, + endTimeOffset1, + inputUri2, + startTimeOffset2, + endTimeOffset2, + outputUri +) { + // [START transcoder_create_job_with_concatenated_inputs] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // inputUri1 = 'gs://my-bucket/my-video-file1'; + // startTimeOffset1 = 0; + // endTimeOffset1 = 8.1; + // inputUri2 = 'gs://my-bucket/my-video-file2'; + // startTimeOffset2 = 3.5; + // endTimeOffset2 = 15; + // outputUri = 'gs://my-bucket/my-output-folder/'; + + function calcOffsetNanoSec(offsetValueFractionalSecs) { + if (offsetValueFractionalSecs.toString().indexOf('.') !== -1) { + return ( + 1000000000 * + Number('.' + offsetValueFractionalSecs.toString().split('.')[1]) + ); + } + return 0; + } + const startTimeOffset1Sec = Math.trunc(startTimeOffset1); + const startTimeOffset1NanoSec = calcOffsetNanoSec(startTimeOffset1); + const endTimeOffset1Sec = Math.trunc(endTimeOffset1); + const endTimeOffset1NanoSec = calcOffsetNanoSec(endTimeOffset1); + + const startTimeOffset2Sec = Math.trunc(startTimeOffset2); + const startTimeOffset2NanoSec = calcOffsetNanoSec(startTimeOffset2); + const endTimeOffset2Sec = Math.trunc(endTimeOffset2); + const endTimeOffset2NanoSec = calcOffsetNanoSec(endTimeOffset2); + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobWithConcatenatedInputs() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + job: { + outputUri: outputUri, + config: { + inputs: [ + { + key: 'input1', + uri: inputUri1, + }, + { + key: 'input2', + uri: inputUri2, + }, + ], + editList: [ + { + key: 'atom1', + inputs: ['input1'], + startTimeOffset: { + seconds: startTimeOffset1Sec, + nanos: startTimeOffset1NanoSec, + }, + endTimeOffset: { + seconds: endTimeOffset1Sec, + nanos: endTimeOffset1NanoSec, + }, + }, + { + key: 'atom2', + inputs: ['input2'], + startTimeOffset: { + seconds: startTimeOffset2Sec, + nanos: startTimeOffset2NanoSec, + }, + endTimeOffset: { + seconds: endTimeOffset2Sec, + nanos: endTimeOffset2NanoSec, + }, + }, + ], + + elementaryStreams: [ + { + key: 'video-stream0', + videoStream: { + h264: { + heightPixels: 360, + widthPixels: 640, + bitrateBps: 550000, + frameRate: 60, + }, + }, + }, + { + key: 'audio-stream0', + audioStream: { + codec: 'aac', + bitrateBps: 64000, + }, + }, + ], + muxStreams: [ + { + key: 'sd', + container: 'mp4', + elementaryStreams: ['video-stream0', 'audio-stream0'], + }, + ], + }, + }, + }; + + // Run request + const [response] = await transcoderServiceClient.createJob(request); + console.log(`Job: ${response.name}`); + } + + createJobWithConcatenatedInputs(); + // [END transcoder_create_job_with_concatenated_inputs] +} + +// node createJobFromStaticOverlay.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/createJobWithEmbeddedCaptions.js b/media/transcoder/createJobWithEmbeddedCaptions.js new file mode 100644 index 0000000000..dc53906d0e --- /dev/null +++ b/media/transcoder/createJobWithEmbeddedCaptions.js @@ -0,0 +1,144 @@ +/** + * Copyright 2022, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, inputVideoUri, inputCaptionsUri, outputUri) { + // [START transcoder_create_job_with_embedded_captions] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // inputVideoUri = 'gs://my-bucket/my-video-file'; + // inputCaptionsUri = 'gs://my-bucket/my-captions-file'; + // outputUri = 'gs://my-bucket/my-output-folder/'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobWithEmbeddedCaptions() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + job: { + outputUri: outputUri, + config: { + inputs: [ + { + key: 'input0', + uri: inputVideoUri, + }, + { + key: 'caption_input0', + uri: inputCaptionsUri, + }, + ], + editList: [ + { + key: 'atom0', + inputs: ['input0', 'caption_input0'], + }, + ], + elementaryStreams: [ + { + key: 'video-stream0', + videoStream: { + h264: { + heightPixels: 360, + widthPixels: 640, + bitrateBps: 550000, + frameRate: 60, + }, + }, + }, + { + key: 'audio-stream0', + audioStream: { + codec: 'aac', + bitrateBps: 64000, + }, + }, + { + key: 'cea-stream0', + textStream: { + codec: 'cea608', + mapping: [ + { + atomKey: 'atom0', + inputKey: 'caption_input0', + inputTrack: 0, + }, + ], + }, + }, + ], + muxStreams: [ + { + key: 'sd', + container: 'mp4', + elementaryStreams: ['video-stream0', 'audio-stream0'], + }, + { + key: 'sd-hls', + container: 'ts', + elementaryStreams: ['video-stream0', 'audio-stream0'], + }, + { + key: 'sd-dash', + container: 'fmp4', + elementaryStreams: ['video-stream0'], + }, + { + key: 'audio-dash', + container: 'fmp4', + elementaryStreams: ['audio-stream0'], + }, + ], + manifests: [ + { + fileName: 'manifest.m3u8', + type: 'HLS', + muxStreams: ['sd-hls'], + }, + { + fileName: 'manifest.mpd', + type: 'DASH', + muxStreams: ['sd-dash', 'audio-dash'], + }, + ], + }, + }, + }; + + // Run request + const [response] = await transcoderServiceClient.createJob(request); + console.log(`Job: ${response.name}`); + } + + createJobWithEmbeddedCaptions(); + // [END transcoder_create_job_with_embedded_captions] +} + +// node createJobWithEmbeddedCaptions.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/createJobWithPeriodicImagesSpritesheet.js b/media/transcoder/createJobWithPeriodicImagesSpritesheet.js new file mode 100644 index 0000000000..ee8f781604 --- /dev/null +++ b/media/transcoder/createJobWithPeriodicImagesSpritesheet.js @@ -0,0 +1,106 @@ +/** + * Copyright 2021, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, inputUri, outputUri) { + // [START transcoder_create_job_with_periodic_images_spritesheet] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // inputUri = 'gs://my-bucket/my-video-file'; + // outputUri = 'gs://my-bucket/my-output-folder/'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobWithPeriodicImagesSpritesheet() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + job: { + inputUri: inputUri, + outputUri: outputUri, + config: { + elementaryStreams: [ + { + key: 'video-stream0', + videoStream: { + h264: { + heightPixels: 360, + widthPixels: 640, + bitrateBps: 550000, + frameRate: 60, + }, + }, + }, + { + key: 'audio-stream0', + audioStream: { + codec: 'aac', + bitrateBps: 64000, + }, + }, + ], + muxStreams: [ + { + key: 'sd', + container: 'mp4', + elementaryStreams: ['video-stream0', 'audio-stream0'], + }, + ], + spriteSheets: [ + { + filePrefix: 'small-sprite-sheet', + spriteHeightPixels: 32, + spriteWidthPixels: 64, + interval: { + seconds: 7, + }, + }, + { + filePrefix: 'large-sprite-sheet', + spriteHeightPixels: 72, + spriteWidthPixels: 128, + interval: { + seconds: 7, + }, + }, + ], + }, + }, + }; + + // Run request + const [response] = await transcoderServiceClient.createJob(request); + console.log(`Job: ${response.name}`); + } + + createJobWithPeriodicImagesSpritesheet(); + // [END transcoder_create_job_with_periodic_images_spritesheet] +} + +// node createJobWithPeriodicImagesSpritesheet.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/createJobWithSetNumberImagesSpritesheet.js b/media/transcoder/createJobWithSetNumberImagesSpritesheet.js new file mode 100644 index 0000000000..6c2f71a7e6 --- /dev/null +++ b/media/transcoder/createJobWithSetNumberImagesSpritesheet.js @@ -0,0 +1,106 @@ +/** + * Copyright 2021, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, inputUri, outputUri) { + // [START transcoder_create_job_with_set_number_images_spritesheet] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // inputUri = 'gs://my-bucket/my-video-file'; + // outputUri = 'gs://my-bucket/my-output-folder/'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobWithSetNumberImagesSpritesheet() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + job: { + inputUri: inputUri, + outputUri: outputUri, + config: { + elementaryStreams: [ + { + key: 'video-stream0', + videoStream: { + h264: { + heightPixels: 360, + widthPixels: 640, + bitrateBps: 550000, + frameRate: 60, + }, + }, + }, + { + key: 'audio-stream0', + audioStream: { + codec: 'aac', + bitrateBps: 64000, + }, + }, + ], + muxStreams: [ + { + key: 'sd', + container: 'mp4', + elementaryStreams: ['video-stream0', 'audio-stream0'], + }, + ], + spriteSheets: [ + { + filePrefix: 'small-sprite-sheet', + spriteHeightPixels: 32, + spriteWidthPixels: 64, + columnCount: 10, + rowCount: 10, + totalCount: 100, + }, + { + filePrefix: 'large-sprite-sheet', + spriteHeightPixels: 72, + spriteWidthPixels: 128, + columnCount: 10, + rowCount: 10, + totalCount: 100, + }, + ], + }, + }, + }; + + // Run request + const [response] = await transcoderServiceClient.createJob(request); + console.log(`Job: ${response.name}`); + } + + createJobWithSetNumberImagesSpritesheet(); + // [END transcoder_create_job_with_set_number_images_spritesheet] +} + +// node createJobWithSetNumberImagesSpritesheet.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/createJobWithStandaloneCaptions.js b/media/transcoder/createJobWithStandaloneCaptions.js new file mode 100644 index 0000000000..1ffb6bd1cc --- /dev/null +++ b/media/transcoder/createJobWithStandaloneCaptions.js @@ -0,0 +1,140 @@ +/** + * Copyright 2022, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, inputVideoUri, inputCaptionsUri, outputUri) { + // [START transcoder_create_job_with_standalone_captions] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // inputVideoUri = 'gs://my-bucket/my-video-file'; + // inputCaptionsUri = 'gs://my-bucket/my-captions-file'; + // outputUri = 'gs://my-bucket/my-output-folder/'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobWithStandaloneCaptions() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + job: { + outputUri: outputUri, + config: { + inputs: [ + { + key: 'input0', + uri: inputVideoUri, + }, + { + key: 'caption_input0', + uri: inputCaptionsUri, + }, + ], + editList: [ + { + key: 'atom0', + inputs: ['input0', 'caption_input0'], + }, + ], + elementaryStreams: [ + { + key: 'video-stream0', + videoStream: { + h264: { + heightPixels: 360, + widthPixels: 640, + bitrateBps: 550000, + frameRate: 60, + }, + }, + }, + { + key: 'audio-stream0', + audioStream: { + codec: 'aac', + bitrateBps: 64000, + }, + }, + { + key: 'vtt-stream0', + textStream: { + codec: 'webvtt', + mapping: [ + { + atomKey: 'atom0', + inputKey: 'caption_input0', + inputTrack: 0, + }, + ], + }, + }, + ], + muxStreams: [ + { + key: 'sd-hls-fmp4', + container: 'fmp4', + elementaryStreams: ['video-stream0'], + }, + { + key: 'audio-hls-fmp4', + container: 'fmp4', + elementaryStreams: ['audio-stream0'], + }, + { + key: 'text-vtt', + container: 'vtt', + elementaryStreams: ['vtt-stream0'], + segmentSettings: { + segmentDuration: { + seconds: 6, + }, + individualSegments: true, + }, + }, + ], + manifests: [ + { + fileName: 'manifest.m3u8', + type: 'HLS', + muxStreams: ['sd-hls-fmp4', 'audio-hls-fmp4', 'text-vtt'], + }, + ], + }, + }, + }; + + // Run request + const [response] = await transcoderServiceClient.createJob(request); + console.log(`Job: ${response.name}`); + } + + createJobWithStandaloneCaptions(); + // [END transcoder_create_job_with_standalone_captions] +} + +// node createJobWithStandaloneCaptions.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/createJobWithStaticOverlay.js b/media/transcoder/createJobWithStaticOverlay.js new file mode 100644 index 0000000000..d311879281 --- /dev/null +++ b/media/transcoder/createJobWithStaticOverlay.js @@ -0,0 +1,121 @@ +/** + * Copyright 2021, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, inputUri, overlayImageUri, outputUri) { + // [START transcoder_create_job_with_static_overlay] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // inputUri = 'gs://my-bucket/my-video-file'; + // overlayImageUri = 'gs://my-bucket/my-overlay-image-file'; // Must be a JPEG + // outputUri = 'gs://my-bucket/my-output-folder/'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function createJobFromStaticOverlay() { + // Construct request + const request = { + parent: transcoderServiceClient.locationPath(projectId, location), + job: { + inputUri: inputUri, + outputUri: outputUri, + config: { + elementaryStreams: [ + { + key: 'video-stream0', + videoStream: { + h264: { + heightPixels: 360, + widthPixels: 640, + bitrateBps: 550000, + frameRate: 60, + }, + }, + }, + { + key: 'audio-stream0', + audioStream: { + codec: 'aac', + bitrateBps: 64000, + }, + }, + ], + muxStreams: [ + { + key: 'sd', + container: 'mp4', + elementaryStreams: ['video-stream0', 'audio-stream0'], + }, + ], + overlays: [ + { + image: { + uri: overlayImageUri, + resolution: { + x: 1, + y: 0.5, + }, + alpha: 1.0, + }, + animations: [ + { + animationStatic: { + xy: { + x: 0, + y: 0, + }, + startTimeOffset: { + seconds: 0, + }, + }, + }, + { + animationEnd: { + startTimeOffset: { + seconds: 10, + }, + }, + }, + ], + }, + ], + }, + }, + }; + + // Run request + const [response] = await transcoderServiceClient.createJob(request); + console.log(`Job: ${response.name}`); + } + + createJobFromStaticOverlay(); + // [END transcoder_create_job_with_static_overlay] +} + +// node createJobFromStaticOverlay.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/deleteJob.js b/media/transcoder/deleteJob.js new file mode 100644 index 0000000000..b173e10dce --- /dev/null +++ b/media/transcoder/deleteJob.js @@ -0,0 +1,52 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, jobId) { + // [START transcoder_delete_job] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // jobId = 'my-job-id'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function deleteJob() { + // Construct request + const request = { + name: transcoderServiceClient.jobPath(projectId, location, jobId), + }; + await transcoderServiceClient.deleteJob(request); + console.log('Deleted job'); + } + + deleteJob(); + // [END transcoder_delete_job] +} + +// node deleteJob.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/deleteJobTemplate.js b/media/transcoder/deleteJobTemplate.js new file mode 100644 index 0000000000..0bcb8659ff --- /dev/null +++ b/media/transcoder/deleteJobTemplate.js @@ -0,0 +1,56 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, templateId) { + // [START transcoder_delete_job_template] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // templateId = 'my-job-template'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function deleteJobTemplate() { + // Construct request + const request = { + name: transcoderServiceClient.jobTemplatePath( + projectId, + location, + templateId + ), + }; + await transcoderServiceClient.deleteJobTemplate(request); + console.log('Deleted job template'); + } + + deleteJobTemplate(); + // [END transcoder_delete_job_template] +} + +// node deleteJobTemplate.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/getJob.js b/media/transcoder/getJob.js new file mode 100644 index 0000000000..af81f04edc --- /dev/null +++ b/media/transcoder/getJob.js @@ -0,0 +1,52 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, jobId) { + // [START transcoder_get_job] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // jobId = 'my-job-id'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function getJob() { + // Construct request + const request = { + name: transcoderServiceClient.jobPath(projectId, location, jobId), + }; + const [response] = await transcoderServiceClient.getJob(request); + console.log(`Job: ${response.name}`); + } + + getJob(); + // [END transcoder_get_job] +} + +// node getJob.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/getJobState.js b/media/transcoder/getJobState.js new file mode 100644 index 0000000000..1a40525e17 --- /dev/null +++ b/media/transcoder/getJobState.js @@ -0,0 +1,52 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, jobId) { + // [START transcoder_get_job_state] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // jobId = 'my-job-id'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function getJob() { + // Construct request + const request = { + name: transcoderServiceClient.jobPath(projectId, location, jobId), + }; + const [response] = await transcoderServiceClient.getJob(request); + console.log(`Job state: ${response.state}`); + } + + getJob(); + // [END transcoder_get_job_state] +} + +// node getJobState.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/getJobTemplate.js b/media/transcoder/getJobTemplate.js new file mode 100644 index 0000000000..29c7b6f732 --- /dev/null +++ b/media/transcoder/getJobTemplate.js @@ -0,0 +1,56 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location, templateId) { + // [START transcoder_get_job_template] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // templateId = 'my-job-template'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function getJobTemplate() { + // Construct request + const request = { + name: transcoderServiceClient.jobTemplatePath( + projectId, + location, + templateId + ), + }; + const [jobTemplate] = await transcoderServiceClient.getJobTemplate(request); + console.log(`Job template: ${jobTemplate.name}`); + } + + getJobTemplate(); + // [END transcoder_get_job_template] +} + +// node getJobTemplate.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/listJobTemplates.js b/media/transcoder/listJobTemplates.js new file mode 100644 index 0000000000..cb3e32861a --- /dev/null +++ b/media/transcoder/listJobTemplates.js @@ -0,0 +1,52 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location) { + // [START transcoder_list_job_templates] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function listJobTemplates() { + const iterable = await transcoderServiceClient.listJobTemplatesAsync({ + parent: transcoderServiceClient.locationPath(projectId, location), + }); + console.info('Job templates:'); + for await (const response of iterable) { + console.log(response.name); + } + } + + listJobTemplates(); + // [END transcoder_list_job_templates] +} + +// node listJobTemplates.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/listJobs.js b/media/transcoder/listJobs.js new file mode 100644 index 0000000000..8c24c0b23c --- /dev/null +++ b/media/transcoder/listJobs.js @@ -0,0 +1,52 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +function main(projectId, location) { + // [START transcoder_list_jobs] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + + // Imports the Transcoder library + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + + // Instantiates a client + const transcoderServiceClient = new TranscoderServiceClient(); + + async function listJobs() { + const iterable = await transcoderServiceClient.listJobsAsync({ + parent: transcoderServiceClient.locationPath(projectId, location), + }); + console.info('Jobs:'); + for await (const response of iterable) { + console.log(response.name); + } + } + + listJobs(); + // [END transcoder_list_jobs] +} + +// node listJobs.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/transcoder/package.json b/media/transcoder/package.json new file mode 100644 index 0000000000..24b23129bd --- /dev/null +++ b/media/transcoder/package.json @@ -0,0 +1,25 @@ +{ + "name": "nodejs-video-transcoder-samples", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=12.0.0" + }, + "files": [ + "*.js", + "!test" + ], + "scripts": { + "test": "c8 mocha --timeout 600000 test/*.js" + }, + "dependencies": { + "@google-cloud/video-transcoder": "^2.2.3" + }, + "devDependencies": { + "@google-cloud/storage": "^6.0.0", + "c8": "^7.3.0", + "mocha": "^8.1.1", + "uuid": "^9.0.0" + } +} diff --git a/media/transcoder/test/transcoder.test.js b/media/transcoder/test/transcoder.test.js new file mode 100644 index 0000000000..199d10ab39 --- /dev/null +++ b/media/transcoder/test/transcoder.test.js @@ -0,0 +1,753 @@ +/** + * Copyright 2020, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const path = require('path'); +const assert = require('assert'); +const {v4: uuidv4} = require('uuid'); +const {execSync} = require('child_process'); +const {describe, it, before, after, afterEach} = require('mocha'); + +const {Storage} = require('@google-cloud/storage'); +const uniqueID = uuidv4().split('-')[0]; +const bucketName = `nodejs-samples-transcoder-test-${uniqueID}`; +const storage = new Storage(); + +const projectId = process.env.GCLOUD_PROJECT; +const location = 'us-central1'; +const templateId = `nodejs-test-transcoder-template-${uniqueID}`; +const preset = 'preset/web-hd'; +const templateName = `/locations/${location}/jobTemplates/${templateId}`; + +const testFileName = 'ChromeCast.mp4'; +const testOverlayFileName = 'overlay.jpg'; +const testConcat1FileName = 'ForBiggerEscapes.mp4'; +const testConcat2FileName = 'ForBiggerJoyrides.mp4'; +const testCaptionFileName = 'caption.srt'; +const inputUri = `gs://${bucketName}/${testFileName}`; +const overlayUri = `gs://${bucketName}/${testOverlayFileName}`; +const concat1Uri = `gs://${bucketName}/${testConcat1FileName}`; +const concat2Uri = `gs://${bucketName}/${testConcat2FileName}`; +const captionsUri = `gs://${bucketName}/${testCaptionFileName}`; +const outputUriForPreset = `gs://${bucketName}/test-output-preset/`; +const outputUriForTemplate = `gs://${bucketName}/test-output-template/`; +const outputUriForAdHoc = `gs://${bucketName}/test-output-adhoc/`; +const outputUriForStaticOverlay = `gs://${bucketName}/test-output-static-overlay/`; +const outputUriForAnimatedOverlay = `gs://${bucketName}/test-output-animated-overlay/`; +const outputDirForSetNumberImagesSpritesheet = + 'test-output-set-number-spritesheet/'; +const outputUriForSetNumberImagesSpritesheet = `gs://${bucketName}/${outputDirForSetNumberImagesSpritesheet}`; +const outputDirForPeriodicImagesSpritesheet = + 'test-output-periodic-spritesheet/'; +const outputUriForPeriodicImagesSpritesheet = `gs://${bucketName}/${outputDirForPeriodicImagesSpritesheet}`; +// Spritesheets are generated from the input video into the bucket directories above. +// Spritesheets use the following file naming conventions: +const smallSpriteSheetFileName = 'small-sprite-sheet0000000000.jpeg'; +const largeSpriteSheetFileName = 'large-sprite-sheet0000000000.jpeg'; +const outputUriForConcatenated = `gs://${bucketName}/test-output-concat/`; +const outputUriForEmbeddedCaptions = `gs://${bucketName}/test-output-embedded-captions/`; +const outputUriForStandaloneCaptions = `gs://${bucketName}/test-output-standalone-captions/`; + +const cwd = path.join(__dirname, '..'); +const videoFile = `testdata/${testFileName}`; +const overlayFile = `testdata/${testOverlayFileName}`; +const concat1File = `testdata/${testConcat1FileName}`; +const concat2File = `testdata/${testConcat2FileName}`; +const captionFile = `testdata/${testCaptionFileName}`; + +const delay = async (test, addMs) => { + const retries = test.currentRetry(); + await new Promise(r => setTimeout(r, addMs)); + // No retry on the first failure. + if (retries === 0) return; + // See: https://cloud.google.com/storage/docs/exponential-backoff + const ms = Math.pow(2, retries) * 10000 + Math.random() * 1000; + return new Promise(done => { + console.info(`retrying "${test.title}" in ${ms}ms`); + setTimeout(done, ms); + }); +}; + +function wait(ms) { + return new Promise(resolve => { + setTimeout(() => { + return resolve(); + }, ms); + }); +} + +const checkFileExists = async function (bucketName, fileName) { + const [files] = await storage.bucket(bucketName).getFiles(); + for (let i = 0; i < files.length; i++) { + if (files[i].name === fileName) { + return true; + } + } + return false; +}; + +before(async () => { + // Create a Cloud Storage bucket to be used for testing. + await storage.createBucket(bucketName); + await storage.bucket(bucketName).upload(videoFile); + await storage.bucket(bucketName).upload(overlayFile); + await storage.bucket(bucketName).upload(concat1File); + await storage.bucket(bucketName).upload(concat2File); + await storage.bucket(bucketName).upload(captionFile); +}); + +after(async () => { + async function deleteFiles() { + const [files] = await storage.bucket(bucketName).getFiles(); + for (const file of files) { + await storage.bucket(bucketName).file(file.name).delete(); + } + } + try { + await deleteFiles(); + await storage.bucket(bucketName).delete(); + } catch (err) { + console.log('Cannot delete bucket'); + } + // Delete outstanding jobs created more than 3 days ago + const {TranscoderServiceClient} = + require('@google-cloud/video-transcoder').v1; + const transcoderServiceClient = new TranscoderServiceClient(); + const [jobs] = await transcoderServiceClient.listJobs({ + parent: transcoderServiceClient.locationPath(projectId, location), + }); + const THREE_DAYS_IN_SEC = 60 * 60 * 24 * 3; + const DATE_NOW_SEC = Math.floor(Date.now() / 1000); + + for (const job of jobs) { + if (job.createTime.seconds < DATE_NOW_SEC - THREE_DAYS_IN_SEC) { + const request = { + name: job.name, + }; + await transcoderServiceClient.deleteJob(request); + } + } +}); + +describe('Job template functions', () => { + before(() => { + // Delete the job template if it already exists + try { + execSync( + `node deleteJobTemplate.js ${projectId} ${location} ${templateId}`, + { + cwd, + } + ); + } catch (err) { + // ignore not found error + } + + const output = execSync( + `node createJobTemplate.js ${projectId} ${location} ${templateId}`, + {cwd} + ); + assert.ok(output.includes(templateName)); + }); + + after(() => { + const output = execSync( + `node deleteJobTemplate.js ${projectId} ${location} ${templateId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job template')); + }); + + it('should get a job template', () => { + const output = execSync( + `node getJobTemplate.js ${projectId} ${location} ${templateId}`, + {cwd} + ); + assert.ok(output.includes(templateName)); + }); + + it('should show a list of job templates', () => { + const output = execSync( + `node listJobTemplates.js ${projectId} ${location}`, + { + cwd, + } + ); + assert.ok(output.includes(templateName)); + }); +}); + +describe('Job functions preset', () => { + let presetJobId; + function createJobFromPreset() { + const output = execSync( + `node createJobFromPreset.js ${projectId} ${location} ${inputUri} ${outputUriForPreset} ${preset}`, + {cwd} + ); + assert.ok(output.includes(`/locations/${location}/jobs/`)); + presetJobId = output.toString().split('/').pop(); + } + + afterEach(() => { + const output = execSync( + `node deleteJob.js ${projectId} ${location} ${presetJobId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job')); + }); + + it('should get a job', () => { + createJobFromPreset(); + const output = execSync( + `node getJob.js ${projectId} ${location} ${presetJobId}`, + {cwd} + ); + const jobName = `/locations/${location}/jobs/${presetJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should show a list of jobs', () => { + createJobFromPreset(); + const output = execSync(`node listJobs.js ${projectId} ${location}`, { + cwd, + }); + const jobName = `/locations/${location}/jobs/${presetJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should check that the job succeeded', async function () { + this.retries(5); + createJobFromPreset(); + await delay(this.test, 30000); + + let getAttempts = 0; + while (getAttempts < 5) { + const ms = Math.pow(2, getAttempts + 1) * 10000 + Math.random() * 1000; + await wait(ms); + const output = execSync( + `node getJobState.js ${projectId} ${location} ${presetJobId}`, + {cwd} + ); + if (output.includes('Job state: SUCCEEDED')) { + assert.ok(true); + return; + } + getAttempts++; + } + assert.ok(false); + }); +}); + +describe('Job functions template', () => { + before(function () { + let output = execSync( + `node createJobTemplate.js ${projectId} ${location} ${templateId}`, + {cwd} + ); + assert.ok(output.includes(templateName)); + output = execSync( + `node createJobFromTemplate.js ${projectId} ${location} ${inputUri} ${outputUriForTemplate} ${templateId}`, + {cwd} + ); + assert.ok(output.includes(`/locations/${location}/jobs/`)); + this.templateJobId = output.toString().split('/').pop(); + }); + + after(function () { + let output = execSync( + `node deleteJob.js ${projectId} ${location} ${this.templateJobId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job')); + output = execSync( + `node deleteJobTemplate.js ${projectId} ${location} ${templateId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job template')); + }); + + it('should get a job', function () { + const output = execSync( + `node getJob.js ${projectId} ${location} ${this.templateJobId}`, + {cwd} + ); + const jobName = `/locations/${location}/jobs/${this.templateJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should show a list of jobs', function () { + const output = execSync(`node listJobs.js ${projectId} ${location}`, { + cwd, + }); + const jobName = `/locations/${location}/jobs/${this.templateJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should check that the job succeeded', async function () { + this.retries(5); + await delay(this.test, 30000); + + let getAttempts = 0; + while (getAttempts < 5) { + const ms = Math.pow(2, getAttempts + 1) * 10000 + Math.random() * 1000; + await wait(ms); + const output = execSync( + `node getJobState.js ${projectId} ${location} ${this.templateJobId}`, + {cwd} + ); + if (output.includes('Job state: SUCCEEDED')) { + assert.ok(true); + return; + } + getAttempts++; + } + assert.ok(false); + }); +}); + +describe('Job functions adhoc', () => { + before(function () { + const output = execSync( + `node createJobFromAdHoc.js ${projectId} ${location} ${inputUri} ${outputUriForAdHoc}`, + {cwd} + ); + assert.ok(output.includes(`/locations/${location}/jobs/`)); + this.adhocJobId = output.toString().split('/').pop(); + }); + + after(function () { + const output = execSync( + `node deleteJob.js ${projectId} ${location} ${this.adhocJobId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job')); + }); + + it('should get a job', function () { + const output = execSync( + `node getJob.js ${projectId} ${location} ${this.adhocJobId}`, + {cwd} + ); + const jobName = `/locations/${location}/jobs/${this.adhocJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should show a list of jobs', function () { + const output = execSync(`node listJobs.js ${projectId} ${location}`, { + cwd, + }); + const jobName = `/locations/${location}/jobs/${this.adhocJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should check that the job succeeded', async function () { + this.retries(5); + await delay(this.test, 30000); + + let getAttempts = 0; + while (getAttempts < 5) { + const ms = Math.pow(2, getAttempts + 1) * 10000 + Math.random() * 1000; + await wait(ms); + const output = execSync( + `node getJobState.js ${projectId} ${location} ${this.adhocJobId}`, + {cwd} + ); + if (output.includes('Job state: SUCCEEDED')) { + assert.ok(true); + return; + } + getAttempts++; + } + assert.ok(false); + }); +}); + +describe('Job with static overlay functions', () => { + before(function () { + const output = execSync( + `node createJobWithStaticOverlay.js ${projectId} ${location} ${inputUri} ${overlayUri} ${outputUriForStaticOverlay}`, + {cwd} + ); + assert.ok(output.includes(`/locations/${location}/jobs/`)); + this.staticOverlayJobId = output.toString().split('/').pop(); + }); + + after(function () { + const output = execSync( + `node deleteJob.js ${projectId} ${location} ${this.staticOverlayJobId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job')); + }); + + it('should get a job', function () { + const output = execSync( + `node getJob.js ${projectId} ${location} ${this.staticOverlayJobId}`, + {cwd} + ); + const jobName = `/locations/${location}/jobs/${this.staticOverlayJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should check that the job succeeded', async function () { + this.retries(5); + await delay(this.test, 30000); + + let getAttempts = 0; + while (getAttempts < 5) { + const ms = Math.pow(2, getAttempts + 1) * 10000 + Math.random() * 1000; + await wait(ms); + const output = execSync( + `node getJobState.js ${projectId} ${location} ${this.staticOverlayJobId}`, + {cwd} + ); + if (output.includes('Job state: SUCCEEDED')) { + assert.ok(true); + return; + } + getAttempts++; + } + assert.ok(false); + }); +}); + +describe('Job with animated overlay functions', () => { + before(function () { + const output = execSync( + `node createJobWithAnimatedOverlay.js ${projectId} ${location} ${inputUri} ${overlayUri} ${outputUriForAnimatedOverlay}`, + {cwd} + ); + assert.ok(output.includes(`/locations/${location}/jobs/`)); + this.animatedOverlayJobId = output.toString().split('/').pop(); + }); + + after(function () { + const output = execSync( + `node deleteJob.js ${projectId} ${location} ${this.animatedOverlayJobId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job')); + }); + + it('should get a job', function () { + const output = execSync( + `node getJob.js ${projectId} ${location} ${this.animatedOverlayJobId}`, + {cwd} + ); + const jobName = `/locations/${location}/jobs/${this.animatedOverlayJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should check that the job succeeded', async function () { + this.retries(5); + await delay(this.test, 30000); + + let getAttempts = 0; + while (getAttempts < 5) { + const ms = Math.pow(2, getAttempts + 1) * 10000 + Math.random() * 1000; + await wait(ms); + const output = execSync( + `node getJobState.js ${projectId} ${location} ${this.animatedOverlayJobId}`, + {cwd} + ); + if (output.includes('Job state: SUCCEEDED')) { + assert.ok(true); + return; + } + getAttempts++; + } + assert.ok(false); + }); +}); + +describe('Job with set number of images spritesheet', () => { + before(function () { + const output = execSync( + `node createJobWithSetNumberImagesSpritesheet.js ${projectId} ${location} ${inputUri} ${outputUriForSetNumberImagesSpritesheet}`, + {cwd} + ); + assert.ok(output.includes(`/locations/${location}/jobs/`)); + this.setNumberSpritesheetJobId = output.toString().split('/').pop(); + }); + + after(function () { + const output = execSync( + `node deleteJob.js ${projectId} ${location} ${this.setNumberSpritesheetJobId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job')); + }); + + it('should get a job', function () { + const output = execSync( + `node getJob.js ${projectId} ${location} ${this.setNumberSpritesheetJobId}`, + {cwd} + ); + const jobName = `/locations/${location}/jobs/${this.setNumberSpritesheetJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should check that the job succeeded', async function () { + this.retries(5); + await delay(this.test, 30000); + + let getAttempts = 0; + while (getAttempts < 5) { + const ms = Math.pow(2, getAttempts + 1) * 10000 + Math.random() * 1000; + await wait(ms); + const output = execSync( + `node getJobState.js ${projectId} ${location} ${this.setNumberSpritesheetJobId}`, + {cwd} + ); + if (output.includes('Job state: SUCCEEDED')) { + assert.ok(true); + return; + } + getAttempts++; + } + assert.ok(false); + }); + + it('should check that the spritesheet files exist in the bucket', async () => { + assert.equal( + await checkFileExists( + bucketName, + `${outputDirForSetNumberImagesSpritesheet}${smallSpriteSheetFileName}` + ), + true + ); + assert.equal( + await checkFileExists( + bucketName, + `${outputDirForSetNumberImagesSpritesheet}${largeSpriteSheetFileName}` + ), + true + ); + }); +}); + +describe('Job with periodic images spritesheet', () => { + before(function () { + const output = execSync( + `node createJobWithPeriodicImagesSpritesheet.js ${projectId} ${location} ${inputUri} ${outputUriForPeriodicImagesSpritesheet}`, + {cwd} + ); + assert.ok(output.includes(`/locations/${location}/jobs/`)); + this.periodicSpritesheetJobId = output.toString().split('/').pop(); + }); + + after(function () { + const output = execSync( + `node deleteJob.js ${projectId} ${location} ${this.periodicSpritesheetJobId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job')); + }); + + it('should get a job', function () { + const output = execSync( + `node getJob.js ${projectId} ${location} ${this.periodicSpritesheetJobId}`, + {cwd} + ); + const jobName = `/locations/${location}/jobs/${this.periodicSpritesheetJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should check that the job succeeded', async function () { + this.retries(5); + await delay(this.test, 30000); + + let getAttempts = 0; + while (getAttempts < 5) { + const ms = Math.pow(2, getAttempts + 1) * 10000 + Math.random() * 1000; + await wait(ms); + const output = execSync( + `node getJobState.js ${projectId} ${location} ${this.periodicSpritesheetJobId}`, + {cwd} + ); + if (output.includes('Job state: SUCCEEDED')) { + assert.ok(true); + return; + } + getAttempts++; + } + assert.ok(false); + }); + + it('should check that the spritesheet files exist in the bucket', async () => { + assert.equal( + await checkFileExists( + bucketName, + `${outputDirForPeriodicImagesSpritesheet}${smallSpriteSheetFileName}` + ), + true + ); + assert.equal( + await checkFileExists( + bucketName, + `${outputDirForPeriodicImagesSpritesheet}${largeSpriteSheetFileName}` + ), + true + ); + }); +}); + +describe('Job with concatenated inputs functions', () => { + before(function () { + const output = execSync( + `node createJobWithConcatenatedInputs.js ${projectId} ${location} ${concat1Uri} 0 8.1 ${concat2Uri} 3.5 15 ${outputUriForConcatenated}`, + {cwd} + ); + assert.ok(output.includes(`/locations/${location}/jobs/`)); + this.concatenatedJobId = output.toString().split('/').pop(); + }); + + after(function () { + const output = execSync( + `node deleteJob.js ${projectId} ${location} ${this.concatenatedJobId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job')); + }); + + it('should get a job', function () { + const output = execSync( + `node getJob.js ${projectId} ${location} ${this.concatenatedJobId}`, + {cwd} + ); + const jobName = `/locations/${location}/jobs/${this.concatenatedJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should check that the job succeeded', async function () { + this.retries(5); + await delay(this.test, 30000); + + let getAttempts = 0; + while (getAttempts < 5) { + const ms = Math.pow(2, getAttempts + 1) * 10000 + Math.random() * 1000; + await wait(ms); + const output = execSync( + `node getJobState.js ${projectId} ${location} ${this.concatenatedJobId}`, + {cwd} + ); + if (output.includes('Job state: SUCCEEDED')) { + assert.ok(true); + return; + } + getAttempts++; + } + assert.ok(false); + }); +}); + +describe('Job with embedded captions', () => { + before(function () { + const output = execSync( + `node createJobWithEmbeddedCaptions.js ${projectId} ${location} ${inputUri} ${captionsUri} ${outputUriForEmbeddedCaptions}`, + {cwd} + ); + assert.ok(output.includes(`/locations/${location}/jobs/`)); + this.embeddedCaptionsJobId = output.toString().split('/').pop(); + }); + + after(function () { + const output = execSync( + `node deleteJob.js ${projectId} ${location} ${this.embeddedCaptionsJobId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job')); + }); + + it('should get a job', function () { + const output = execSync( + `node getJob.js ${projectId} ${location} ${this.embeddedCaptionsJobId}`, + {cwd} + ); + const jobName = `/locations/${location}/jobs/${this.embeddedCaptionsJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should check that the job succeeded', async function () { + this.retries(5); + await delay(this.test, 30000); + + let getAttempts = 0; + while (getAttempts < 5) { + const ms = Math.pow(2, getAttempts + 1) * 10000 + Math.random() * 1000; + await wait(ms); + const output = execSync( + `node getJobState.js ${projectId} ${location} ${this.embeddedCaptionsJobId}`, + {cwd} + ); + if (output.includes('Job state: SUCCEEDED')) { + assert.ok(true); + return; + } + getAttempts++; + } + assert.ok(false); + }); +}); + +describe('Job with standalone captions', () => { + before(function () { + const output = execSync( + `node createJobWithStandaloneCaptions.js ${projectId} ${location} ${inputUri} ${captionsUri} ${outputUriForStandaloneCaptions}`, + {cwd} + ); + assert.ok(output.includes(`/locations/${location}/jobs/`)); + this.standaloneCaptionsJobId = output.toString().split('/').pop(); + }); + + after(function () { + const output = execSync( + `node deleteJob.js ${projectId} ${location} ${this.standaloneCaptionsJobId}`, + {cwd} + ); + assert.ok(output.includes('Deleted job')); + }); + + it('should get a job', function () { + const output = execSync( + `node getJob.js ${projectId} ${location} ${this.standaloneCaptionsJobId}`, + {cwd} + ); + const jobName = `/locations/${location}/jobs/${this.standaloneCaptionsJobId}`; + assert.ok(output.includes(jobName)); + }); + + it('should check that the job succeeded', async function () { + this.retries(5); + await delay(this.test, 30000); + + let getAttempts = 0; + while (getAttempts < 5) { + const ms = Math.pow(2, getAttempts + 1) * 10000 + Math.random() * 1000; + await wait(ms); + const output = execSync( + `node getJobState.js ${projectId} ${location} ${this.standaloneCaptionsJobId}`, + {cwd} + ); + if (output.includes('Job state: SUCCEEDED')) { + assert.ok(true); + return; + } + getAttempts++; + } + assert.ok(false); + }); +}); diff --git a/media/transcoder/testdata/ChromeCast.mp4 b/media/transcoder/testdata/ChromeCast.mp4 new file mode 100644 index 0000000000..8a06ad7d8c Binary files /dev/null and b/media/transcoder/testdata/ChromeCast.mp4 differ diff --git a/media/transcoder/testdata/ForBiggerEscapes.mp4 b/media/transcoder/testdata/ForBiggerEscapes.mp4 new file mode 100644 index 0000000000..3ae36b91c8 Binary files /dev/null and b/media/transcoder/testdata/ForBiggerEscapes.mp4 differ diff --git a/media/transcoder/testdata/ForBiggerJoyrides.mp4 b/media/transcoder/testdata/ForBiggerJoyrides.mp4 new file mode 100644 index 0000000000..33f1dfe1a2 Binary files /dev/null and b/media/transcoder/testdata/ForBiggerJoyrides.mp4 differ diff --git a/media/transcoder/testdata/caption.srt b/media/transcoder/testdata/caption.srt new file mode 100644 index 0000000000..fcd2b64a09 --- /dev/null +++ b/media/transcoder/testdata/caption.srt @@ -0,0 +1,32 @@ +1 +00:00:00,000 --> 00:00:06,500 +[MUSIC PLAYING] + +2 +00:00:06,500 --> 00:00:08,500 +LITTLE GIRL: Tada. + +3 +00:00:09,200 --> 00:00:10,500 +FATHER: Woah! + +4 +00:00:11,500 --> 00:00:13,000 +MOVIE FAN: Showtime. + +5 +00:00:14,000 --> 00:00:17,000 +MOVIE FAN: Arghhh - did you see this, +did you see this. + +6 +00:00:19,000 --> 00:00:20,000 +ALL: Ohh! + +7 +00:00:20,400 --> 00:00:22,500 +BUSTER: I'm a MONSTER! + +8 +00:00:24,000 --> 00:00:28,000 +[MUSIC CONTINUES] \ No newline at end of file diff --git a/media/transcoder/testdata/overlay.jpg b/media/transcoder/testdata/overlay.jpg new file mode 100644 index 0000000000..ded44b4df9 Binary files /dev/null and b/media/transcoder/testdata/overlay.jpg differ