@eluvio/elv-client-js 4.0.76 → 4.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ElvClient-min.js +18 -10
- package/dist/ElvClient-node-min.js +18 -10
- package/dist/ElvFrameClient-min.js +9 -9
- package/dist/ElvPermissionsClient-min.js +10 -10
- package/dist/ElvWalletClient-min.js +18 -10
- package/dist/ElvWalletClient-node-min.js +18 -10
- package/dist/src/AuthorizationClient.js +18 -12
- package/dist/src/Crypto.js +2 -2
- package/dist/src/ElvClient.js +111 -76
- package/dist/src/EthClient.js +2 -2
- package/dist/src/FrameClient.js +4 -4
- package/dist/src/PermissionsClient.js +2 -2
- package/dist/src/Utils.js +6 -5
- package/dist/src/abr_profiles/abr_profile_live_drm.js +1621 -0
- package/dist/src/abr_profiles/abr_profile_live_to_vod.js +1599 -0
- package/dist/src/client/ABRPublishing.js +2 -2
- package/dist/src/client/AccessGroups.js +19 -20
- package/dist/src/client/ContentAccess.js +48 -28
- package/dist/src/client/ContentManagement.js +3 -3
- package/dist/src/client/Contracts.js +235 -203
- package/dist/src/client/Files.js +2 -2
- package/dist/src/client/LiveConf.js +152 -21
- package/dist/src/client/LiveStream.js +1376 -1248
- package/dist/src/client/NFT.js +2 -2
- package/dist/src/contracts/v3b/BaseAccessControlGroup.js +1704 -0
- package/dist/src/walletClient/ClientMethods.js +423 -280
- package/dist/src/walletClient/Profile.js +2 -2
- package/dist/src/walletClient/Utils.js +7 -3
- package/dist/src/walletClient/index.js +124 -44
- package/package.json +2 -1
- package/src/FrameClient.js +4 -0
- package/src/abr_profiles/abr_profile_live_drm.js +1907 -0
- package/src/abr_profiles/abr_profile_live_to_vod.js +1885 -0
- package/src/client/ContentAccess.js +21 -0
- package/src/client/LiveConf.js +133 -17
- package/src/client/LiveStream.js +907 -947
- package/testScripts/Test.js +7 -0
package/src/client/LiveStream.js
CHANGED
|
@@ -6,16 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
const {LiveConf} = require("./LiveConf");
|
|
8
8
|
const path = require("path");
|
|
9
|
-
|
|
10
9
|
const fs = require("fs");
|
|
11
|
-
|
|
12
10
|
const HttpClient = require("../HttpClient");
|
|
13
|
-
|
|
14
|
-
// const {
|
|
15
|
-
// ValidateLibrary,
|
|
16
|
-
// ValidateVersion,
|
|
17
|
-
// ValidateParameters
|
|
18
|
-
// } = require("../Validation");
|
|
11
|
+
const Fraction = require("fraction.js");
|
|
19
12
|
|
|
20
13
|
const MakeTxLessToken = async({client, libraryId, objectId, versionHash}) => {
|
|
21
14
|
const tok = await client.authClient.AuthorizationToken({libraryId, objectId,
|
|
@@ -25,18 +18,298 @@ const MakeTxLessToken = async({client, libraryId, objectId, versionHash}) => {
|
|
|
25
18
|
return tok;
|
|
26
19
|
};
|
|
27
20
|
|
|
28
|
-
|
|
21
|
+
const Sleep = (ms) => {
|
|
29
22
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
30
|
-
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set the offering for the live stream
|
|
27
|
+
*
|
|
28
|
+
* @methodGroup Live Stream
|
|
29
|
+
* @namedParams
|
|
30
|
+
* @param {Object} client - The client object
|
|
31
|
+
* @param {string} libraryId - ID of the library for the new live stream object
|
|
32
|
+
* @param {string} objectId - ID of the new live stream object
|
|
33
|
+
* @param {string=} typeAbrMaster - Content type hash
|
|
34
|
+
* @param {string=} typeLiveStream - Content type hash
|
|
35
|
+
* @param {string} streamUrl - Live source URL
|
|
36
|
+
* @param {object} abrProfile - ABR Profile for the offering
|
|
37
|
+
* @param {number} aBitRate - Audio bitrate
|
|
38
|
+
* @param {number} aChannels - Audio channels
|
|
39
|
+
* @param {number} aSampleRate - Audio sample rate
|
|
40
|
+
* @param {number} aStreamIndex - Audio stream index
|
|
41
|
+
* @param {string} aTimeBase - Audio time base as a fraction, e.g. "1/48000" (usually equal to 1/aSampleRate)
|
|
42
|
+
* @param {string} aChannelLayout - Channel layout, e.g. "stereo"
|
|
43
|
+
* @param {number} vBitRate - Video bitrate
|
|
44
|
+
* @param {number} vHeight - Video height
|
|
45
|
+
* @param {number} vStreamIndex - Video stream index
|
|
46
|
+
* @param {number} vWidth - Video width
|
|
47
|
+
* @param {string} vDisplayAspectRatio - Display aspect ratio as a fraction, e.g. "16/9"
|
|
48
|
+
* @param {string} vFrameRate - Frame rate as an integer, e.g. "30"
|
|
49
|
+
* @param {string} vTimeBase - Time base as a fraction, e.g. "1/30000"
|
|
50
|
+
*
|
|
51
|
+
* @return {Promise<string>} - Final hash of the live stream object
|
|
52
|
+
*/
|
|
53
|
+
const StreamGenerateOffering = async({
|
|
54
|
+
client,
|
|
55
|
+
libraryId,
|
|
56
|
+
objectId,
|
|
57
|
+
typeAbrMaster,
|
|
58
|
+
typeLiveStream,
|
|
59
|
+
streamUrl,
|
|
60
|
+
abrProfile,
|
|
61
|
+
aBitRate,
|
|
62
|
+
aChannels,
|
|
63
|
+
aSampleRate,
|
|
64
|
+
aStreamIndex,
|
|
65
|
+
aTimeBase,
|
|
66
|
+
aChannelLayout,
|
|
67
|
+
vBitRate,
|
|
68
|
+
vHeight,
|
|
69
|
+
vStreamIndex,
|
|
70
|
+
vWidth,
|
|
71
|
+
vDisplayAspectRatio,
|
|
72
|
+
vFrameRate,
|
|
73
|
+
vTimeBase
|
|
74
|
+
}) => {
|
|
75
|
+
// compute duration_ts
|
|
76
|
+
const DUMMY_DURATION = 1001; // should result in integer duration_ts values for both audio and video
|
|
77
|
+
const aDurationTs = Fraction(aTimeBase).inverse().mul(DUMMY_DURATION).valueOf();
|
|
78
|
+
const vDurationTs = Fraction(vTimeBase).inverse().mul(DUMMY_DURATION).valueOf();
|
|
79
|
+
|
|
80
|
+
// construct /production_master/sources/STREAM_URL/streams
|
|
81
|
+
|
|
82
|
+
const sourceAudioStream = {
|
|
83
|
+
"bit_rate": aBitRate,
|
|
84
|
+
"channel_layout": aChannelLayout,
|
|
85
|
+
"channels": aChannels,
|
|
86
|
+
"codec_name": "aac",
|
|
87
|
+
"duration": DUMMY_DURATION,
|
|
88
|
+
"duration_ts": aDurationTs,
|
|
89
|
+
"frame_count": 0,
|
|
90
|
+
"language": "",
|
|
91
|
+
"max_bit_rate": aBitRate,
|
|
92
|
+
"sample_rate": aSampleRate,
|
|
93
|
+
"start_pts": 0,
|
|
94
|
+
"start_time": 0,
|
|
95
|
+
"time_base": aTimeBase,
|
|
96
|
+
"type": "StreamAudio"
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const sourceVideoStream = {
|
|
100
|
+
"bit_rate": vBitRate,
|
|
101
|
+
"codec_name": "h264",
|
|
102
|
+
"display_aspect_ratio": vDisplayAspectRatio,
|
|
103
|
+
"duration": DUMMY_DURATION,
|
|
104
|
+
"duration_ts": vDurationTs,
|
|
105
|
+
"field_order": "progressive",
|
|
106
|
+
"frame_count": 0,
|
|
107
|
+
"frame_rate": vFrameRate,
|
|
108
|
+
"hdr": null,
|
|
109
|
+
"height": vHeight,
|
|
110
|
+
"language": "",
|
|
111
|
+
"max_bit_rate": vBitRate,
|
|
112
|
+
"sample_aspect_ratio": "1",
|
|
113
|
+
"start_pts": 0,
|
|
114
|
+
"start_time": 0,
|
|
115
|
+
"time_base": vTimeBase,
|
|
116
|
+
"type": "StreamVideo",
|
|
117
|
+
"width": vWidth
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// placeholder stream to use if [aStreamIndex, vStreamIndex].sort() is not [0,1]
|
|
121
|
+
const DUMMY_STREAM = {
|
|
122
|
+
"bit_rate": 0,
|
|
123
|
+
"codec_name": "",
|
|
124
|
+
"duration": DUMMY_DURATION,
|
|
125
|
+
"duration_ts": 2500 * DUMMY_DURATION,
|
|
126
|
+
"frame_count": 1,
|
|
127
|
+
"language": "",
|
|
128
|
+
"max_bit_rate": 0,
|
|
129
|
+
"start_pts": 0,
|
|
130
|
+
"start_time": 0,
|
|
131
|
+
"time_base": "1/2500",
|
|
132
|
+
"type": "StreamData"
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const sourceStreams = [];
|
|
136
|
+
const maxStreamIndex = Math.max(aStreamIndex, vStreamIndex);
|
|
137
|
+
|
|
138
|
+
for(let i = 0; i <= maxStreamIndex; i++) {
|
|
139
|
+
if(i === aStreamIndex) {
|
|
140
|
+
sourceStreams.push(sourceAudioStream);
|
|
141
|
+
} else if(i === vStreamIndex) {
|
|
142
|
+
sourceStreams.push(sourceVideoStream);
|
|
143
|
+
} else {
|
|
144
|
+
sourceStreams.push(DUMMY_STREAM);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// construct /production_master/sources
|
|
149
|
+
const sources = {
|
|
150
|
+
[streamUrl]: {
|
|
151
|
+
"container_format": {
|
|
152
|
+
"duration": DUMMY_DURATION,
|
|
153
|
+
"filename": streamUrl,
|
|
154
|
+
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
|
|
155
|
+
"start_time": 0
|
|
156
|
+
},
|
|
157
|
+
"streams": sourceStreams
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// construct /production_master/variants
|
|
162
|
+
const variants = {
|
|
163
|
+
"default": {
|
|
164
|
+
"streams": {
|
|
165
|
+
"audio": {
|
|
166
|
+
"default_for_media_type": false,
|
|
167
|
+
"label": "",
|
|
168
|
+
"language": "",
|
|
169
|
+
"mapping_info": "",
|
|
170
|
+
"sources": [
|
|
171
|
+
{
|
|
172
|
+
"files_api_path": streamUrl,
|
|
173
|
+
"stream_index": aStreamIndex
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
"video": {
|
|
178
|
+
"default_for_media_type": false,
|
|
179
|
+
"label": "",
|
|
180
|
+
"language": "",
|
|
181
|
+
"mapping_info": "",
|
|
182
|
+
"sources": [
|
|
183
|
+
{
|
|
184
|
+
"files_api_path": streamUrl,
|
|
185
|
+
"stream_index": vStreamIndex
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// construct /production_master
|
|
194
|
+
const production_master = {sources, variants};
|
|
195
|
+
|
|
196
|
+
// get existing metadata
|
|
197
|
+
console.log("Retrieving current metadata...");
|
|
198
|
+
let metadata = await client.ContentObjectMetadata({
|
|
199
|
+
libraryId,
|
|
200
|
+
objectId
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// add /production_master to metadata
|
|
204
|
+
metadata.production_master = production_master;
|
|
205
|
+
|
|
206
|
+
// write back to object
|
|
207
|
+
console.log("Getting write token...");
|
|
208
|
+
let editResponse = await client.EditContentObject({
|
|
209
|
+
libraryId,
|
|
210
|
+
objectId,
|
|
211
|
+
options: {
|
|
212
|
+
type: typeAbrMaster
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
let writeToken = editResponse.write_token;
|
|
216
|
+
console.log(`New write token: ${writeToken}`);
|
|
217
|
+
|
|
218
|
+
console.log("Writing back metadata with /production_master added...");
|
|
219
|
+
await client.ReplaceMetadata({
|
|
220
|
+
libraryId,
|
|
221
|
+
metadata,
|
|
222
|
+
objectId,
|
|
223
|
+
writeToken
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
console.log("Finalizing...");
|
|
227
|
+
let finalizeResponse = await client.FinalizeContentObject({
|
|
228
|
+
libraryId,
|
|
229
|
+
objectId,
|
|
230
|
+
writeToken
|
|
231
|
+
});
|
|
232
|
+
let masterVersionHash = finalizeResponse.hash;
|
|
233
|
+
console.log(`Finalized, new version hash: ${masterVersionHash}`);
|
|
234
|
+
|
|
235
|
+
// Generate offering
|
|
236
|
+
const createResponse = await client.CreateABRMezzanine({
|
|
237
|
+
libraryId,
|
|
238
|
+
objectId,
|
|
239
|
+
masterVersionHash,
|
|
240
|
+
variant: "default",
|
|
241
|
+
offeringKey: "default",
|
|
242
|
+
abrProfile
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if(createResponse.warnings.length > 0) {
|
|
246
|
+
console.log("WARNINGS:");
|
|
247
|
+
console.log(JSON.stringify(createResponse.warnings, null, 2));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if(createResponse.errors.length > 0) {
|
|
251
|
+
console.log("ERRORS:");
|
|
252
|
+
console.log(JSON.stringify(createResponse.errors, null, 2));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let versionHash = createResponse.hash;
|
|
256
|
+
console.log(`New version hash: ${versionHash}`);
|
|
257
|
+
|
|
258
|
+
// get new metadata
|
|
259
|
+
console.log("Retrieving revised metadata with offering...");
|
|
260
|
+
metadata = await client.ContentObjectMetadata({
|
|
261
|
+
libraryId,
|
|
262
|
+
versionHash
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
console.log("Moving /abr_mezzanine/offerings to /offerings and removing /abr_mezzanine...");
|
|
266
|
+
metadata.offerings = metadata.abr_mezzanine.offerings;
|
|
267
|
+
delete metadata.abr_mezzanine;
|
|
268
|
+
|
|
269
|
+
// add items to media_struct needed to use options.json handler
|
|
270
|
+
metadata.offerings.default.media_struct.duration_rat = `${DUMMY_DURATION}`;
|
|
271
|
+
|
|
272
|
+
// write back to object
|
|
273
|
+
console.log("Getting write token...");
|
|
274
|
+
editResponse = await client.EditContentObject({
|
|
275
|
+
libraryId,
|
|
276
|
+
objectId,
|
|
277
|
+
options: {
|
|
278
|
+
type: typeLiveStream
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
writeToken = editResponse.write_token;
|
|
282
|
+
console.log(`New write token: ${writeToken}`);
|
|
283
|
+
|
|
284
|
+
console.log("Writing back metadata with /offerings...");
|
|
285
|
+
await client.ReplaceMetadata({
|
|
286
|
+
libraryId,
|
|
287
|
+
metadata,
|
|
288
|
+
objectId,
|
|
289
|
+
writeToken
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
console.log("Finalizing...");
|
|
293
|
+
finalizeResponse = await client.FinalizeContentObject({
|
|
294
|
+
libraryId,
|
|
295
|
+
objectId,
|
|
296
|
+
writeToken
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const finalHash = finalizeResponse.hash;
|
|
300
|
+
console.log(`Finalized, new version hash: ${finalHash}`);
|
|
301
|
+
|
|
302
|
+
return finalHash;
|
|
303
|
+
};
|
|
31
304
|
|
|
32
305
|
/**
|
|
33
306
|
* Retrieve the status of the current live stream session
|
|
34
307
|
*
|
|
35
308
|
* @methodGroup Live Stream
|
|
36
309
|
* @namedParams
|
|
37
|
-
* @param {string} name -
|
|
38
|
-
* @param {boolean} stopLro -
|
|
39
|
-
* @param {boolean} showParams -
|
|
310
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
311
|
+
* @param {boolean=} stopLro - If specified, will stop LRO
|
|
312
|
+
* @param {boolean=} showParams - If specified, will return recording_params with status
|
|
40
313
|
* States:
|
|
41
314
|
* unconfigured - no live_recording_config
|
|
42
315
|
* uninitialized - no live_recording config generated
|
|
@@ -46,15 +319,13 @@ function sleep(ms) {
|
|
|
46
319
|
* running - stream is running and producing output
|
|
47
320
|
* stalled - LRO running but no source data (so not producing output)
|
|
48
321
|
*
|
|
49
|
-
* @return {Object} - The status response for the object, as well as logs, warnings and errors from the master initialization
|
|
322
|
+
* @return {Promise<Object>} - The status response for the object, as well as logs, warnings and errors from the master initialization
|
|
50
323
|
*/
|
|
51
324
|
exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
|
|
52
325
|
let conf = await this.LoadConf({name});
|
|
53
|
-
|
|
54
326
|
let status = {name: name};
|
|
55
327
|
|
|
56
328
|
try {
|
|
57
|
-
|
|
58
329
|
let libraryId = await this.ContentObjectLibraryId({objectId: conf.objectId});
|
|
59
330
|
status.library_id = libraryId;
|
|
60
331
|
status.object_id = conf.objectId;
|
|
@@ -68,6 +339,8 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
|
|
|
68
339
|
]
|
|
69
340
|
});
|
|
70
341
|
|
|
342
|
+
status.reference_url = mainMeta.live_recording_config.reference_url;
|
|
343
|
+
|
|
71
344
|
if(mainMeta.live_recording_config == undefined || mainMeta.live_recording_config.url == undefined) {
|
|
72
345
|
status.state = "unconfigured";
|
|
73
346
|
return status;
|
|
@@ -96,7 +369,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
|
|
|
96
369
|
status.url = mainMeta.live_recording.recording_config.recording_params.origin_url;
|
|
97
370
|
|
|
98
371
|
let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
|
|
99
|
-
if(edgeWriteToken
|
|
372
|
+
if(!edgeWriteToken) {
|
|
100
373
|
status.state = "inactive";
|
|
101
374
|
return status;
|
|
102
375
|
}
|
|
@@ -114,6 +387,8 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
|
|
|
114
387
|
]
|
|
115
388
|
});
|
|
116
389
|
|
|
390
|
+
status.edge_meta_size = JSON.stringify(edgeMeta).length;
|
|
391
|
+
|
|
117
392
|
// If a stream has never been started return state 'inactive'
|
|
118
393
|
if(edgeMeta.live_recording === undefined ||
|
|
119
394
|
edgeMeta.live_recording.recordings === undefined ||
|
|
@@ -168,7 +443,7 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
|
|
|
168
443
|
}
|
|
169
444
|
|
|
170
445
|
if(showParams) {
|
|
171
|
-
status.
|
|
446
|
+
status.recording_params = edgeMeta.live_recording.recording_config.recording_params;
|
|
172
447
|
}
|
|
173
448
|
|
|
174
449
|
let state = "stopped";
|
|
@@ -207,10 +482,12 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
|
|
|
207
482
|
await this.utils.ResponseToJson(
|
|
208
483
|
await HttpClient.Fetch(lroStopUrl)
|
|
209
484
|
);
|
|
485
|
+
|
|
210
486
|
console.log("LRO Stop: ", lroStatus.body);
|
|
211
487
|
} catch(error) {
|
|
212
488
|
console.log("LRO Stop (failed): ", error.response.statusCode);
|
|
213
489
|
}
|
|
490
|
+
|
|
214
491
|
state = "stopped";
|
|
215
492
|
status.state = state;
|
|
216
493
|
}
|
|
@@ -290,36 +567,18 @@ exports.StreamStatus = async function({name, stopLro=false, showParams=false}) {
|
|
|
290
567
|
return status;
|
|
291
568
|
};
|
|
292
569
|
|
|
293
|
-
// async StatusPrep({name}) {
|
|
294
|
-
//
|
|
295
|
-
// let conf = await this.LoadConf({name});
|
|
296
|
-
//
|
|
297
|
-
// try {
|
|
298
|
-
//
|
|
299
|
-
// // Set static token - avoid individual auth for separate channels/streams
|
|
300
|
-
// let token = await MakeTxLessToken({client: this.client, libraryId: conf.libraryId});
|
|
301
|
-
// this.client.SetStaticToken({token});
|
|
302
|
-
//
|
|
303
|
-
// } catch(error) {
|
|
304
|
-
// console.log("StatusPrep failed: ", error);
|
|
305
|
-
// return null;
|
|
306
|
-
// }
|
|
307
|
-
//
|
|
308
|
-
// }
|
|
309
|
-
|
|
310
570
|
/**
|
|
311
571
|
* Create a new edge write token
|
|
312
572
|
*
|
|
313
573
|
* @methodGroup Live Stream
|
|
314
574
|
* @namedParams
|
|
315
|
-
* @param {string} name -
|
|
316
|
-
* @param {boolean} start -
|
|
575
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
576
|
+
* @param {boolean=} start - If specified, will start the stream after creation
|
|
317
577
|
*
|
|
318
|
-
* @return {Object} - The status response for the object
|
|
578
|
+
* @return {Promise<Object>} - The status response for the object
|
|
319
579
|
*
|
|
320
580
|
*/
|
|
321
|
-
exports.StreamCreate = async function({name, start
|
|
322
|
-
|
|
581
|
+
exports.StreamCreate = async function({name, start=false}) {
|
|
323
582
|
let status = await this.StreamStatus({name});
|
|
324
583
|
if(status.state !== "inactive" && status.state !== "terminated" && status.state !== "stopped") {
|
|
325
584
|
return {
|
|
@@ -392,6 +651,7 @@ exports.StreamCreate = async function({name, start = false}) {
|
|
|
392
651
|
writeToken: writeToken,
|
|
393
652
|
commitMessage: "Create stream edge write token " + edgeToken
|
|
394
653
|
});
|
|
654
|
+
|
|
395
655
|
const objectHash = response.hash;
|
|
396
656
|
this.Log("Finalized object: ", objectHash);
|
|
397
657
|
|
|
@@ -417,20 +677,19 @@ exports.StreamCreate = async function({name, start = false}) {
|
|
|
417
677
|
*
|
|
418
678
|
* @methodGroup Live Stream
|
|
419
679
|
* @namedParams
|
|
420
|
-
* @param {string} name -
|
|
421
|
-
* @param {string
|
|
680
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
681
|
+
* @param {string} op - The operation to perform. Possible values:
|
|
422
682
|
* 'start'
|
|
423
683
|
* 'reset' - Stops current LRO recording and starts a new one. Does
|
|
424
684
|
* not create a new edge write token (just creates a new recording
|
|
425
685
|
* period in the existing edge write token)
|
|
426
686
|
* - 'stop'
|
|
427
687
|
*
|
|
428
|
-
* @return {Object} - The status response for the stream
|
|
688
|
+
* @return {Promise<Object>} - The status response for the stream
|
|
429
689
|
*
|
|
430
690
|
*/
|
|
431
691
|
exports.StreamStartOrStopOrReset = async function({name, op}) {
|
|
432
692
|
try {
|
|
433
|
-
console.log("Stream ", op, ": ", name);
|
|
434
693
|
let status = await this.StreamStatus({name});
|
|
435
694
|
if(status.state != "stopped") {
|
|
436
695
|
if(op === "start") {
|
|
@@ -440,7 +699,6 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
|
|
|
440
699
|
}
|
|
441
700
|
|
|
442
701
|
if(status.state == "running" || status.state == "starting" || status.state == "stalled") {
|
|
443
|
-
console.log("STOPPING");
|
|
444
702
|
try {
|
|
445
703
|
await this.CallBitcodeMethod({
|
|
446
704
|
libraryId: status.library_id,
|
|
@@ -458,13 +716,14 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
|
|
|
458
716
|
let tries = 10;
|
|
459
717
|
while(status.state != "stopped" && tries-- > 0) {
|
|
460
718
|
console.log("Wait to terminate - ", status.state);
|
|
461
|
-
await
|
|
719
|
+
await Sleep(1000);
|
|
462
720
|
status = await this.StreamStatus({name});
|
|
463
721
|
}
|
|
464
|
-
|
|
722
|
+
|
|
723
|
+
console.log("Status after stop - ", status.state);
|
|
465
724
|
|
|
466
725
|
if(tries <= 0) {
|
|
467
|
-
console.log("Failed to
|
|
726
|
+
console.log("Failed to stop");
|
|
468
727
|
return status;
|
|
469
728
|
}
|
|
470
729
|
}
|
|
@@ -495,7 +754,7 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
|
|
|
495
754
|
let tries = 10;
|
|
496
755
|
while(status.state != "starting" && tries-- > 0) {
|
|
497
756
|
console.log("Wait to start - ", status.state);
|
|
498
|
-
await
|
|
757
|
+
await Sleep(1000);
|
|
499
758
|
status = await this.StreamStatus({name});
|
|
500
759
|
}
|
|
501
760
|
|
|
@@ -507,416 +766,505 @@ exports.StreamStartOrStopOrReset = async function({name, op}) {
|
|
|
507
766
|
}
|
|
508
767
|
};
|
|
509
768
|
|
|
510
|
-
|
|
769
|
+
/**
|
|
511
770
|
* Stop the live stream session and close the edge write token.
|
|
512
771
|
* Not implemented fully
|
|
772
|
+
*
|
|
773
|
+
* @methodGroup Live Stream
|
|
774
|
+
* @namedParams
|
|
775
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
776
|
+
*
|
|
777
|
+
* @return {Promise<Object>} - The finalize response for the stream object
|
|
513
778
|
*/
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
//
|
|
520
|
-
// let conf = await this.LoadConf({name});
|
|
521
|
-
//
|
|
522
|
-
// let objectId = conf.objectId;
|
|
523
|
-
// let libraryId = await this.client.ContentObjectLibraryId({objectId: objectId});
|
|
524
|
-
//
|
|
525
|
-
// let mainMeta = await this.client.ContentObjectMetadata({
|
|
526
|
-
// libraryId: libraryId,
|
|
527
|
-
// objectId: objectId
|
|
528
|
-
// });
|
|
529
|
-
//
|
|
530
|
-
// let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
|
|
531
|
-
// // Support both hostname and URL ingress_node_api
|
|
532
|
-
// if(!fabURI.startsWith("http")) {
|
|
533
|
-
// // Assume https
|
|
534
|
-
// fabURI = "https://" + fabURI;
|
|
535
|
-
// }
|
|
536
|
-
//
|
|
537
|
-
// this.client.SetNodes({fabricURIs: [fabURI]});
|
|
538
|
-
//
|
|
539
|
-
// let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
|
|
540
|
-
//
|
|
541
|
-
// if(edgeWriteToken === undefined || edgeWriteToken === "") {
|
|
542
|
-
// return {
|
|
543
|
-
// state: "inactive",
|
|
544
|
-
// error: "no active streams - must create a stream first"
|
|
545
|
-
// };
|
|
546
|
-
// }
|
|
547
|
-
// let edgeMeta = await this.client.ContentObjectMetadata({
|
|
548
|
-
// libraryId: libraryId,
|
|
549
|
-
// objectId: objectId,
|
|
550
|
-
// writeToken: edgeWriteToken
|
|
551
|
-
// });
|
|
552
|
-
//
|
|
553
|
-
// // Stop the LRO if running
|
|
554
|
-
// let status = await this.Status({name});
|
|
555
|
-
// if(status.state != "terminated") {
|
|
556
|
-
// console.log("STOPPING");
|
|
557
|
-
// try {
|
|
558
|
-
// await this.client.CallBitcodeMethod({
|
|
559
|
-
// libraryId: status.library_id,
|
|
560
|
-
// objectId: status.object_id,
|
|
561
|
-
// writeToken: status.edge_write_token,
|
|
562
|
-
// method: "/live/stop/" + status.tlro,
|
|
563
|
-
// constant: false
|
|
564
|
-
// });
|
|
565
|
-
// } catch(error) {
|
|
566
|
-
// // The /call/lro/stop API returns empty response
|
|
567
|
-
// // console.log("LRO Stop (failed): ", error);
|
|
568
|
-
// }
|
|
569
|
-
//
|
|
570
|
-
// // Wait until LRO is terminated
|
|
571
|
-
// let tries = 10;
|
|
572
|
-
// while (status.state != "terminated" && tries-- > 0) {
|
|
573
|
-
// console.log("Wait to terminate - ", status.state);
|
|
574
|
-
// await sleep(1000);
|
|
575
|
-
// status = await this.Status({name});
|
|
576
|
-
// }
|
|
577
|
-
// console.log("Status after terminate - ", status.state);
|
|
578
|
-
//
|
|
579
|
-
// if(tries <= 0) {
|
|
580
|
-
// console.log("Failed to terminate");
|
|
581
|
-
// return status;
|
|
582
|
-
// }
|
|
583
|
-
// }
|
|
584
|
-
//
|
|
585
|
-
// // Set stop time
|
|
586
|
-
// edgeMeta.recording_stop_time = Math.floor(new Date().getTime() / 1000);
|
|
587
|
-
// console.log("recording_start_time: ", edgeMeta.recording_start_time);
|
|
588
|
-
// console.log("recording_stop_time: ", edgeMeta.recording_stop_time);
|
|
589
|
-
//
|
|
590
|
-
// edgeMeta.live_recording.status = {
|
|
591
|
-
// state: "terminated",
|
|
592
|
-
// recording_stop_time: edgeMeta.recording_stop_time
|
|
593
|
-
// };
|
|
594
|
-
//
|
|
595
|
-
// edgeMeta.live_recording.fabric_config.edge_write_token = "";
|
|
596
|
-
//
|
|
597
|
-
// await this.client.ReplaceMetadata({
|
|
598
|
-
// libraryId: libraryId,
|
|
599
|
-
// objectId: objectId,
|
|
600
|
-
// writeToken: edgeWriteToken,
|
|
601
|
-
// metadata: edgeMeta
|
|
602
|
-
// });
|
|
603
|
-
//
|
|
604
|
-
// let fin = await this.client.FinalizeContentObject({
|
|
605
|
-
// libraryId,
|
|
606
|
-
// objectId,
|
|
607
|
-
// writeToken: edgeWriteToken,
|
|
608
|
-
// commitMessage: "Finalize live stream - stop time " + edgeMeta.recording_stop_time,
|
|
609
|
-
// publish: false // Don't publish this version because it is not currently useful
|
|
610
|
-
// });
|
|
611
|
-
//
|
|
612
|
-
// return {
|
|
613
|
-
// fin,
|
|
614
|
-
// name: name,
|
|
615
|
-
// edge_write_token: edgeWriteToken,
|
|
616
|
-
// state: "terminated"
|
|
617
|
-
// };
|
|
618
|
-
//
|
|
619
|
-
// } catch(error) {
|
|
620
|
-
// console.error(error);
|
|
621
|
-
// }
|
|
622
|
-
// }
|
|
623
|
-
|
|
624
|
-
// async Initialize({name, drm=false, format}) {
|
|
625
|
-
//
|
|
626
|
-
// const contentTypes = await this.client.ContentTypes();
|
|
627
|
-
//
|
|
628
|
-
// let typeAbrMaster;
|
|
629
|
-
// let typeLiveStream;
|
|
630
|
-
// for (let i = 0; i < Object.keys(contentTypes).length; i ++) {
|
|
631
|
-
// const key = Object.keys(contentTypes)[i];
|
|
632
|
-
// if(contentTypes[key].name.includes("ABR Master") || contentTypes[key].name.includes("Title")) {
|
|
633
|
-
// typeAbrMaster = contentTypes[key].hash;
|
|
634
|
-
// }
|
|
635
|
-
// if(contentTypes[key].name.includes("Live Stream")) {
|
|
636
|
-
// typeLiveStream = contentTypes[key].hash;
|
|
637
|
-
// }
|
|
638
|
-
// }
|
|
639
|
-
//
|
|
640
|
-
// if(typeAbrMaster === undefined || typeLiveStream === undefined) {
|
|
641
|
-
// console.log("ERROR - unable to find content types", "ABR Master", typeAbrMaster, "Live Stream", typeLiveStream);
|
|
642
|
-
// return {};
|
|
643
|
-
// }
|
|
644
|
-
// let res = await this.SetOfferingAndDRM({name, typeAbrMaster, typeLiveStream, drm, format});
|
|
645
|
-
// return res;
|
|
646
|
-
// }
|
|
647
|
-
|
|
648
|
-
// async SetOfferingAndDRM({name, typeAbrMaster, typeLiveStream, drm=false, format}) {
|
|
649
|
-
//
|
|
650
|
-
// let status = await this.Status({name});
|
|
651
|
-
// if(status.state != "inactive" && status.state != "terminated") {
|
|
652
|
-
// return {
|
|
653
|
-
// state: status.state,
|
|
654
|
-
// error: "stream still active - must terminate first"
|
|
655
|
-
// };
|
|
656
|
-
// }
|
|
657
|
-
//
|
|
658
|
-
// let objectId = status.object_id;
|
|
659
|
-
//
|
|
660
|
-
// console.log("INIT: ", name, objectId);
|
|
661
|
-
//
|
|
662
|
-
// const {GenerateOffering} = require("./LiveObjectSetupStepOne");
|
|
663
|
-
//
|
|
664
|
-
// const aBitRate = 128000;
|
|
665
|
-
// const aChannels = 2;
|
|
666
|
-
// const aSampleRate = 48000;
|
|
667
|
-
// const aStreamIndex = 1;
|
|
668
|
-
// const aTimeBase = "1/48000";
|
|
669
|
-
// const aChannelLayout = "stereo";
|
|
670
|
-
//
|
|
671
|
-
// const vBitRate = 14000000;
|
|
672
|
-
// const vHeight = 720;
|
|
673
|
-
// const vStreamIndex = 0;
|
|
674
|
-
// const vWidth = 1280;
|
|
675
|
-
// const vDisplayAspectRatio = "16/9";
|
|
676
|
-
// const vFrameRate = "30000/1001";
|
|
677
|
-
// const vTimeBase = "1/30000"; // "1/16000";
|
|
678
|
-
//
|
|
679
|
-
// const abrProfile = require("./abr_profile_live_drm.json");
|
|
680
|
-
//
|
|
681
|
-
// let playoutFormats = abrProfile.playout_formats;
|
|
682
|
-
// if(format) {
|
|
683
|
-
// drm = true; // Override DRM parameter
|
|
684
|
-
// playoutFormats = {};
|
|
685
|
-
// let formats = format.split(",");
|
|
686
|
-
// for (let i = 0; i < formats.length; i++) {
|
|
687
|
-
// if(formats[i] === "hls-clear") {
|
|
688
|
-
// abrProfile.drm_optional = true;
|
|
689
|
-
// playoutFormats["hls-clear"] = {
|
|
690
|
-
// "drm": null,
|
|
691
|
-
// "protocol": {
|
|
692
|
-
// "type": "ProtoHls"
|
|
693
|
-
// }
|
|
694
|
-
// };
|
|
695
|
-
// continue;
|
|
696
|
-
// }
|
|
697
|
-
// playoutFormats[formats[i]] = abrProfile.playout_formats[formats[i]];
|
|
698
|
-
// }
|
|
699
|
-
// } else if(!drm) {
|
|
700
|
-
// abrProfile.drm_optional = true;
|
|
701
|
-
// playoutFormats = {
|
|
702
|
-
// "hls-clear": {
|
|
703
|
-
// "drm": null,
|
|
704
|
-
// "protocol": {
|
|
705
|
-
// "type": "ProtoHls"
|
|
706
|
-
// }
|
|
707
|
-
// }
|
|
708
|
-
// };
|
|
709
|
-
// }
|
|
710
|
-
//
|
|
711
|
-
// abrProfile.playout_formats = playoutFormats;
|
|
712
|
-
//
|
|
713
|
-
// let libraryId = await this.client.ContentObjectLibraryId({objectId});
|
|
714
|
-
//
|
|
715
|
-
// try {
|
|
716
|
-
//
|
|
717
|
-
// let mainMeta = await this.client.ContentObjectMetadata({
|
|
718
|
-
// libraryId: libraryId,
|
|
719
|
-
// objectId: objectId
|
|
720
|
-
// });
|
|
721
|
-
//
|
|
722
|
-
// let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
|
|
723
|
-
// // Support both hostname and URL ingress_node_api
|
|
724
|
-
// if(!fabURI.startsWith("http")) {
|
|
725
|
-
// // Assume https
|
|
726
|
-
// fabURI = "https://" + fabURI;
|
|
727
|
-
// }
|
|
728
|
-
//
|
|
729
|
-
// this.client.SetNodes({fabricURIs: [fabURI]});
|
|
730
|
-
//
|
|
731
|
-
// let streamUrl = mainMeta.live_recording.recording_config.recording_params.origin_url;
|
|
732
|
-
//
|
|
733
|
-
// await GenerateOffering({
|
|
734
|
-
// client: this.client,
|
|
735
|
-
// libraryId,
|
|
736
|
-
// objectId,
|
|
737
|
-
// typeAbrMaster, typeLiveStream,
|
|
738
|
-
// streamUrl,
|
|
739
|
-
// abrProfile,
|
|
740
|
-
// aBitRate, aChannels, aSampleRate, aStreamIndex,
|
|
741
|
-
// aTimeBase,
|
|
742
|
-
// aChannelLayout,
|
|
743
|
-
// vBitRate, vHeight, vStreamIndex, vWidth,
|
|
744
|
-
// vDisplayAspectRatio, vFrameRate, vTimeBase
|
|
745
|
-
// });
|
|
746
|
-
//
|
|
747
|
-
// console.log("GenerateOffering - DONE");
|
|
748
|
-
//
|
|
749
|
-
// return {
|
|
750
|
-
// name,
|
|
751
|
-
// object_id: objectId,
|
|
752
|
-
// state: "initialized"
|
|
753
|
-
// };
|
|
754
|
-
// } catch(error) {
|
|
755
|
-
// console.error(error);
|
|
756
|
-
// }
|
|
757
|
-
// }
|
|
758
|
-
|
|
759
|
-
// Add a content insertion entry
|
|
760
|
-
// Parameters:
|
|
761
|
-
// - insertionTime - seconds (float)
|
|
762
|
-
// - sinceStart - true if time specified since stream start, false if since epoch
|
|
763
|
-
// - duration - seconds (float, deafault 20.0)
|
|
764
|
-
// - targetHash - playable
|
|
765
|
-
// - remove - flag to remove the insertion at that exact 'time' (instead of adding)
|
|
766
|
-
// async Insertion({name, insertionTime, sinceStart, duration, targetHash, remove}) {
|
|
767
|
-
//
|
|
768
|
-
// // Determine audio and video parameters of the insertion
|
|
769
|
-
// const insertionInfo = await this.getOfferingInfo({versionHash: targetHash});
|
|
770
|
-
// const audioAbrDuration = insertionInfo.audio.seg_duration_sec;
|
|
771
|
-
// const videoAbrDuration = insertionInfo.video.seg_duration_sec;
|
|
772
|
-
//
|
|
773
|
-
// if(audioAbrDuration === 0 || videoAbrDuration === 0) {
|
|
774
|
-
// throw new Error("Bad segment duration hash:", targetHash);
|
|
775
|
-
// }
|
|
776
|
-
//
|
|
777
|
-
// if(duration === undefined) {
|
|
778
|
-
// duration = insertionInfo.duration_sec; // Use full duration of the insertion
|
|
779
|
-
// } else {
|
|
780
|
-
// if(duration > insertionInfo.duration_sec) {
|
|
781
|
-
// throw new Error("Bad duration - larger than insertion object duration", insertionInfo.duration_sec);
|
|
782
|
-
// }
|
|
783
|
-
// }
|
|
784
|
-
//
|
|
785
|
-
// let conf = await this.LoadConf({name});
|
|
786
|
-
// let libraryId = await this.client.ContentObjectLibraryId({objectId: conf.objectId});
|
|
787
|
-
// let objectId = conf.objectId;
|
|
788
|
-
//
|
|
789
|
-
// let mainMeta = await this.client.ContentObjectMetadata({
|
|
790
|
-
// libraryId: libraryId,
|
|
791
|
-
// objectId: conf.objectId
|
|
792
|
-
// });
|
|
793
|
-
//
|
|
794
|
-
// let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
|
|
795
|
-
//
|
|
796
|
-
// // Support both hostname and URL ingress_node_api
|
|
797
|
-
// if(!fabURI.startsWith("http")) {
|
|
798
|
-
// // Assume https
|
|
799
|
-
// fabURI = "https://" + fabURI;
|
|
800
|
-
// }
|
|
801
|
-
// this.client.SetNodes({fabricURIs: [fabURI]});
|
|
802
|
-
// let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
|
|
803
|
-
//
|
|
804
|
-
// let edgeMeta = await this.client.ContentObjectMetadata({
|
|
805
|
-
// libraryId: libraryId,
|
|
806
|
-
// objectId: conf.objectId,
|
|
807
|
-
// writeToken: edgeWriteToken
|
|
808
|
-
// });
|
|
809
|
-
//
|
|
810
|
-
// // Find stream start time (from the most recent recording section)
|
|
811
|
-
// let recordings = edgeMeta.live_recording.recordings;
|
|
812
|
-
// let sequence = 1;
|
|
813
|
-
// let streamStartTime = 0;
|
|
814
|
-
// if(recordings != undefined && recordings.recording_sequence != undefined) {
|
|
815
|
-
// // We have at least one recording - check if still active
|
|
816
|
-
// sequence = recordings.recording_sequence;
|
|
817
|
-
// let period = recordings.live_offering[sequence - 1];
|
|
818
|
-
//
|
|
819
|
-
// if(period.end_time_epoch_sec > 0) {
|
|
820
|
-
// // The last period is closed - apply insertions to the next period
|
|
821
|
-
// sequence ++;
|
|
822
|
-
// } else {
|
|
823
|
-
// // The period is active
|
|
824
|
-
// streamStartTime = period.start_time_epoch_sec;
|
|
825
|
-
// }
|
|
826
|
-
// }
|
|
827
|
-
//
|
|
828
|
-
// if(streamStartTime === 0) {
|
|
829
|
-
// // There is no active period - must use absolute time
|
|
830
|
-
// if(sinceStart === false) {
|
|
831
|
-
// throw new Error("Stream not running - must use 'time since start'");
|
|
832
|
-
// }
|
|
833
|
-
// }
|
|
834
|
-
//
|
|
835
|
-
// // Find the current period playout configuration
|
|
836
|
-
// if(edgeMeta.live_recording.playout_config.interleaves === undefined) {
|
|
837
|
-
// edgeMeta.live_recording.playout_config.interleaves = {};
|
|
838
|
-
// }
|
|
839
|
-
// if(edgeMeta.live_recording.playout_config.interleaves[sequence] === undefined) {
|
|
840
|
-
// edgeMeta.live_recording.playout_config.interleaves[sequence] = [];
|
|
841
|
-
// }
|
|
842
|
-
//
|
|
843
|
-
// let playoutConfig = edgeMeta.live_recording.playout_config;
|
|
844
|
-
// let insertions = playoutConfig.interleaves[sequence];
|
|
845
|
-
//
|
|
846
|
-
// let res = {};
|
|
847
|
-
//
|
|
848
|
-
// if(!sinceStart) {
|
|
849
|
-
// insertionTime = insertionTime - streamStartTime;
|
|
850
|
-
// }
|
|
851
|
-
//
|
|
852
|
-
// // Assume insertions are sorted by insertion time
|
|
853
|
-
// let errs = [];
|
|
854
|
-
// let currentTime = -1;
|
|
855
|
-
// let insertionDone = false;
|
|
856
|
-
// let newInsertion = {
|
|
857
|
-
// insertion_time: insertionTime,
|
|
858
|
-
// duration: duration,
|
|
859
|
-
// audio_abr_duration: audioAbrDuration,
|
|
860
|
-
// video_abr_duration: videoAbrDuration,
|
|
861
|
-
// playout: "/qfab/" + targetHash + "/rep/playout" // TO FIX - should be a link
|
|
862
|
-
// };
|
|
863
|
-
//
|
|
864
|
-
// for (let i = 0; i < insertions.length; i ++) {
|
|
865
|
-
// if(insertions[i].insertion_time <= currentTime) {
|
|
866
|
-
// // Bad insertion - must be later than current time
|
|
867
|
-
// append(errs, "Bad insertion - time:", insertions[i].insertion_time);
|
|
868
|
-
// }
|
|
869
|
-
// if(remove) {
|
|
870
|
-
// if(insertions[i].insertion_time === insertionTime) {
|
|
871
|
-
// insertions.splice(i, 1);
|
|
872
|
-
// break;
|
|
873
|
-
// }
|
|
874
|
-
// } else {
|
|
875
|
-
// if(insertions[i].insertion_time > insertionTime) {
|
|
876
|
-
// if(i > 0) {
|
|
877
|
-
// insertions = [
|
|
878
|
-
// ...insertions.splice(0, i),
|
|
879
|
-
// newInsertion,
|
|
880
|
-
// ...insertions.splice(i)
|
|
881
|
-
// ];
|
|
882
|
-
// } else {
|
|
883
|
-
// insertions = [
|
|
884
|
-
// newInsertion,
|
|
885
|
-
// ...insertions.splice(i)
|
|
886
|
-
// ];
|
|
887
|
-
// }
|
|
888
|
-
// insertionDone = true;
|
|
889
|
-
// break;
|
|
890
|
-
// }
|
|
891
|
-
// }
|
|
892
|
-
// }
|
|
893
|
-
//
|
|
894
|
-
// if(!remove && !insertionDone) {
|
|
895
|
-
// // Add to the end of the insertions list
|
|
896
|
-
// console.log("Add insertion at the end");
|
|
897
|
-
// insertions = [
|
|
898
|
-
// ...insertions,
|
|
899
|
-
// newInsertion
|
|
900
|
-
// ];
|
|
901
|
-
// }
|
|
902
|
-
//
|
|
903
|
-
// playoutConfig.interleaves[sequence] = insertions;
|
|
904
|
-
//
|
|
905
|
-
// // Store the new insertions in the write token
|
|
906
|
-
// await this.client.ReplaceMetadata({
|
|
907
|
-
// libraryId: libraryId,
|
|
908
|
-
// objectId: objectId,
|
|
909
|
-
// writeToken: edgeWriteToken,
|
|
910
|
-
// metadataSubtree: "/live_recording/playout_config",
|
|
911
|
-
// metadata: edgeMeta.live_recording.playout_config
|
|
912
|
-
// });
|
|
913
|
-
//
|
|
914
|
-
// res.errors = errs;
|
|
915
|
-
// res.insertions = insertions;
|
|
916
|
-
// return res;
|
|
917
|
-
// }
|
|
779
|
+
exports.StreamStopSession = async function({name}) {
|
|
780
|
+
try {
|
|
781
|
+
console.log("TERMINATE: ", name);
|
|
782
|
+
|
|
783
|
+
let conf = await this.LoadConf({name});
|
|
918
784
|
|
|
785
|
+
let {objectId} = conf;
|
|
786
|
+
let libraryId = await this.ContentObjectLibraryId({objectId});
|
|
787
|
+
|
|
788
|
+
let mainMeta = await this.ContentObjectMetadata({
|
|
789
|
+
libraryId,
|
|
790
|
+
objectId
|
|
791
|
+
});
|
|
919
792
|
|
|
793
|
+
let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
|
|
794
|
+
// Support both hostname and URL ingress_node_api
|
|
795
|
+
if(!fabURI.startsWith("http")) {
|
|
796
|
+
// Assume https
|
|
797
|
+
fabURI = "https://" + fabURI;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
this.SetNodes({fabricURIs: [fabURI]});
|
|
801
|
+
|
|
802
|
+
let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
|
|
803
|
+
|
|
804
|
+
if(edgeWriteToken === undefined || edgeWriteToken === "") {
|
|
805
|
+
return {
|
|
806
|
+
state: "inactive",
|
|
807
|
+
error: "no active streams - must create a stream first"
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
let edgeMeta = await this.ContentObjectMetadata({
|
|
811
|
+
libraryId,
|
|
812
|
+
objectId,
|
|
813
|
+
writeToken: edgeWriteToken
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Stop the LRO if running
|
|
817
|
+
let status = await this.StreamStatus({name});
|
|
818
|
+
if(status.state != "terminated") {
|
|
819
|
+
console.log("STOPPING");
|
|
820
|
+
try {
|
|
821
|
+
await this.CallBitcodeMethod({
|
|
822
|
+
libraryId: status.library_id,
|
|
823
|
+
objectId: status.object_id,
|
|
824
|
+
writeToken: status.edge_write_token,
|
|
825
|
+
method: "/live/stop/" + status.tlro,
|
|
826
|
+
constant: false
|
|
827
|
+
});
|
|
828
|
+
} catch(error) {
|
|
829
|
+
// The /call/lro/stop API returns empty response
|
|
830
|
+
// console.log("LRO Stop (failed): ", error);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Wait until LRO is terminated
|
|
834
|
+
let tries = 10;
|
|
835
|
+
while(status.state != "stopped" && tries-- > 0) {
|
|
836
|
+
console.log("Wait to terminate - ", status.state);
|
|
837
|
+
await Sleep(1000);
|
|
838
|
+
status = await this.StreamStatus({name});
|
|
839
|
+
}
|
|
840
|
+
console.log("Status after stop - ", status.state);
|
|
841
|
+
|
|
842
|
+
if(tries <= 0) {
|
|
843
|
+
console.log("Failed to stop");
|
|
844
|
+
return status;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Set stop time
|
|
849
|
+
edgeMeta.recording_stop_time = Math.floor(new Date().getTime() / 1000);
|
|
850
|
+
console.log("recording_start_time: ", edgeMeta.recording_start_time);
|
|
851
|
+
console.log("recording_stop_time: ", edgeMeta.recording_stop_time);
|
|
852
|
+
|
|
853
|
+
edgeMeta.live_recording.status = {
|
|
854
|
+
state: "terminated",
|
|
855
|
+
recording_stop_time: edgeMeta.recording_stop_time
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
edgeMeta.live_recording.fabric_config.edge_write_token = "";
|
|
859
|
+
|
|
860
|
+
await this.ReplaceMetadata({
|
|
861
|
+
libraryId,
|
|
862
|
+
objectId,
|
|
863
|
+
writeToken: edgeWriteToken,
|
|
864
|
+
metadata: edgeMeta
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
let fin = await this.FinalizeContentObject({
|
|
868
|
+
libraryId,
|
|
869
|
+
objectId,
|
|
870
|
+
writeToken: edgeWriteToken,
|
|
871
|
+
commitMessage: "Finalize live stream - stop time " + edgeMeta.recording_stop_time,
|
|
872
|
+
publish: false // Don't publish this version because it is not currently useful
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
return {
|
|
876
|
+
fin,
|
|
877
|
+
name,
|
|
878
|
+
edge_write_token: edgeWriteToken,
|
|
879
|
+
state: "terminated"
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
} catch(error) {
|
|
883
|
+
console.error(error);
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Initialize the stream object
|
|
889
|
+
*
|
|
890
|
+
* @methodGroup Live Stream
|
|
891
|
+
* @namedParams
|
|
892
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
893
|
+
* @param {boolean=} drm - If specified, playout will be DRM protected
|
|
894
|
+
* @param {string=} format - Specify the list of playout formats and DRM to support,
|
|
895
|
+
comma-separated (hls-clear, hls-aes128, hls-sample-aes,
|
|
896
|
+
hls-fairplay)
|
|
897
|
+
*
|
|
898
|
+
* @return {Promise<Object>} - The name, object ID, and state of the stream
|
|
899
|
+
*/
|
|
900
|
+
exports.StreamInitialize = async function({name, drm=false, format}) {
|
|
901
|
+
const contentTypes = await this.ContentTypes();
|
|
902
|
+
|
|
903
|
+
let typeAbrMaster;
|
|
904
|
+
let typeLiveStream;
|
|
905
|
+
|
|
906
|
+
for(let i = 0; i < Object.keys(contentTypes).length; i++) {
|
|
907
|
+
const key = Object.keys(contentTypes)[i];
|
|
908
|
+
|
|
909
|
+
if(contentTypes[key].name.includes("ABR Master") || contentTypes[key].name.includes("Title")) {
|
|
910
|
+
typeAbrMaster = contentTypes[key].hash;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if(contentTypes[key].name.includes("Live Stream")) {
|
|
914
|
+
typeLiveStream = contentTypes[key].hash;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if(typeAbrMaster === undefined || typeLiveStream === undefined) {
|
|
919
|
+
console.log("ERROR - unable to find content types", "ABR Master", typeAbrMaster, "Live Stream", typeLiveStream);
|
|
920
|
+
return {};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const res = await this.StreamSetOfferingAndDRM({name, typeAbrMaster, typeLiveStream, drm, format});
|
|
924
|
+
|
|
925
|
+
return res;
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Set the Live Stream offering
|
|
930
|
+
*
|
|
931
|
+
* @methodGroup Live Stream
|
|
932
|
+
* @namedParams
|
|
933
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
934
|
+
* @param {string=} typeAbrMaster - Content type hash
|
|
935
|
+
* @param {string=} typeLiveStream - Content type hash
|
|
936
|
+
* @param {boolean=} drm - If specified, DRM will be applied to the stream
|
|
937
|
+
* @param {string=} format - A list of playout formats and DRM to support, comma-separated
|
|
938
|
+
* (hls-clear, hls-aes128, hls-sample-aes, hls-fairplay). If specified,
|
|
939
|
+
* this will take precedence over the drm value
|
|
940
|
+
*
|
|
941
|
+
* @return {Promise<Object>} - The name, object ID, and state of the stream
|
|
942
|
+
*/
|
|
943
|
+
exports.StreamSetOfferingAndDRM = async function({name, typeAbrMaster, typeLiveStream, drm=false, format}) {
|
|
944
|
+
let status = await this.StreamStatus({name});
|
|
945
|
+
if(status.state != "inactive" && status.state != "stopped") {
|
|
946
|
+
return {
|
|
947
|
+
state: status.state,
|
|
948
|
+
error: "stream still active - must terminate first"
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
let objectId = status.object_id;
|
|
953
|
+
|
|
954
|
+
console.log("INIT: ", name, objectId);
|
|
955
|
+
|
|
956
|
+
const aBitRate = 128000;
|
|
957
|
+
const aChannels = 2;
|
|
958
|
+
const aSampleRate = 48000;
|
|
959
|
+
const aStreamIndex = 1;
|
|
960
|
+
const aTimeBase = "1/48000";
|
|
961
|
+
const aChannelLayout = "stereo";
|
|
962
|
+
|
|
963
|
+
const vBitRate = 14000000;
|
|
964
|
+
const vHeight = 720;
|
|
965
|
+
const vStreamIndex = 0;
|
|
966
|
+
const vWidth = 1280;
|
|
967
|
+
const vDisplayAspectRatio = "16/9";
|
|
968
|
+
const vFrameRate = "30000/1001";
|
|
969
|
+
const vTimeBase = "1/30000"; // "1/16000";
|
|
970
|
+
|
|
971
|
+
const abrProfile = require("../abr_profiles/abr_profile_live_drm.js");
|
|
972
|
+
|
|
973
|
+
let playoutFormats = abrProfile.playout_formats;
|
|
974
|
+
if(format) {
|
|
975
|
+
drm = true; // Override DRM parameter
|
|
976
|
+
playoutFormats = {};
|
|
977
|
+
let formats = format.split(",");
|
|
978
|
+
for(let i = 0; i < formats.length; i++) {
|
|
979
|
+
if(formats[i] === "hls-clear") {
|
|
980
|
+
abrProfile.drm_optional = true;
|
|
981
|
+
playoutFormats["hls-clear"] = {
|
|
982
|
+
"drm": null,
|
|
983
|
+
"protocol": {
|
|
984
|
+
"type": "ProtoHls"
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
playoutFormats[formats[i]] = abrProfile.playout_formats[formats[i]];
|
|
990
|
+
}
|
|
991
|
+
} else if(!drm) {
|
|
992
|
+
abrProfile.drm_optional = true;
|
|
993
|
+
playoutFormats = {
|
|
994
|
+
"hls-clear": {
|
|
995
|
+
"drm": null,
|
|
996
|
+
"protocol": {
|
|
997
|
+
"type": "ProtoHls"
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
abrProfile.playout_formats = playoutFormats;
|
|
1004
|
+
|
|
1005
|
+
let libraryId = await this.ContentObjectLibraryId({objectId});
|
|
1006
|
+
|
|
1007
|
+
try {
|
|
1008
|
+
let mainMeta = await this.ContentObjectMetadata({
|
|
1009
|
+
libraryId,
|
|
1010
|
+
objectId
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
|
|
1014
|
+
// Support both hostname and URL ingress_node_api
|
|
1015
|
+
if(!fabURI.startsWith("http")) {
|
|
1016
|
+
// Assume https
|
|
1017
|
+
fabURI = "https://" + fabURI;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
this.SetNodes({fabricURIs: [fabURI]});
|
|
1021
|
+
|
|
1022
|
+
let streamUrl = mainMeta.live_recording.recording_config.recording_params.origin_url;
|
|
1023
|
+
|
|
1024
|
+
await StreamGenerateOffering({
|
|
1025
|
+
client: this,
|
|
1026
|
+
libraryId,
|
|
1027
|
+
objectId,
|
|
1028
|
+
typeAbrMaster,
|
|
1029
|
+
typeLiveStream,
|
|
1030
|
+
streamUrl,
|
|
1031
|
+
abrProfile,
|
|
1032
|
+
aBitRate,
|
|
1033
|
+
aChannels,
|
|
1034
|
+
aSampleRate,
|
|
1035
|
+
aStreamIndex,
|
|
1036
|
+
aTimeBase,
|
|
1037
|
+
aChannelLayout,
|
|
1038
|
+
vBitRate,
|
|
1039
|
+
vHeight,
|
|
1040
|
+
vStreamIndex,
|
|
1041
|
+
vWidth,
|
|
1042
|
+
vDisplayAspectRatio,
|
|
1043
|
+
vFrameRate,
|
|
1044
|
+
vTimeBase
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
console.log("Finished generating offering");
|
|
1048
|
+
|
|
1049
|
+
return {
|
|
1050
|
+
name,
|
|
1051
|
+
object_id: objectId,
|
|
1052
|
+
state: "initialized"
|
|
1053
|
+
};
|
|
1054
|
+
} catch(error) {
|
|
1055
|
+
console.error(error);
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Add a content insertion entry
|
|
1061
|
+
*
|
|
1062
|
+
* @methodGroup Live Stream
|
|
1063
|
+
* @namedParams
|
|
1064
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
1065
|
+
* @param {number} insertionTime - Time in seconds (float)
|
|
1066
|
+
* @param {boolean=} sinceStart - If specified, time specified will be elapsed seconds
|
|
1067
|
+
* since stream start, otherwise, time will be elapsed since epoch
|
|
1068
|
+
* @param {number=} duration - Time in seconds (float). Default: 20.0
|
|
1069
|
+
* @param {string} targetHash - The target content object hash (playable)
|
|
1070
|
+
* @param {boolean=} remove - If specified, will remove the inseration at the exact 'time' (instead of adding)
|
|
1071
|
+
*
|
|
1072
|
+
* @return {Promise<Object>} - Insertions, as well as any errors from bad insertions
|
|
1073
|
+
*/
|
|
1074
|
+
exports.StreamInsertion = async function({name, insertionTime, sinceStart=false, duration, targetHash, remove=false}) {
|
|
1075
|
+
// Determine audio and video parameters of the insertion
|
|
1076
|
+
|
|
1077
|
+
// Content Type check is currently disabled due to permissions
|
|
1078
|
+
/*
|
|
1079
|
+
let ct = await this.client.ContentObject({versionHash});
|
|
1080
|
+
if(ct.type != undefined && ct.type != "") {
|
|
1081
|
+
let typeMeta = await this.client.ContentObjectMetadata({
|
|
1082
|
+
versionHash: ct.type
|
|
1083
|
+
});
|
|
1084
|
+
if(typeMeta.bitcode_flags != "abrmaster") {
|
|
1085
|
+
throw new Error("Not a playable VoD object " + versionHash);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
*/
|
|
1089
|
+
let offeringMeta = await this.ContentObjectMetadata({
|
|
1090
|
+
versionHash: targetHash,
|
|
1091
|
+
metadataSubtree: "/offerings/default"
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
var insertionInfo = {
|
|
1095
|
+
duration_sec: 0 // Minimum of video and audio duration
|
|
1096
|
+
};
|
|
1097
|
+
["video", "audio"].forEach(mt => {
|
|
1098
|
+
const stream = offeringMeta.media_struct.streams[mt];
|
|
1099
|
+
insertionInfo[mt] = {
|
|
1100
|
+
seg_duration_sec: stream.optimum_seg_dur.float,
|
|
1101
|
+
duration_sec: stream.duration.float,
|
|
1102
|
+
frame_rate_rat: stream.rate,
|
|
1103
|
+
};
|
|
1104
|
+
if(insertionInfo.duration_sec === 0 || stream.duration.float < insertionInfo.duration_sec) {
|
|
1105
|
+
insertionInfo.duration_sec = stream.duration.float;
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
const audioAbrDuration = insertionInfo.audio.seg_duration_sec;
|
|
1110
|
+
const videoAbrDuration = insertionInfo.video.seg_duration_sec;
|
|
1111
|
+
|
|
1112
|
+
if(audioAbrDuration === 0 || videoAbrDuration === 0) {
|
|
1113
|
+
throw new Error("Bad segment duration hash:", targetHash);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if(duration === undefined) {
|
|
1117
|
+
duration = insertionInfo.duration_sec; // Use full duration of the insertion
|
|
1118
|
+
} else {
|
|
1119
|
+
if(duration > insertionInfo.duration_sec) {
|
|
1120
|
+
throw new Error("Bad duration - larger than insertion object duration", insertionInfo.duration_sec);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
let conf = await this.LoadConf({name});
|
|
1125
|
+
let libraryId = await this.ContentObjectLibraryId({objectId: conf.objectId});
|
|
1126
|
+
let objectId = conf.objectId;
|
|
1127
|
+
|
|
1128
|
+
let mainMeta = await this.ContentObjectMetadata({
|
|
1129
|
+
libraryId: libraryId,
|
|
1130
|
+
objectId: conf.objectId
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
|
|
1134
|
+
|
|
1135
|
+
// Support both hostname and URL ingress_node_api
|
|
1136
|
+
if(!fabURI.startsWith("http")) {
|
|
1137
|
+
// Assume https
|
|
1138
|
+
fabURI = "https://" + fabURI;
|
|
1139
|
+
}
|
|
1140
|
+
this.SetNodes({fabricURIs: [fabURI]});
|
|
1141
|
+
let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
|
|
1142
|
+
|
|
1143
|
+
let edgeMeta = await this.ContentObjectMetadata({
|
|
1144
|
+
libraryId: libraryId,
|
|
1145
|
+
objectId: conf.objectId,
|
|
1146
|
+
writeToken: edgeWriteToken
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// Find stream start time (from the most recent recording section)
|
|
1150
|
+
let recordings = edgeMeta.live_recording.recordings;
|
|
1151
|
+
let sequence = 1;
|
|
1152
|
+
let streamStartTime = 0;
|
|
1153
|
+
if(recordings != undefined && recordings.recording_sequence != undefined) {
|
|
1154
|
+
// We have at least one recording - check if still active
|
|
1155
|
+
sequence = recordings.recording_sequence;
|
|
1156
|
+
let period = recordings.live_offering[sequence - 1];
|
|
1157
|
+
|
|
1158
|
+
if(period.end_time_epoch_sec > 0) {
|
|
1159
|
+
// The last period is closed - apply insertions to the next period
|
|
1160
|
+
sequence ++;
|
|
1161
|
+
} else {
|
|
1162
|
+
// The period is active
|
|
1163
|
+
streamStartTime = period.start_time_epoch_sec;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if(streamStartTime === 0) {
|
|
1168
|
+
// There is no active period - must use absolute time
|
|
1169
|
+
if(sinceStart === false) {
|
|
1170
|
+
throw new Error("Stream not running - must use 'time since start'");
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Find the current period playout configuration
|
|
1175
|
+
if(edgeMeta.live_recording.playout_config.interleaves === undefined) {
|
|
1176
|
+
edgeMeta.live_recording.playout_config.interleaves = {};
|
|
1177
|
+
}
|
|
1178
|
+
if(edgeMeta.live_recording.playout_config.interleaves[sequence] === undefined) {
|
|
1179
|
+
edgeMeta.live_recording.playout_config.interleaves[sequence] = [];
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
let playoutConfig = edgeMeta.live_recording.playout_config;
|
|
1183
|
+
let insertions = playoutConfig.interleaves[sequence];
|
|
1184
|
+
|
|
1185
|
+
let res = {};
|
|
1186
|
+
|
|
1187
|
+
if(!sinceStart) {
|
|
1188
|
+
insertionTime = insertionTime - streamStartTime;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Assume insertions are sorted by insertion time
|
|
1192
|
+
let errs = [];
|
|
1193
|
+
let currentTime = -1;
|
|
1194
|
+
let insertionDone = false;
|
|
1195
|
+
let newInsertion = {
|
|
1196
|
+
insertion_time: insertionTime,
|
|
1197
|
+
duration: duration,
|
|
1198
|
+
audio_abr_duration: audioAbrDuration,
|
|
1199
|
+
video_abr_duration: videoAbrDuration,
|
|
1200
|
+
playout: "/qfab/" + targetHash + "/rep/playout" // TO FIX - should be a link
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
for(let i = 0; i < insertions.length; i ++) {
|
|
1204
|
+
if(insertions[i].insertion_time <= currentTime) {
|
|
1205
|
+
// Bad insertion - must be later than current time
|
|
1206
|
+
append(errs, "Bad insertion - time:", insertions[i].insertion_time);
|
|
1207
|
+
}
|
|
1208
|
+
if(remove) {
|
|
1209
|
+
if(insertions[i].insertion_time === insertionTime) {
|
|
1210
|
+
insertions.splice(i, 1);
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
} else {
|
|
1214
|
+
if(insertions[i].insertion_time > insertionTime) {
|
|
1215
|
+
if(i > 0) {
|
|
1216
|
+
insertions = [
|
|
1217
|
+
...insertions.splice(0, i),
|
|
1218
|
+
newInsertion,
|
|
1219
|
+
...insertions.splice(i)
|
|
1220
|
+
];
|
|
1221
|
+
} else {
|
|
1222
|
+
insertions = [
|
|
1223
|
+
newInsertion,
|
|
1224
|
+
...insertions.splice(i)
|
|
1225
|
+
];
|
|
1226
|
+
}
|
|
1227
|
+
insertionDone = true;
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if(!remove && !insertionDone) {
|
|
1234
|
+
// Add to the end of the insertions list
|
|
1235
|
+
console.log("Add insertion at the end");
|
|
1236
|
+
insertions = [
|
|
1237
|
+
...insertions,
|
|
1238
|
+
newInsertion
|
|
1239
|
+
];
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
playoutConfig.interleaves[sequence] = insertions;
|
|
1243
|
+
|
|
1244
|
+
// Store the new insertions in the write token
|
|
1245
|
+
await this.ReplaceMetadata({
|
|
1246
|
+
libraryId: libraryId,
|
|
1247
|
+
objectId: objectId,
|
|
1248
|
+
writeToken: edgeWriteToken,
|
|
1249
|
+
metadataSubtree: "/live_recording/playout_config",
|
|
1250
|
+
metadata: edgeMeta.live_recording.playout_config
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
res.errors = errs;
|
|
1254
|
+
res.insertions = insertions;
|
|
1255
|
+
|
|
1256
|
+
return res;
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Load cached stream configuration
|
|
1261
|
+
*
|
|
1262
|
+
* @methodGroup Live Stream
|
|
1263
|
+
* @namedParams
|
|
1264
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
1265
|
+
*
|
|
1266
|
+
* @return {Promise<Object>} - The configuration of the stream
|
|
1267
|
+
*/
|
|
920
1268
|
exports.LoadConf = async function({name}) {
|
|
921
1269
|
if(name.startsWith("iq__")) {
|
|
922
1270
|
return {
|
|
@@ -945,180 +1293,22 @@ exports.LoadConf = async function({name}) {
|
|
|
945
1293
|
return conf;
|
|
946
1294
|
};
|
|
947
1295
|
|
|
948
|
-
/*
|
|
949
|
-
* Read a playable contnet object and get information about a particular offering
|
|
950
|
-
*/
|
|
951
|
-
// async getOfferingInfo({versionHash, offering = "default"}) {
|
|
952
|
-
//
|
|
953
|
-
// // Content Type check is currently disabled due to permissions
|
|
954
|
-
// /*
|
|
955
|
-
// let ct = await this.client.ContentObject({versionHash});
|
|
956
|
-
// if(ct.type != undefined && ct.type != "") {
|
|
957
|
-
// let typeMeta = await this.client.ContentObjectMetadata({
|
|
958
|
-
// versionHash: ct.type
|
|
959
|
-
// });
|
|
960
|
-
// if(typeMeta.bitcode_flags != "abrmaster") {
|
|
961
|
-
// throw new Error("Not a playable VoD object " + versionHash);
|
|
962
|
-
// }
|
|
963
|
-
// }
|
|
964
|
-
// */
|
|
965
|
-
// let offeringMeta = await this.client.ContentObjectMetadata({
|
|
966
|
-
// versionHash,
|
|
967
|
-
// metadataSubtree: "/offerings/" + offering
|
|
968
|
-
// });
|
|
969
|
-
//
|
|
970
|
-
// var info = {
|
|
971
|
-
// duration_sec: 0 // Minimum of video and audio duration
|
|
972
|
-
// };
|
|
973
|
-
// ["video", "audio"].forEach(mt => {
|
|
974
|
-
// const stream = offeringMeta.media_struct.streams[mt];
|
|
975
|
-
// info[mt] = {
|
|
976
|
-
// seg_duration_sec: stream.optimum_seg_dur.float,
|
|
977
|
-
// duration_sec: stream.duration.float,
|
|
978
|
-
// frame_rate_rat: stream.rate,
|
|
979
|
-
// };
|
|
980
|
-
// if(info.duration_sec === 0 || stream.duration.float < info.duration_sec) {
|
|
981
|
-
// info.duration_sec = stream.duration.float;
|
|
982
|
-
// }
|
|
983
|
-
// });
|
|
984
|
-
// return info;
|
|
985
|
-
// }
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
// async StreamDownload({name, period}) {
|
|
989
|
-
//
|
|
990
|
-
// let conf = await this.LoadConf({name});
|
|
991
|
-
//
|
|
992
|
-
// let status = {name};
|
|
993
|
-
//
|
|
994
|
-
// try {
|
|
995
|
-
//
|
|
996
|
-
// let libraryId = await this.client.ContentObjectLibraryId({objectId: conf.objectId});
|
|
997
|
-
// status.library_id = libraryId;
|
|
998
|
-
// status.object_id = conf.objectId;
|
|
999
|
-
//
|
|
1000
|
-
// let mainMeta = await this.client.ContentObjectMetadata({
|
|
1001
|
-
// libraryId: libraryId,
|
|
1002
|
-
// objectId: conf.objectId
|
|
1003
|
-
// });
|
|
1004
|
-
//
|
|
1005
|
-
// let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
|
|
1006
|
-
// if(fabURI === undefined) {
|
|
1007
|
-
// console.log("bad fabric config - missing ingress node API");
|
|
1008
|
-
// }
|
|
1009
|
-
//
|
|
1010
|
-
// // Support both hostname and URL ingress_node_api
|
|
1011
|
-
// if(!fabURI.startsWith("http")) {
|
|
1012
|
-
// // Assume https
|
|
1013
|
-
// fabURI = "https://" + fabURI;
|
|
1014
|
-
// }
|
|
1015
|
-
// this.client.SetNodes({fabricURIs: [fabURI]});
|
|
1016
|
-
//
|
|
1017
|
-
// let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
|
|
1018
|
-
// let edgeMeta = await this.client.ContentObjectMetadata({
|
|
1019
|
-
// libraryId: libraryId,
|
|
1020
|
-
// objectId: conf.objectId,
|
|
1021
|
-
// writeToken: edgeWriteToken
|
|
1022
|
-
// });
|
|
1023
|
-
//
|
|
1024
|
-
// // If a stream has never been started return state 'inactive'
|
|
1025
|
-
// if(edgeMeta.live_recording === undefined ||
|
|
1026
|
-
// edgeMeta.live_recording.recordings === undefined ||
|
|
1027
|
-
// edgeMeta.live_recording.recordings.recording_sequence === undefined) {
|
|
1028
|
-
// status.state = "no recordings";
|
|
1029
|
-
// return status;
|
|
1030
|
-
// }
|
|
1031
|
-
//
|
|
1032
|
-
// let recordings = edgeMeta.live_recording.recordings;
|
|
1033
|
-
// status.recording_period_sequence = recordings.recording_sequence;
|
|
1034
|
-
//
|
|
1035
|
-
// let sequence = recordings.recording_sequence;
|
|
1036
|
-
// if(period === undefined || period < 0 || period > sequence - 1) {
|
|
1037
|
-
// period = sequence - 1;
|
|
1038
|
-
// }
|
|
1039
|
-
//
|
|
1040
|
-
// console.log("Downloading stream", name, " period", period, " latest", sequence - 1);
|
|
1041
|
-
//
|
|
1042
|
-
// let recording = recordings.live_offering[period];
|
|
1043
|
-
// if(recording === undefined) {
|
|
1044
|
-
// console.log("ERROR - recording period not found: ", period);
|
|
1045
|
-
// }
|
|
1046
|
-
//
|
|
1047
|
-
// let dpath = "DOWNLOAD/" + edgeWriteToken + "." + period;
|
|
1048
|
-
// !fs.existsSync(dpath) && fs.mkdirSync(dpath, {recursive: true});
|
|
1049
|
-
//
|
|
1050
|
-
// let mts = ["audio", "video"];
|
|
1051
|
-
// for (let mi = 0; mi < mts.length; mi ++) {
|
|
1052
|
-
// let mt = mts[mi];
|
|
1053
|
-
// console.log("Downloading ", mt);
|
|
1054
|
-
// let mtpath = dpath + "/" + mt;
|
|
1055
|
-
// let partsfile = dpath + "/parts_" + mt + ".txt";
|
|
1056
|
-
// !fs.existsSync(mtpath) && fs.mkdirSync(mtpath);
|
|
1057
|
-
// var sources = recording.sources[mt];
|
|
1058
|
-
// for (let i = 0; i < sources.length - 1; i++) {
|
|
1059
|
-
// console.log(sources[i].hash);
|
|
1060
|
-
// let partHash = sources[i].hash;
|
|
1061
|
-
// let buf = await this.client.DownloadPart({
|
|
1062
|
-
// libraryId,
|
|
1063
|
-
// objectId: conf.objectId,
|
|
1064
|
-
// partHash,
|
|
1065
|
-
// format: "buffer",
|
|
1066
|
-
// chunked: false,
|
|
1067
|
-
// callback: ({bytesFinished, bytesTotal}) => {
|
|
1068
|
-
// console.log(" progress: ", bytesFinished + "/" + bytesTotal);
|
|
1069
|
-
// }
|
|
1070
|
-
// });
|
|
1071
|
-
//
|
|
1072
|
-
// let partfile = mtpath + "/" + partHash + ".mp4";
|
|
1073
|
-
// fs.appendFile(partfile, buf, (err) => {
|
|
1074
|
-
// if(err)
|
|
1075
|
-
// console.log(err);
|
|
1076
|
-
// });
|
|
1077
|
-
// fs.appendFile(partsfile, "file '" + mt + "/" + partHash + ".mp4'\n", (err) => {
|
|
1078
|
-
// if(err)
|
|
1079
|
-
// console.log(err);
|
|
1080
|
-
// });
|
|
1081
|
-
// }
|
|
1082
|
-
//
|
|
1083
|
-
// // Concatenate parts into one mp4
|
|
1084
|
-
// let cmd = "ffmpeg -f concat -safe 0 -i " + partsfile + " -c copy " + dpath + "/" + mt + ".mp4";
|
|
1085
|
-
// console.log("Running", cmd);
|
|
1086
|
-
// execSync(cmd);
|
|
1087
|
-
// }
|
|
1088
|
-
//
|
|
1089
|
-
// // Create final mp4 file
|
|
1090
|
-
// let f = dpath + "/download.mp4";
|
|
1091
|
-
// let cmd = "ffmpeg -i " + dpath + "/video.mp4" + " -i " + dpath + "/audio.mp4" + " -map 0:v:0 -map 1:a:0 -c copy -shortest " + f;
|
|
1092
|
-
// console.log("Running", cmd);
|
|
1093
|
-
// execSync(cmd);
|
|
1094
|
-
//
|
|
1095
|
-
// status.file = f;
|
|
1096
|
-
// status.state = "completed";
|
|
1097
|
-
// } catch(e) {
|
|
1098
|
-
// console.log("Download failed", e);
|
|
1099
|
-
// throw e;
|
|
1100
|
-
// }
|
|
1101
|
-
//
|
|
1102
|
-
// return status;
|
|
1103
|
-
// }
|
|
1104
|
-
|
|
1105
1296
|
/**
|
|
1106
1297
|
* Configure the stream
|
|
1107
1298
|
*
|
|
1108
1299
|
* @methodGroup Live Stream
|
|
1109
1300
|
* @namedParams
|
|
1110
|
-
* @param {string} name -
|
|
1111
|
-
* @param {
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
* - 'stop'
|
|
1301
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
1302
|
+
* @param {Object=} customSettings - Additional options to customize configuration settings
|
|
1303
|
+
* - audioBitrate
|
|
1304
|
+
* - audioIndex
|
|
1305
|
+
* - partTtl
|
|
1306
|
+
* - channelLayout
|
|
1117
1307
|
*
|
|
1118
|
-
* @return {Object} - The status response for the stream
|
|
1308
|
+
* @return {Promise<Object>} - The status response for the stream
|
|
1119
1309
|
*
|
|
1120
1310
|
*/
|
|
1121
|
-
exports.StreamConfig = async function({name, customSettings}) {
|
|
1311
|
+
exports.StreamConfig = async function({name, customSettings={}}) {
|
|
1122
1312
|
let conf = await this.LoadConf({name});
|
|
1123
1313
|
let status = {name};
|
|
1124
1314
|
|
|
@@ -1133,9 +1323,10 @@ exports.StreamConfig = async function({name, customSettings}) {
|
|
|
1133
1323
|
|
|
1134
1324
|
let userConfig = mainMeta.live_recording_config;
|
|
1135
1325
|
status.user_config = userConfig;
|
|
1326
|
+
console.log("userConfig", userConfig);
|
|
1136
1327
|
|
|
1137
1328
|
// Get node URI from user config
|
|
1138
|
-
const hostName = userConfig.url.replace("udp://", "").replace("rtmp://", "").split(":")[0];
|
|
1329
|
+
const hostName = userConfig.url.replace("udp://", "").replace("rtmp://", "").replace("srt://", "").split(":")[0];
|
|
1139
1330
|
const streamUrl = new URL(userConfig.url);
|
|
1140
1331
|
|
|
1141
1332
|
console.log("Retrieving nodes...");
|
|
@@ -1188,7 +1379,6 @@ exports.StreamConfig = async function({name, customSettings}) {
|
|
|
1188
1379
|
}
|
|
1189
1380
|
}
|
|
1190
1381
|
|
|
1191
|
-
console.log("PROBE", probe);
|
|
1192
1382
|
probe.format.filename = streamUrl.href;
|
|
1193
1383
|
|
|
1194
1384
|
// Create live recording config
|
|
@@ -1218,14 +1408,6 @@ exports.StreamConfig = async function({name, customSettings}) {
|
|
|
1218
1408
|
metadata: liveRecordingConfig.live_recording
|
|
1219
1409
|
});
|
|
1220
1410
|
|
|
1221
|
-
await this.ReplaceMetadata({
|
|
1222
|
-
libraryId,
|
|
1223
|
-
objectId: conf.objectId,
|
|
1224
|
-
writeToken,
|
|
1225
|
-
metadataSubtree: "probe",
|
|
1226
|
-
metadata: probe
|
|
1227
|
-
});
|
|
1228
|
-
|
|
1229
1411
|
status.fin = await this.FinalizeContentObject({
|
|
1230
1412
|
libraryId,
|
|
1231
1413
|
objectId: conf.objectId,
|
|
@@ -1236,321 +1418,99 @@ exports.StreamConfig = async function({name, customSettings}) {
|
|
|
1236
1418
|
return status;
|
|
1237
1419
|
};
|
|
1238
1420
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
// }
|
|
1248
|
-
//
|
|
1249
|
-
// try {
|
|
1250
|
-
//
|
|
1251
|
-
// let meta = await client.ContentObjectMetadata({
|
|
1252
|
-
// libraryId: conf.libraryId,
|
|
1253
|
-
// objectId: conf.objectId
|
|
1254
|
-
// });
|
|
1255
|
-
//
|
|
1256
|
-
// status.channel_title = meta.public.asset_metadata.title;
|
|
1257
|
-
// let source = meta.channel.offerings.default.items[0].source["/"];
|
|
1258
|
-
// let hash = source.split("/")[2];
|
|
1259
|
-
// status.stream_hash = hash;
|
|
1260
|
-
// latestHash = await client.LatestVersionHash({
|
|
1261
|
-
// versionHash: hash
|
|
1262
|
-
// });
|
|
1263
|
-
// status.stream_latest_hash = latestHash;
|
|
1264
|
-
//
|
|
1265
|
-
// if(hash != latestHash) {
|
|
1266
|
-
// status.warnings = ["Stream version is not the latest"];
|
|
1267
|
-
// }
|
|
1268
|
-
//
|
|
1269
|
-
// let channelFormatsUrl = await client.FabricUrl({
|
|
1270
|
-
// libraryId: conf.libraryId,
|
|
1271
|
-
// objectId: conf.objectId,
|
|
1272
|
-
// rep: "channel/options.json"
|
|
1273
|
-
// });
|
|
1274
|
-
//
|
|
1275
|
-
// try {
|
|
1276
|
-
// let offerings = await got(channelFormatsUrl);
|
|
1277
|
-
// status.offerings = JSON.parse(offerings.body);
|
|
1278
|
-
// } catch(error) {
|
|
1279
|
-
// console.log(error);
|
|
1280
|
-
// status.offerings_error = "Failed to retrieve channel offerings";
|
|
1281
|
-
// }
|
|
1282
|
-
//
|
|
1283
|
-
// status.playout = await ChannelPlayout({client, libraryId: conf.libraryId, objectId: conf.objectId});
|
|
1284
|
-
//
|
|
1285
|
-
// } catch(error) {
|
|
1286
|
-
// console.error(error);
|
|
1287
|
-
// }
|
|
1288
|
-
//
|
|
1289
|
-
// return status;
|
|
1290
|
-
// };
|
|
1291
|
-
|
|
1292
|
-
/*
|
|
1293
|
-
* Performs client-side playout operations - open the channel, read offerings,
|
|
1294
|
-
* retrieve playlist and one video init segment.
|
|
1295
|
-
*/
|
|
1296
|
-
// const ChannelPlayout = async({client, libraryId, objectId}) => {
|
|
1297
|
-
//
|
|
1298
|
-
// let playout = {};
|
|
1299
|
-
//
|
|
1300
|
-
// const offerings = await client.AvailableOfferings({
|
|
1301
|
-
// libraryId,
|
|
1302
|
-
// objectId,
|
|
1303
|
-
// handler: "channel",
|
|
1304
|
-
// linkPath: "/public/asset_metadata/offerings"
|
|
1305
|
-
// });
|
|
1306
|
-
//
|
|
1307
|
-
// // Choosing offering 'default'
|
|
1308
|
-
// let offering = offerings.default;
|
|
1309
|
-
//
|
|
1310
|
-
// const playoutOptions = await client.PlayoutOptions({
|
|
1311
|
-
// libraryId,
|
|
1312
|
-
// objectId,
|
|
1313
|
-
// offeringURI: offering.uri
|
|
1314
|
-
// });
|
|
1315
|
-
//
|
|
1316
|
-
// // Retrieve master playlist
|
|
1317
|
-
// let masterPlaylistUrl = playoutOptions["hls"]["playoutMethods"]["fairplay"]["playoutUrl"];
|
|
1318
|
-
// playout.master_playlist_url = masterPlaylistUrl;
|
|
1319
|
-
// try {
|
|
1320
|
-
// //let masterPlaylist = await got(masterPlaylistUrl);
|
|
1321
|
-
// playout.master_playlist = "success";
|
|
1322
|
-
// } catch(error) {
|
|
1323
|
-
// playout.master_playlist = "fail";
|
|
1324
|
-
// }
|
|
1325
|
-
//
|
|
1326
|
-
// let url = new URL(masterPlaylistUrl);
|
|
1327
|
-
// let p = url.pathname.split("/");
|
|
1328
|
-
//
|
|
1329
|
-
// // Retrieve media playlist
|
|
1330
|
-
// p[p.length - 1] = "video/720@14000000/live.m3u8";
|
|
1331
|
-
// let pathMediaPlaylist = p.join("/");
|
|
1332
|
-
// url.pathname = pathMediaPlaylist;
|
|
1333
|
-
// let mediaPlaylistUrl = url.toString();
|
|
1334
|
-
// playout.media_playlist_url = mediaPlaylistUrl;
|
|
1335
|
-
// let mediaPlaylist;
|
|
1336
|
-
// try {
|
|
1337
|
-
// mediaPlaylist = await got(mediaPlaylistUrl);
|
|
1338
|
-
// playout.media_playlist = "success";
|
|
1339
|
-
// } catch(error) {
|
|
1340
|
-
// playout.media_playlist = "fail";
|
|
1341
|
-
// }
|
|
1342
|
-
//
|
|
1343
|
-
// // Retrieve init segment
|
|
1344
|
-
// var regex = new RegExp("^#EXT-X-MAP:URI=\"init.m4s.(.*)\"$", "m");
|
|
1345
|
-
// var match = regex.exec(mediaPlaylist.body);
|
|
1346
|
-
// let initQueryParams;
|
|
1347
|
-
// if(match) {
|
|
1348
|
-
// initQueryParams = match[1];
|
|
1349
|
-
// }
|
|
1350
|
-
//
|
|
1351
|
-
// p[p.length - 1] = "video/720@14000000/init.m4s";
|
|
1352
|
-
// let pathInit = p.join("/");
|
|
1353
|
-
// url.pathname = pathInit;
|
|
1354
|
-
// url.search=initQueryParams;
|
|
1355
|
-
// let initUrl = url.toString();
|
|
1356
|
-
// playout.init_segment_url = initUrl;
|
|
1357
|
-
// /*
|
|
1358
|
-
// try {
|
|
1359
|
-
// let initSegment = await got(initUrl);
|
|
1360
|
-
// playout.init_segment = "success"
|
|
1361
|
-
// } catch(error) {
|
|
1362
|
-
// playout.init_segment = "fail";
|
|
1363
|
-
// }
|
|
1364
|
-
// */
|
|
1365
|
-
// return playout;
|
|
1366
|
-
// };
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
// const Summary = async ({client}) => {
|
|
1370
|
-
//
|
|
1371
|
-
// let summary = {};
|
|
1372
|
-
//
|
|
1373
|
-
// try {
|
|
1374
|
-
// for (const [key] of Object.entries(streams)) {
|
|
1375
|
-
// conf = streams[key];
|
|
1376
|
-
// summary[key] = await Status({client, name: key, stopLro: false});
|
|
1377
|
-
// }
|
|
1378
|
-
//
|
|
1379
|
-
// } catch(error) {
|
|
1380
|
-
// console.error(error);
|
|
1381
|
-
// }
|
|
1382
|
-
// return summary;
|
|
1383
|
-
// };
|
|
1384
|
-
|
|
1385
|
-
// const ChannelSummary = async ({client}) => {
|
|
1386
|
-
//
|
|
1387
|
-
// let summary = {};
|
|
1388
|
-
//
|
|
1389
|
-
// try {
|
|
1390
|
-
// for (const [key] of Object.entries(channels)) {
|
|
1391
|
-
// conf = channels[key];
|
|
1392
|
-
// summary[key] = await ChannelStatus({client, name: key});
|
|
1393
|
-
// }
|
|
1394
|
-
//
|
|
1395
|
-
// } catch(error) {
|
|
1396
|
-
// console.error(error);
|
|
1397
|
-
// }
|
|
1398
|
-
// return summary;
|
|
1399
|
-
// };
|
|
1400
|
-
|
|
1401
|
-
// const ConfigStreamRebroadcast = async () => {
|
|
1402
|
-
//
|
|
1403
|
-
// const t = 1619850660;
|
|
1404
|
-
//
|
|
1405
|
-
// try {
|
|
1406
|
-
// let client;
|
|
1407
|
-
// if(conf.clientConf.configUrl) {
|
|
1408
|
-
// client = await ElvClient.FromConfigurationUrl({
|
|
1409
|
-
// configUrl: conf.clientConf.configUrl
|
|
1410
|
-
// });
|
|
1411
|
-
// } else {
|
|
1412
|
-
// client = new ElvClient(conf.clientConf);
|
|
1413
|
-
// }
|
|
1414
|
-
// const wallet = client.GenerateWallet();
|
|
1415
|
-
// const signer = wallet.AddAccount({ privateKey: conf.signerPrivateKey });
|
|
1416
|
-
// client.SetSigner({ signer });
|
|
1417
|
-
// const fabURI = client.fabricURIs[0];
|
|
1418
|
-
// console.log("Fabric URI: " + fabURI);
|
|
1419
|
-
// const ethURI = client.ethereumURIs[0];
|
|
1420
|
-
// console.log("Ethereum URI: " + ethURI);
|
|
1421
|
-
//
|
|
1422
|
-
// client.ToggleLogging(false);
|
|
1423
|
-
//
|
|
1424
|
-
// let mainMeta = await client.ContentObjectMetadata({
|
|
1425
|
-
// libraryId: conf.libraryId,
|
|
1426
|
-
// objectId: conf.objectId
|
|
1427
|
-
// });
|
|
1428
|
-
// console.log("Main meta:", mainMeta);
|
|
1429
|
-
//
|
|
1430
|
-
// edgeWriteToken = mainMeta.edge_write_token;
|
|
1431
|
-
// console.log("Edge: ", edgeWriteToken);
|
|
1432
|
-
//
|
|
1433
|
-
// let edgeMeta = await client.ContentObjectMetadata({
|
|
1434
|
-
// libraryId: conf.libraryId,
|
|
1435
|
-
// objectId: conf.objectId,
|
|
1436
|
-
// writeToken: edgeWriteToken
|
|
1437
|
-
// });
|
|
1438
|
-
// console.log("Edge meta:", edgeMeta);
|
|
1439
|
-
//
|
|
1440
|
-
// //console.log("CONFIG: ", edgeMeta.live_recording_parameters.live_playout_config);
|
|
1441
|
-
// console.log("recording_start_time: ", edgeMeta.recording_start_time);
|
|
1442
|
-
// console.log("recording_stop_time: ", edgeMeta.recording_stop_time);
|
|
1443
|
-
//
|
|
1444
|
-
// // Set rebroadcast start
|
|
1445
|
-
// edgeMeta.live_recording_parameters.live_playout_config.rebroadcast_start_time_sec_epoch = t;
|
|
1446
|
-
//
|
|
1447
|
-
// if(PRINT_DEBUG) console.log("MergeMetadata", conf.libraryId, conf.objectId, writeToken);
|
|
1448
|
-
// await client.MergeMetadata({
|
|
1449
|
-
// libraryId: conf.libraryId,
|
|
1450
|
-
// objectId: conf.objectId,
|
|
1451
|
-
// writeToken: edgeWriteToken,
|
|
1452
|
-
// metadata: {
|
|
1453
|
-
// "live_recording_parameters": {
|
|
1454
|
-
// "live_playout_config" : edgeMeta.live_recording_parameters.live_playout_config
|
|
1455
|
-
// }
|
|
1456
|
-
// }
|
|
1457
|
-
// });
|
|
1458
|
-
//
|
|
1459
|
-
// } catch(error) {
|
|
1460
|
-
// console.error(error);
|
|
1461
|
-
// }
|
|
1462
|
-
// };
|
|
1463
|
-
|
|
1464
|
-
// async function EnsureAll() {
|
|
1465
|
-
// client = await StatusPrep({name: null});
|
|
1466
|
-
// let summary = await Summary({client});
|
|
1467
|
-
//
|
|
1468
|
-
// var res = {
|
|
1469
|
-
// running: 0,
|
|
1470
|
-
// stalled: 0,
|
|
1471
|
-
// terminated: 0
|
|
1472
|
-
// };
|
|
1473
|
-
//
|
|
1474
|
-
// try {
|
|
1475
|
-
// for (const [key, value] of Object.entries(summary)) {
|
|
1476
|
-
// if(value.state === "stalled") {
|
|
1477
|
-
// console.log("Stream stalled: ", key, " - restarting");
|
|
1478
|
-
// console.log("todo ...");
|
|
1479
|
-
// }
|
|
1480
|
-
// res[value.state] = res[value.state] + 1;
|
|
1481
|
-
// }
|
|
1482
|
-
// } catch(error) {
|
|
1483
|
-
// console.error(error);
|
|
1484
|
-
// }
|
|
1485
|
-
//
|
|
1486
|
-
// return res;
|
|
1487
|
-
// }
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
/*
|
|
1491
|
-
* Original Run() function - kept for reference
|
|
1421
|
+
/**
|
|
1422
|
+
* Deactivate the stream
|
|
1423
|
+
*
|
|
1424
|
+
* @methodGroup Live Stream
|
|
1425
|
+
* @namedParams
|
|
1426
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
1427
|
+
*
|
|
1428
|
+
* @return {Promise<Object>} - The status response for the stream
|
|
1492
1429
|
*/
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
//
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
//
|
|
1523
|
-
|
|
1524
|
-
//
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
//
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1430
|
+
exports.StreamDeactivate = async function({name}) {
|
|
1431
|
+
try {
|
|
1432
|
+
let conf = await this.LoadConf({name});
|
|
1433
|
+
|
|
1434
|
+
let {objectId} = conf;
|
|
1435
|
+
let libraryId = await this.ContentObjectLibraryId({objectId});
|
|
1436
|
+
|
|
1437
|
+
let mainMeta = await this.ContentObjectMetadata({
|
|
1438
|
+
libraryId,
|
|
1439
|
+
objectId
|
|
1440
|
+
});
|
|
1441
|
+
let status = await this.StreamStatus({name});
|
|
1442
|
+
|
|
1443
|
+
if(!mainMeta.live_recording) {
|
|
1444
|
+
return {
|
|
1445
|
+
state: status.state,
|
|
1446
|
+
error: "Stream must be configured before deactivating"
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Return error if the LRO is running
|
|
1451
|
+
if(status.state !== "stopped") {
|
|
1452
|
+
return {
|
|
1453
|
+
state: status.state,
|
|
1454
|
+
error: "Stream must be stopped before deactivating"
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
let fabURI = mainMeta.live_recording.fabric_config.ingress_node_api;
|
|
1459
|
+
// Support both hostname and URL ingress_node_api
|
|
1460
|
+
if(!fabURI.startsWith("http")) {
|
|
1461
|
+
// Assume https
|
|
1462
|
+
fabURI = "https://" + fabURI;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
this.SetNodes({fabricURIs: [fabURI]});
|
|
1466
|
+
|
|
1467
|
+
let edgeWriteToken = mainMeta.live_recording.fabric_config.edge_write_token;
|
|
1468
|
+
|
|
1469
|
+
if(edgeWriteToken === undefined || edgeWriteToken === "") {
|
|
1470
|
+
return {
|
|
1471
|
+
state: "inactive",
|
|
1472
|
+
error: "stream is already inactive"
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
let edgeMeta = await this.ContentObjectMetadata({
|
|
1476
|
+
libraryId,
|
|
1477
|
+
objectId,
|
|
1478
|
+
writeToken: edgeWriteToken
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
// Set stop time
|
|
1482
|
+
edgeMeta.recording_stop_time = Math.floor(new Date().getTime() / 1000);
|
|
1483
|
+
const newState = "inactive";
|
|
1484
|
+
|
|
1485
|
+
edgeMeta.live_recording.status = {
|
|
1486
|
+
state: newState,
|
|
1487
|
+
recording_stop_time: edgeMeta.recording_stop_time
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
edgeMeta.live_recording.fabric_config.edge_write_token = "";
|
|
1491
|
+
|
|
1492
|
+
await this.ReplaceMetadata({
|
|
1493
|
+
libraryId,
|
|
1494
|
+
objectId,
|
|
1495
|
+
writeToken: edgeWriteToken,
|
|
1496
|
+
metadata: edgeMeta
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
let fin = await this.FinalizeContentObject({
|
|
1500
|
+
libraryId,
|
|
1501
|
+
objectId,
|
|
1502
|
+
writeToken: edgeWriteToken,
|
|
1503
|
+
commitMessage: "Deactivate stream"
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
return {
|
|
1507
|
+
reference_url: status.reference_url,
|
|
1508
|
+
fin,
|
|
1509
|
+
name,
|
|
1510
|
+
edge_write_token: edgeWriteToken,
|
|
1511
|
+
state: newState
|
|
1512
|
+
};
|
|
1513
|
+
} catch(error) {
|
|
1514
|
+
console.error(error);
|
|
1515
|
+
}
|
|
1516
|
+
};
|