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/test/transcoder.test.js b/media/transcoder/test/transcoder.test.js index cdef26549b..4292a30a7d 100644 --- a/media/transcoder/test/transcoder.test.js +++ b/media/transcoder/test/transcoder.test.js @@ -35,9 +35,13 @@ const templateName = `projects/${projectNumber}/locations/${location}/jobTemplat const testFileName = 'ChromeCast.mp4'; const testOverlayFileName = 'overlay.jpg'; +const testConcat1FileName = 'ForBiggerEscapes.mp4'; +const testConcat2FileName = 'ForBiggerJoyrides.mp4'; const inputUri = `gs://${bucketName}/${testFileName}`; const overlayUri = `gs://${bucketName}/${testOverlayFileName}`; +const concat1Uri = `gs://${bucketName}/${testConcat1FileName}`; +const concat2Uri = `gs://${bucketName}/${testConcat2FileName}`; const outputUriForPreset = `gs://${bucketName}/test-output-preset/`; const outputUriForTemplate = `gs://${bucketName}/test-output-template/`; const outputUriForAdHoc = `gs://${bucketName}/test-output-adhoc/`; @@ -53,10 +57,13 @@ const outputUriForPeriodicImagesSpritesheet = `gs://${bucketName}/${outputDirFor // 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 cwd = path.join(__dirname, '..'); const videoFile = `testdata/${testFileName}`; const overlayFile = `testdata/${testOverlayFileName}`; +const concat1File = `testdata/${testConcat1FileName}`; +const concat2File = `testdata/${testConcat2FileName}`; const delay = async (test, addMs) => { const retries = test.currentRetry(); @@ -98,6 +105,8 @@ before(async () => { 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); }); after(async () => { @@ -591,3 +600,54 @@ describe('Job with periodic images spritesheet', () => { ); }); }); + +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(`projects/${projectNumber}/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 = `projects/${projectNumber}/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); + }); +}); 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