From 2b37aa6e6a95d79b18c225f3a1698c233ab2ed53 Mon Sep 17 00:00:00 2001 From: Nick Cook Date: Tue, 15 Oct 2024 16:32:07 -0700 Subject: [PATCH 1/3] feat: add Live Stream API channel clip samples and tests --- media/livestream/createChannel.js | 7 ++ media/livestream/createChannelClip.js | 91 +++++++++++++++++ media/livestream/createInput.js | 1 + media/livestream/deleteChannelClip.js | 61 ++++++++++++ media/livestream/getChannelClip.js | 58 +++++++++++ media/livestream/listChannelClips.js | 57 +++++++++++ media/livestream/package.json | 3 +- media/livestream/test/livestream.test.js | 120 +++++++++++++++++++++++ 8 files changed, 397 insertions(+), 1 deletion(-) create mode 100644 media/livestream/createChannelClip.js create mode 100644 media/livestream/deleteChannelClip.js create mode 100644 media/livestream/getChannelClip.js create mode 100644 media/livestream/listChannelClips.js diff --git a/media/livestream/createChannel.js b/media/livestream/createChannel.js index b180f20d1a..e9dca374c5 100644 --- a/media/livestream/createChannel.js +++ b/media/livestream/createChannel.js @@ -93,10 +93,17 @@ function main(projectId, location, channelId, inputId, outputUri) { { fileName: 'manifest.m3u8', type: 'HLS', + key: 'manifest_hls', muxStreams: ['mux_video', 'mux_audio'], maxSegmentCount: 5, }, ], + // Optional, but required for VOD clips + retentionConfig: { + retentionWindowDuration: { + seconds: 86400, + }, + }, }, }; diff --git a/media/livestream/createChannelClip.js b/media/livestream/createChannelClip.js new file mode 100644 index 0000000000..91c39fbc7b --- /dev/null +++ b/media/livestream/createChannelClip.js @@ -0,0 +1,91 @@ +/** + * Copyright 2024 Google LLC + * + * 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 + * + * https://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, channelId, clipId, outputUri) { + // [START livestream_create_channel_clip] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // channelId = 'my-channel'; + // clipId = 'my-channel-clip'; + // outputUri = 'gs://my-bucket/my-output-folder'; + + // Imports the Livestream library + const {LivestreamServiceClient} = require('@google-cloud/livestream').v1; + + // Instantiates a client + const livestreamServiceClient = new LivestreamServiceClient(); + + async function createChannelClip() { + // Create a 20 second clip starting 40 seconds ago + const recent = new Date(); + recent.setSeconds(recent.getSeconds() - 20); + const earlier = new Date(); + earlier.setSeconds(earlier.getSeconds() - 40); + + // Construct request + const request = { + parent: livestreamServiceClient.channelPath( + projectId, + location, + channelId + ), + clipId: clipId, + clip: { + outputUri: outputUri, + slices: [ + { + timeSlice: { + markinTime: { + seconds: Math.floor(earlier.getTime() / 1000), + nanos: earlier.getMilliseconds() * 1000000, + }, + markoutTime: { + seconds: Math.floor(recent.getTime() / 1000), + nanos: recent.getMilliseconds() * 1000000, + }, + }, + }, + ], + clipManifests: [ + { + manifestKey: 'manifest_hls', + }, + ], + }, + }; + + // Run request + const [operation] = await livestreamServiceClient.createClip(request); + const response = await operation.promise(); + const [clip] = response; + console.log(`Channel clip: ${clip.name}`); + } + + createChannelClip(); + // [END livestream_create_channel_clip] +} + +// node createChannelClip.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/livestream/createInput.js b/media/livestream/createInput.js index 34a28a85f7..007943c0a3 100644 --- a/media/livestream/createInput.js +++ b/media/livestream/createInput.js @@ -45,6 +45,7 @@ function main(projectId, location, inputId) { const response = await operation.promise(); const [input] = response; console.log(`Input: ${input.name}`); + console.log(`Uri: ${input.uri}`); } createInput(); diff --git a/media/livestream/deleteChannelClip.js b/media/livestream/deleteChannelClip.js new file mode 100644 index 0000000000..185fdb0ff4 --- /dev/null +++ b/media/livestream/deleteChannelClip.js @@ -0,0 +1,61 @@ +/** + * Copyright 2024 Google LLC + * + * 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 + * + * https://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, channelId, clipId) { + // [START livestream_delete_channel_clip] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // channelId = 'my-channel'; + // clipId = 'my-channel-clip'; + + // Imports the Livestream library + const {LivestreamServiceClient} = require('@google-cloud/livestream').v1; + + // Instantiates a client + const livestreamServiceClient = new LivestreamServiceClient(); + + async function deleteChannelClip() { + // Construct request + const request = { + name: livestreamServiceClient.clipPath( + projectId, + location, + channelId, + clipId + ), + }; + + // Run request + const [operation] = await livestreamServiceClient.deleteClip(request); + await operation.promise(); + console.log('Deleted channel clip'); + } + + deleteChannelClip(); + // [END livestream_delete_channel_clip] +} + +// node deleteChannelClip.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/livestream/getChannelClip.js b/media/livestream/getChannelClip.js new file mode 100644 index 0000000000..645cfd4ee5 --- /dev/null +++ b/media/livestream/getChannelClip.js @@ -0,0 +1,58 @@ +/** + * Copyright 2024 Google LLC + * + * 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 + * + * https://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, channelId, clipId) { + // [START livestream_get_channel_clip] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // channelId = 'my-channel'; + // clipId = 'my-channel-clip'; + + // Imports the Livestream library + const {LivestreamServiceClient} = require('@google-cloud/livestream').v1; + + // Instantiates a client + const livestreamServiceClient = new LivestreamServiceClient(); + + async function getChannelClip() { + // Construct request + const request = { + name: livestreamServiceClient.clipPath( + projectId, + location, + channelId, + clipId + ), + }; + const [clip] = await livestreamServiceClient.getClip(request); + console.log(`Channel clip: ${clip.name}`); + } + + getChannelClip(); + // [END livestream_get_channel_clip] +} + +// node getChannelClip.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/livestream/listChannelClips.js b/media/livestream/listChannelClips.js new file mode 100644 index 0000000000..1997392b82 --- /dev/null +++ b/media/livestream/listChannelClips.js @@ -0,0 +1,57 @@ +/** + * Copyright 2024 Google LLC + * + * 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 + * + * https://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, channelId) { + // [START livestream_list_channel_clips] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // projectId = 'my-project-id'; + // location = 'us-central1'; + // channelId = 'my-channel'; + + // Imports the Livestream library + const {LivestreamServiceClient} = require('@google-cloud/livestream').v1; + + // Instantiates a client + const livestreamServiceClient = new LivestreamServiceClient(); + + async function listChannelClips() { + const iterable = await livestreamServiceClient.listClipsAsync({ + parent: livestreamServiceClient.channelPath( + projectId, + location, + channelId + ), + }); + console.info('Channel clips:'); + for await (const response of iterable) { + console.log(response.name); + } + } + + listChannelClips(); + // [END livestream_list_channel_clips] +} + +// node listChannelClips.js +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/media/livestream/package.json b/media/livestream/package.json index f905e3ed86..d33a83b5fc 100644 --- a/media/livestream/package.json +++ b/media/livestream/package.json @@ -13,9 +13,10 @@ "test": "c8 mocha -p -j 2 --timeout 600000 test/*.js" }, "dependencies": { - "@google-cloud/livestream": "^1.0.0" + "@google-cloud/livestream": "^1.4.0" }, "devDependencies": { + "@google-cloud/storage": "^7.0.0", "c8": "^10.0.0", "chai": "^4.5.0", "mocha": "^10.1.0", diff --git a/media/livestream/test/livestream.test.js b/media/livestream/test/livestream.test.js index f574c351be..6f42281cba 100644 --- a/media/livestream/test/livestream.test.js +++ b/media/livestream/test/livestream.test.js @@ -20,6 +20,8 @@ const assert = require('assert'); const {v4: uuidv4} = require('uuid'); const {execSync} = require('child_process'); const {describe, it, before, after} = require('mocha'); +const {Storage} = require('@google-cloud/storage'); +const storage = new Storage(); const uniqueID = uuidv4().split('-')[0]; const bucketName = `nodejs-samples-livestream-test-${uniqueID}`; @@ -32,16 +34,21 @@ const backupInputId = `nodejs-test-livestream-backup-input-${uniqueID}`; const backupInputName = `projects/${projectId}/locations/${location}/inputs/${backupInputId}`; const channelId = `nodejs-test-livestream-channel-${uniqueID}`; const channelName = `projects/${projectId}/locations/${location}/channels/${channelId}`; +const clipId = `nodejs-test-livestream-clip-${uniqueID}`; +const clipName = `projects/${projectId}/locations/${location}/channels/${channelId}/clips/${clipId}`; const eventId = `nodejs-test-livestream-event-${uniqueID}`; const eventName = `projects/${projectId}/locations/${location}/channels/${channelId}/events/${eventId}`; const assetId = `nodejs-test-livestream-asset-${uniqueID}`; const assetName = `projects/${projectId}/locations/${location}/assets/${assetId}`; const assetUri = 'gs://cloud-samples-data/media/ForBiggerEscapes.mp4'; const outputUri = `gs://${bucketName}/test-output-channel/`; +const clipOutputUri = `${outputUri}clips`; const poolId = 'default'; const poolName = `projects/${projectId}/locations/${location}/pools/${poolId}`; const cwd = path.join(__dirname, '..'); +let rtmpUri = ''; + before(async () => { // Delete outstanding channels, inputs, and assets created more than 3 hours ago const {LivestreamServiceClient} = require('@google-cloud/livestream').v1; @@ -77,6 +84,17 @@ before(async () => { name: event.name, }); } + + const [clips] = await livestreamServiceClient.listClips({ + parent: channel.name, + }); + + for (const clip of clips) { + await livestreamServiceClient.deleteClip({ + name: clip.name, + }); + } + await livestreamServiceClient.deleteChannel(request); } } @@ -103,6 +121,23 @@ before(async () => { await livestreamServiceClient.deleteAsset(request); } } + // Create bucket for test files + await storage.createBucket(bucketName); +}); + +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'); + } }); describe('Input functions', () => { @@ -321,6 +356,91 @@ describe('Channel event functions', () => { }); }); +describe('Channel clip functions', () => { + before(() => { + let output = execSync( + `node createInput.js ${projectId} ${location} ${inputId}`, + {cwd} + ); + assert.ok(output.includes(inputName)); + assert.ok(output.includes('Uri:')); + + const match = new RegExp('rtmp.*', 'g').exec(output); + rtmpUri = match[0].trim(); + + output = execSync( + `node createChannel.js ${projectId} ${location} ${channelId} ${inputId} ${outputUri}`, + {cwd} + ); + assert.ok(output.includes(channelName)); + + output = execSync( + `node startChannel.js ${projectId} ${location} ${channelId}`, + {cwd} + ); + assert.ok(output.includes('Started channel')); + }); + + after(() => { + let output = execSync( + `node stopChannel.js ${projectId} ${location} ${channelId}`, + {cwd} + ); + assert.ok(output.includes('Stopped channel')); + + output = execSync( + `node deleteChannel.js ${projectId} ${location} ${channelId}`, + {cwd} + ); + assert.ok(output.includes('Deleted channel')); + + output = execSync( + `node deleteInput.js ${projectId} ${location} ${inputId}`, + {cwd} + ); + assert.ok(output.includes('Deleted input')); + }); + + it('should create a channel clip', async () => { + // Run the test stream for 45 seconds + await execSync( + `ffmpeg -re -f lavfi -t 45 -i "testsrc=size=1280x720 [out0]; sine=frequency=500 [out1]" \ + -acodec aac -vcodec h264 -f flv ${rtmpUri}`, + {cwd} + ); + + const output = execSync( + `node createChannelClip.js ${projectId} ${location} ${channelId} ${clipId} ${clipOutputUri}`, + {cwd} + ); + assert.ok(output.includes(clipName)); + }); + + it('should show a list of channel clips', () => { + const output = execSync( + `node listChannelClips.js ${projectId} ${location} ${channelId}`, + {cwd} + ); + assert.ok(output.includes(clipName)); + }); + + it('should get a channel clip', () => { + const output = execSync( + `node getChannelClip.js ${projectId} ${location} ${channelId} ${clipId}`, + {cwd} + ); + assert.ok(output.includes(clipName)); + }); + + it('should delete a channel clip', () => { + const output = execSync( + `node deleteChannelClip.js ${projectId} ${location} ${channelId} ${clipId}`, + {cwd} + ); + assert.ok(output.includes('Deleted channel clip')); + }); +}); + describe('Asset functions', () => { it('should create an asset', () => { const output = execSync( From d52c869f5d06f32bb42d173d518b7a83b44b355b Mon Sep 17 00:00:00 2001 From: Nick Cook Date: Tue, 15 Oct 2024 16:37:49 -0700 Subject: [PATCH 2/3] license check fix --- media/livestream/createChannel.js | 5 +++-- media/livestream/createInput.js | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/media/livestream/createChannel.js b/media/livestream/createChannel.js index e9dca374c5..01cb059d92 100644 --- a/media/livestream/createChannel.js +++ b/media/livestream/createChannel.js @@ -1,10 +1,11 @@ /** - * Copyright 2022, Google, Inc. + * Copyright 2024 Google LLC + * * 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 + * https://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, diff --git a/media/livestream/createInput.js b/media/livestream/createInput.js index 007943c0a3..621db3c435 100644 --- a/media/livestream/createInput.js +++ b/media/livestream/createInput.js @@ -1,10 +1,11 @@ /** - * Copyright 2022, Google, Inc. + * Copyright 2024 Google LLC + * * 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 + * https://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, From 60f96031d4061c4caf0efa77bfb6c580e1ecbaba Mon Sep 17 00:00:00 2001 From: Nick Cook Date: Tue, 15 Oct 2024 20:22:20 -0700 Subject: [PATCH 3/3] revert copyright year changes --- media/livestream/createChannel.js | 2 +- media/livestream/createInput.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/media/livestream/createChannel.js b/media/livestream/createChannel.js index 01cb059d92..0be5a9a576 100644 --- a/media/livestream/createChannel.js +++ b/media/livestream/createChannel.js @@ -1,5 +1,5 @@ /** - * Copyright 2024 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/media/livestream/createInput.js b/media/livestream/createInput.js index 621db3c435..5972cdc22f 100644 --- a/media/livestream/createInput.js +++ b/media/livestream/createInput.js @@ -1,5 +1,5 @@ /** - * Copyright 2024 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.