@eluvio/elv-client-js 4.2.23 → 4.2.25
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 +1 -1
- package/dist/ElvClient-node-min.js +1 -1
- package/dist/ElvFrameClient-min.js +1 -1
- package/dist/ElvPermissionsClient-min.js +1 -1
- package/dist/ElvWalletClient-min.js +1 -1
- package/dist/ElvWalletClient-node-min.js +1 -1
- package/dist/src/AuthorizationClient.js +7 -12
- package/dist/src/ContentObjectAudit.js +2 -1
- package/dist/src/ContentObjectVerification.js +281 -0
- package/dist/src/Crypto.js +33 -5
- package/dist/src/ElvClient.js +9 -10
- package/dist/src/FrameClient.js +1 -1
- package/dist/src/HttpClient.js +90 -47
- package/dist/src/NetworkUrls.js +8 -0
- package/dist/src/RemoteSigner.js +170 -70
- package/dist/src/abr_profiles/abr_profile_live_drm.js +0 -10
- package/dist/src/client/ContentAccess.js +1165 -1149
- package/dist/src/client/ContentManagement.js +2 -2
- package/dist/src/client/Contracts.js +2 -2
- package/dist/src/client/LiveConf.js +171 -84
- package/dist/src/client/LiveStream.js +5649 -2155
- package/dist/src/client/NTP.js +172 -77
- package/dist/src/live_recording_config_profiles/live_recording_config_default.js +45 -0
- package/package.json +1 -1
- package/src/FrameClient.js +2 -0
- package/src/client/Contracts.js +1 -1
- package/src/client/LiveConf.js +46 -27
- package/src/client/LiveStream.js +337 -92
- package/utilities/LiveOutputs.js +9 -4
package/src/client/LiveStream.js
CHANGED
|
@@ -226,7 +226,13 @@ exports.StreamCreate = async function({
|
|
|
226
226
|
]
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
let tenantContentAdminGroup;
|
|
230
|
+
try {
|
|
231
|
+
tenantContentAdminGroup = await this.ContentAdminGroup({tenantContractId: tenantId}) || [];
|
|
232
|
+
} catch(error) {
|
|
233
|
+
console.error("Unable to load content admin group", error);
|
|
234
|
+
}
|
|
235
|
+
|
|
230
236
|
adminGroups = adminGroups.concat(tenantContentAdminGroup ?? []);
|
|
231
237
|
|
|
232
238
|
contentType = tenantMeta.content_types?.live_stream;
|
|
@@ -1081,14 +1087,13 @@ exports.StreamStatus = async function({name, showParams=false, writeToken}) {
|
|
|
1081
1087
|
}
|
|
1082
1088
|
|
|
1083
1089
|
// Convert LRO 'state' to desired 'state'
|
|
1084
|
-
if(state
|
|
1085
|
-
state = "starting"; // The LRO returns 'running' even if the source hasn't connected
|
|
1086
|
-
} else if(state == "terminated") {
|
|
1090
|
+
if(state == "terminated") {
|
|
1087
1091
|
state = "stopped"; // The LRO reports 'terminated' which for the recording means 'stopped'
|
|
1088
1092
|
}
|
|
1089
1093
|
status.state = state;
|
|
1090
1094
|
|
|
1091
|
-
|
|
1095
|
+
const retrievePlayoutOptions = false; // PENDING(SS) make this a function argument if useful for the API
|
|
1096
|
+
if(retrievePlayoutOptions && state === "running") {
|
|
1092
1097
|
let playoutUrls = {};
|
|
1093
1098
|
let playout_options;
|
|
1094
1099
|
|
|
@@ -2667,6 +2672,13 @@ exports.StreamConfig = async function({
|
|
|
2667
2672
|
});
|
|
2668
2673
|
}
|
|
2669
2674
|
|
|
2675
|
+
const currentLiveRecordingMeta = await this.ContentObjectMetadata({
|
|
2676
|
+
libraryId,
|
|
2677
|
+
objectId,
|
|
2678
|
+
writeToken,
|
|
2679
|
+
metadataSubtree: "/live_recording"
|
|
2680
|
+
});
|
|
2681
|
+
|
|
2670
2682
|
// Create live recording config
|
|
2671
2683
|
const liveConf = new LiveConf({
|
|
2672
2684
|
url: liveRecordingConfigProfile.url,
|
|
@@ -2675,7 +2687,8 @@ exports.StreamConfig = async function({
|
|
|
2675
2687
|
nodeUrl: endpoint,
|
|
2676
2688
|
includeAVSegDurations: false,
|
|
2677
2689
|
overwriteOriginUrl: false,
|
|
2678
|
-
syncAudioToVideo: true
|
|
2690
|
+
syncAudioToVideo: true,
|
|
2691
|
+
liveRecordingMeta: currentLiveRecordingMeta
|
|
2679
2692
|
});
|
|
2680
2693
|
|
|
2681
2694
|
const liveRecordingConfigMeta = liveConf.generateLiveConf({
|
|
@@ -3329,38 +3342,153 @@ exports.AuditStream = async function({objectId, versionHash, salt, samples, auth
|
|
|
3329
3342
|
};
|
|
3330
3343
|
|
|
3331
3344
|
/**
|
|
3345
|
+
* Live output delivery protocols. Exactly one delivery block is present on a LiveOutput.
|
|
3346
|
+
*
|
|
3347
|
+
* @typedef {("srt_pull"|"srt_push"|"rtp"|"udp")} OutputDeliveryType
|
|
3348
|
+
*/
|
|
3349
|
+
|
|
3350
|
+
/**
|
|
3351
|
+
* Input stream configuration for a live output.
|
|
3352
|
+
*
|
|
3353
|
+
* @typedef {Object} OutputInput
|
|
3354
|
+
* @property {string=} stream - Object ID of the input stream, e.g. "iq__AAA" (null to disconnect)
|
|
3355
|
+
* @property {string=} offering - Offering key of the input stream to use (optional; defaults to the stream's default offering)
|
|
3356
|
+
*/
|
|
3357
|
+
|
|
3358
|
+
/**
|
|
3359
|
+
* Configuration options for an SRT connection. Durations are strings with time units
|
|
3360
|
+
* (e.g. "200ms", "1s500ms") or a floating-point number of seconds (e.g. 0.2, 1.5).
|
|
3361
|
+
*
|
|
3362
|
+
* @typedef {Object} SrtConnectionConfig
|
|
3363
|
+
* @property {string=} connection_timeout - Connection timeout (SRTO_CONNTIMEO)
|
|
3364
|
+
* @property {boolean=} enforced_encryption - Reject connection if parties set different passphrase (SRTO_ENFORCEDENCRYPTION)
|
|
3365
|
+
* @property {number=} fc - Flow control window size in packets (SRTO_FC)
|
|
3366
|
+
* @property {number=} input_bw - Input bandwidth in bytes per second (SRTO_INPUTBW)
|
|
3367
|
+
* @property {number=} iptos - IP socket "type of service" (SRTO_IPTOS)
|
|
3368
|
+
* @property {number=} ipttl - IP socket "time to live" option (SRTO_IPTTL)
|
|
3369
|
+
* @property {number=} km_pre_announce - Duration of stream encryption key switchover in packets (SRTO_KMPREANNOUNCE)
|
|
3370
|
+
* @property {number=} km_refresh_rate - Stream encryption key refresh rate in packets (SRTO_KMREFRESHRATE)
|
|
3371
|
+
* @property {string=} latency - Maximum accepted transmission latency (SRTO_LATENCY)
|
|
3372
|
+
* @property {number=} loss_max_ttl - Packet reorder tolerance (SRTO_LOSSMAXTTL)
|
|
3373
|
+
* @property {number=} max_bw - Bandwidth limit in bytes per second (SRTO_MAXBW)
|
|
3374
|
+
* @property {boolean=} message_api - Enable SRT message mode (SRTO_MESSAGEAPI)
|
|
3375
|
+
* @property {number=} min_input_bw - Minimum input bandwidth (SRTO_MININPUTBW)
|
|
3376
|
+
* @property {number=} min_version - Minimum SRT library version of a peer, encoded (major<<16)|(minor<<8)|patch (SRTO_MINVERSION)
|
|
3377
|
+
* @property {number=} mss - MTU size (SRTO_MSS)
|
|
3378
|
+
* @property {number=} overhead_bw - Limit bandwidth overhead in percent (SRTO_OHEADBW)
|
|
3379
|
+
* @property {number=} payload_size - Maximum payload size in bytes (SRTO_PAYLOADSIZE)
|
|
3380
|
+
* @property {number=} pb_keylen - Length of encryption key in bytes (SRTO_PBKEYLEN): 0=disabled, 16=AES-128, 24=AES-192, 32=AES-256
|
|
3381
|
+
* @property {string=} peer_idle_timeout - Peer idle timeout (SRTO_PEERIDLETIMEO)
|
|
3382
|
+
* @property {string=} peer_latency - Minimum receiver latency to be requested by sender (SRTO_PEERLATENCY)
|
|
3383
|
+
* @property {number=} receiver_buffer_size - Receiver buffer size in bytes (SRTO_RCVBUF)
|
|
3384
|
+
* @property {string=} receiver_latency - Receiver-side latency (SRTO_RCVLATENCY)
|
|
3385
|
+
* @property {number=} send_buffer_size - Sender buffer size in bytes (SRTO_SNDBUF)
|
|
3386
|
+
* @property {string=} send_drop_delay - Sender's delay before dropping packets (SRTO_SNDDROPDELAY)
|
|
3387
|
+
* @property {boolean=} allow_peer_ip_change - Allow a new IP address to start sending data on an existing socket id
|
|
3388
|
+
*/
|
|
3389
|
+
|
|
3390
|
+
/**
|
|
3391
|
+
* SRT push delivery: the fabric node designated by node_id connects to the given SRT URL in caller
|
|
3392
|
+
* mode and pushes the live stream to it.
|
|
3393
|
+
*
|
|
3394
|
+
* @typedef {Object} OutputSrtPush
|
|
3395
|
+
* @property {string} url - Destination SRT URL (e.g. "srt://192.0.2.10:9000")
|
|
3396
|
+
* @property {string=} node_id - ID of the fabric node that pushes to the destination URL, e.g. "inodAAA" (resolved from elvgeo if omitted)
|
|
3397
|
+
* @property {string=} elvgeo - Geo region used to select a node; resolved to node_id on create
|
|
3398
|
+
* @property {string=} passphrase - Optional passphrase for SRT encryption. Must be at least 10 characters if specified.
|
|
3399
|
+
* @property {boolean=} strip_rtp - If true, strip RTP encapsulation from the source stream before pushing
|
|
3400
|
+
* @property {SrtConnectionConfig=} connection - Configuration options for an SRT connection
|
|
3401
|
+
*/
|
|
3402
|
+
|
|
3403
|
+
/**
|
|
3404
|
+
* SRT pull delivery: an external client pulls from a synthetic SRT URL exposed by the fabric.
|
|
3405
|
+
*
|
|
3406
|
+
* @typedef {Object} OutputSrtPull
|
|
3407
|
+
* @property {Array<string>=} elvgeos - Geo region(s) used to select node(s); resolved to node_ids on create
|
|
3408
|
+
* @property {Array<string>=} node_ids - Egress node ID(s), e.g. ["inodAAA"] (resolved from elvgeos if omitted)
|
|
3409
|
+
* @property {string=} passphrase - SRT passphrase for encrypted delivery
|
|
3410
|
+
* @property {boolean=} strip_rtp - Whether to strip RTP headers
|
|
3411
|
+
* @property {Array<string>=} urls - Fabric-generated SRT pull URL(s) (generated by the server on PUT; not set by caller)
|
|
3412
|
+
* @property {SrtConnectionConfig=} connection - Configuration options for an SRT connection
|
|
3413
|
+
*/
|
|
3414
|
+
|
|
3415
|
+
/**
|
|
3416
|
+
* RTP delivery: the fabric node designated by node_id pushes the live stream as MPEG-TS over RTP
|
|
3417
|
+
* to the specified URL.
|
|
3418
|
+
*
|
|
3419
|
+
* @typedef {Object} OutputRtp
|
|
3420
|
+
* @property {string} url - Destination RTP URL (e.g. "rtp://239.0.0.1:5004")
|
|
3421
|
+
* @property {string=} node_id - ID of the fabric node that pushes to the destination URL, e.g. "inodAAA" (resolved from elvgeo if omitted)
|
|
3422
|
+
* @property {string=} elvgeo - Geo region used to select a node; resolved to node_id on create
|
|
3423
|
+
*/
|
|
3424
|
+
|
|
3425
|
+
/**
|
|
3426
|
+
* UDP delivery: the fabric node designated by node_id pushes the live stream as raw MPEG-TS over UDP
|
|
3427
|
+
* to the specified URL.
|
|
3428
|
+
*
|
|
3429
|
+
* @typedef {Object} OutputUdp
|
|
3430
|
+
* @property {string} url - Destination UDP URL (e.g. "udp://239.0.0.1:1234")
|
|
3431
|
+
* @property {string=} node_id - ID of the fabric node that pushes to the destination URL, e.g. "inodAAA" (resolved from elvgeo if omitted)
|
|
3432
|
+
* @property {string=} elvgeo - Geo region used to select a node; resolved to node_id on create
|
|
3433
|
+
*/
|
|
3434
|
+
|
|
3435
|
+
/**
|
|
3436
|
+
* A single live output. Keyed by output ID in the outputs map. Exactly one of
|
|
3437
|
+
* srt_push / srt_pull / rtp / udp is present and selects the delivery protocol.
|
|
3438
|
+
*
|
|
3332
3439
|
* @typedef {Object} LiveOutput
|
|
3333
3440
|
* @property {boolean=} enabled - Whether the output is enabled
|
|
3334
3441
|
* @property {string=} name - Display name for the output
|
|
3335
3442
|
* @property {string=} description - Description of the output
|
|
3336
3443
|
* @property {string=} external_id - External identifier for the output
|
|
3337
|
-
* @property {
|
|
3338
|
-
* @property {
|
|
3339
|
-
* @property {
|
|
3340
|
-
* @property {
|
|
3341
|
-
* @property {
|
|
3342
|
-
* @property {string=} srt_pull.passphrase - SRT passphrase for encrypted delivery
|
|
3343
|
-
* @property {boolean=} srt_pull.strip_rtp - Whether to strip RTP headers
|
|
3344
|
-
* @property {Object=} srt_pull.connection - Additional SRT connection configuration
|
|
3345
|
-
* @property {Array<string>=} srt_pull.urls - SRT URLs (returned by server, not set by caller)
|
|
3444
|
+
* @property {OutputInput=} input - Input stream configuration
|
|
3445
|
+
* @property {OutputSrtPush=} srt_push - SRT push delivery configuration
|
|
3446
|
+
* @property {OutputSrtPull=} srt_pull - SRT pull delivery configuration
|
|
3447
|
+
* @property {OutputRtp=} rtp - RTP delivery configuration
|
|
3448
|
+
* @property {OutputUdp=} udp - UDP delivery configuration
|
|
3346
3449
|
*/
|
|
3347
3450
|
|
|
3348
|
-
|
|
3451
|
+
/**
|
|
3452
|
+
* Delivery envelope accepted by OutputsCreate. The `type` selects the protocol and `settings`
|
|
3453
|
+
* carries the matching delivery block (OutputSrtPush / OutputSrtPull / OutputRtp / OutputUdp).
|
|
3454
|
+
*
|
|
3455
|
+
* @typedef {Object} OutputDelivery
|
|
3456
|
+
* @property {OutputDeliveryType} type - Delivery protocol
|
|
3457
|
+
* @property {(OutputSrtPush|OutputSrtPull|OutputRtp|OutputUdp)} settings - Protocol-specific delivery settings
|
|
3458
|
+
*/
|
|
3459
|
+
|
|
3460
|
+
const DELIVERY_PROTOCOLS = ["srt_pull", "srt_push", "rtp", "udp"];
|
|
3461
|
+
|
|
3462
|
+
const OutputDeliverySettings = (value) => {
|
|
3463
|
+
const type = DELIVERY_PROTOCOLS.find(protocol => value && value[protocol]);
|
|
3464
|
+
return type ? value[type] : undefined;
|
|
3465
|
+
};
|
|
3466
|
+
|
|
3467
|
+
// Egress node ID, normalizing srt_pull's node_ids array and the singular node_id of other protocols.
|
|
3468
|
+
const OutputDeliveryNodeId = (value) => {
|
|
3469
|
+
const settings = OutputDeliverySettings(value);
|
|
3470
|
+
return settings?.node_id ?? settings?.node_ids?.[0];
|
|
3471
|
+
};
|
|
3472
|
+
|
|
3473
|
+
// Rewrite the host of fabric-generated srt_pull URLs to the egress node's fabric hostname.
|
|
3349
3474
|
// Necessary because the backend API doesn't return the proper SRT URLs currently.
|
|
3475
|
+
const RewriteSrtPullUrls = (value, fabricUrl) => {
|
|
3476
|
+
if(!fabricUrl || !value.srt_pull?.urls) { return; }
|
|
3477
|
+
|
|
3478
|
+
const egressHost = new URL(fabricUrl).hostname;
|
|
3479
|
+
value.srt_pull.urls = value.srt_pull.urls.map(url =>
|
|
3480
|
+
url.replace(/^srt:\/\/[^:/?]+/, `srt://${egressHost}`)
|
|
3481
|
+
);
|
|
3482
|
+
};
|
|
3483
|
+
|
|
3484
|
+
// Resolve egress node and replace SRT URL with the egress endpoint hostname.
|
|
3350
3485
|
exports.OutputsResolveSrtPullUrls = async function({value}) {
|
|
3351
3486
|
const nodeId = value.srt_pull?.node_ids?.[0];
|
|
3352
3487
|
if(!nodeId) { return value; }
|
|
3353
3488
|
|
|
3354
3489
|
const nodes = await this.SpaceNodes({matchNodeId: nodeId});
|
|
3355
3490
|
const fabricUrl = nodes?.[0]?.services?.fabric_api?.urls?.[0];
|
|
3356
|
-
|
|
3357
|
-
const egressHost = new URL(fabricUrl).hostname;
|
|
3358
|
-
if(value.srt_pull?.urls) {
|
|
3359
|
-
value.srt_pull.urls = value.srt_pull.urls.map(url =>
|
|
3360
|
-
url.replace(/^srt:\/\/[^:/?]+/, `srt://${egressHost}`)
|
|
3361
|
-
);
|
|
3362
|
-
}
|
|
3363
|
-
}
|
|
3491
|
+
RewriteSrtPullUrls(value, fabricUrl);
|
|
3364
3492
|
|
|
3365
3493
|
return value;
|
|
3366
3494
|
};
|
|
@@ -3398,33 +3526,85 @@ exports.OutputsList = async function({libraryId, objectId, includeState=true}) {
|
|
|
3398
3526
|
restore();
|
|
3399
3527
|
}
|
|
3400
3528
|
|
|
3401
|
-
|
|
3402
|
-
|
|
3529
|
+
// Enrich each output's input stream name/status concurrently (read-only, no routing changes)
|
|
3530
|
+
await this.utils.LimitedMap(
|
|
3531
|
+
10,
|
|
3532
|
+
Object.values(outputs),
|
|
3533
|
+
async value => {
|
|
3534
|
+
const streamId = value.input?.stream;
|
|
3535
|
+
if(!streamId) { return; }
|
|
3403
3536
|
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3537
|
+
const [streamLibraryId, streamStatus] = await Promise.all([
|
|
3538
|
+
this.ContentObjectLibraryId({objectId: streamId}),
|
|
3539
|
+
this.StreamStatus({name: streamId})
|
|
3540
|
+
]);
|
|
3541
|
+
|
|
3542
|
+
value.input.name = await this.ContentObjectMetadata({
|
|
3543
|
+
libraryId: streamLibraryId,
|
|
3407
3544
|
objectId: streamId,
|
|
3408
3545
|
metadataSubtree: "/public/name",
|
|
3409
3546
|
});
|
|
3547
|
+
value.input.status = streamStatus?.state;
|
|
3548
|
+
}
|
|
3549
|
+
);
|
|
3550
|
+
|
|
3551
|
+
if(!includeState) {
|
|
3552
|
+
return outputs;
|
|
3553
|
+
}
|
|
3410
3554
|
|
|
3411
|
-
|
|
3555
|
+
// Resolve each distinct egress node's fabric URL once (concurrent, read-only)
|
|
3556
|
+
const nodeIds = [...new Set(
|
|
3557
|
+
Object.values(outputs).map(value => OutputDeliveryNodeId(value)).filter(Boolean)
|
|
3558
|
+
)];
|
|
3412
3559
|
|
|
3413
|
-
|
|
3414
|
-
|
|
3560
|
+
const nodeUrls = {};
|
|
3561
|
+
await this.utils.LimitedMap(10, nodeIds, async nodeId => {
|
|
3562
|
+
const nodes = await this.SpaceNodes({matchNodeId: nodeId});
|
|
3563
|
+
const fabricUrl = nodes?.[0]?.services?.fabric_api?.urls?.[0];
|
|
3564
|
+
if(fabricUrl) { nodeUrls[nodeId] = fabricUrl; }
|
|
3565
|
+
});
|
|
3566
|
+
|
|
3567
|
+
// Rewrite fabric-generated srt_pull URLs to the egress host, and group outputs by node
|
|
3568
|
+
const outputsByNode = {};
|
|
3569
|
+
for(const [key, value] of Object.entries(outputs)) {
|
|
3570
|
+
const nodeId = OutputDeliveryNodeId(value);
|
|
3571
|
+
const fabricUrl = nodeId && nodeUrls[nodeId];
|
|
3572
|
+
|
|
3573
|
+
RewriteSrtPullUrls(value, fabricUrl);
|
|
3574
|
+
|
|
3575
|
+
if(!fabricUrl) {
|
|
3576
|
+
value.state = {};
|
|
3577
|
+
continue;
|
|
3415
3578
|
}
|
|
3579
|
+
(outputsByNode[nodeId] = outputsByNode[nodeId] || []).push(key);
|
|
3580
|
+
}
|
|
3416
3581
|
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3582
|
+
// Fetch state per node: route to the node once, fetch its outputs concurrently, then restore
|
|
3583
|
+
const savedURIs = [...this.fabricURIs];
|
|
3584
|
+
try {
|
|
3585
|
+
for(const [nodeId, outputIds] of Object.entries(outputsByNode)) {
|
|
3586
|
+
this.SetNodes({fabricURIs: [nodeUrls[nodeId]]});
|
|
3587
|
+
|
|
3588
|
+
await this.utils.LimitedMap(10, outputIds, async outputId => {
|
|
3589
|
+
try {
|
|
3590
|
+
outputs[outputId].state = await this.CallBitcodeMethod({
|
|
3591
|
+
libraryId,
|
|
3592
|
+
objectId,
|
|
3593
|
+
method: UrlJoin("live", "outputs", outputId, "state"),
|
|
3594
|
+
queryParams: {
|
|
3595
|
+
"client_stats": 1,
|
|
3596
|
+
"srt_stats": 1
|
|
3597
|
+
},
|
|
3598
|
+
constant: true
|
|
3599
|
+
});
|
|
3600
|
+
} catch(error) {
|
|
3601
|
+
this.Log(`Failed to retrieve state for output ${outputId}: ${error.message}`, true);
|
|
3602
|
+
outputs[outputId].state = {};
|
|
3603
|
+
}
|
|
3604
|
+
});
|
|
3427
3605
|
}
|
|
3606
|
+
} finally {
|
|
3607
|
+
this.SetNodes({fabricURIs: savedURIs});
|
|
3428
3608
|
}
|
|
3429
3609
|
|
|
3430
3610
|
return outputs;
|
|
@@ -3489,7 +3669,7 @@ exports.OutputsListItem = async function({libraryId, objectId, outputId, include
|
|
|
3489
3669
|
|
|
3490
3670
|
if(includeState) {
|
|
3491
3671
|
try {
|
|
3492
|
-
const nodeId = value
|
|
3672
|
+
const nodeId = OutputDeliveryNodeId(value);
|
|
3493
3673
|
const result = await this.OutputsState({outputId, objectId, libraryId, nodeId, includeState: true});
|
|
3494
3674
|
value.state = result.state;
|
|
3495
3675
|
} catch(error) {
|
|
@@ -3557,7 +3737,7 @@ exports.OutputsState = async function({libraryId, objectId, outputId, nodeId, in
|
|
|
3557
3737
|
/**
|
|
3558
3738
|
* Pin (route) the client to the egress node for a specific output. Fetches the output config
|
|
3559
3739
|
* to determine the node ID then sets the client fabric URIs.
|
|
3560
|
-
*
|
|
3740
|
+
* Node ID is retrieved from the output's delivery block (srt_push / srt_pull / rtp / udp).
|
|
3561
3741
|
* Assumes the client is already routed to an eligible egress node (via RouteToLiveEgress).
|
|
3562
3742
|
* Returns a function that restores the original fabric URIs.
|
|
3563
3743
|
*
|
|
@@ -3580,7 +3760,7 @@ const RouteToOutputNode = async ({client, libraryId, objectId, outputId, nodeId}
|
|
|
3580
3760
|
method: UrlJoin("live", "outputs", outputId),
|
|
3581
3761
|
constant: true
|
|
3582
3762
|
});
|
|
3583
|
-
nodeId = config
|
|
3763
|
+
nodeId = OutputDeliveryNodeId(config);
|
|
3584
3764
|
}
|
|
3585
3765
|
|
|
3586
3766
|
if(nodeId) {
|
|
@@ -3662,9 +3842,13 @@ const RetrieveOutputNodeId = async ({client, nodeIds, geos}) => {
|
|
|
3662
3842
|
/**
|
|
3663
3843
|
* Create a new live output.
|
|
3664
3844
|
*
|
|
3665
|
-
*
|
|
3666
|
-
*
|
|
3667
|
-
*
|
|
3845
|
+
* The delivery protocol is selected with `delivery.type` and configured with `delivery.settings`,
|
|
3846
|
+
* whose fields match the corresponding delivery block (see {@link OutputSrtPush}, {@link OutputSrtPull},
|
|
3847
|
+
* {@link OutputRtp}, {@link OutputUdp}). The output is pinned to an egress node, resolved in order
|
|
3848
|
+
* (node_ids/elvgeos for srt_pull, node_id/elvgeo for srt_push/rtp/udp):
|
|
3849
|
+
* - the node ID, if provided directly
|
|
3850
|
+
* - the geo, resolved against the fabric config `live_egress` services
|
|
3851
|
+
* - otherwise, the first available `live_egress` node
|
|
3668
3852
|
*
|
|
3669
3853
|
* Note: Output creation and modification is transactional. To create multiple outputs in a single
|
|
3670
3854
|
* transaction, use EditContentObject to open a write token, call CallBitcodeMethod for each output,
|
|
@@ -3675,13 +3859,12 @@ const RetrieveOutputNodeId = async ({client, nodeIds, geos}) => {
|
|
|
3675
3859
|
* @param {string=} libraryId - Library ID of the output settings object. If not provided, it will be retrieved automatically.
|
|
3676
3860
|
* @param {string} objectId - Object ID of the outputs settings object
|
|
3677
3861
|
* @param {string=} streamObjectId - Object ID of the input stream to use as the output source
|
|
3862
|
+
* @param {string=} offering - Offering key of the input stream to use
|
|
3863
|
+
* @param {boolean=} enabled - Whether the output is enabled (forced to false when no stream is specified)
|
|
3678
3864
|
* @param {string=} name - Display name for the output
|
|
3679
3865
|
* @param {string=} description - Description of the output
|
|
3680
|
-
* @param {
|
|
3681
|
-
* @param {
|
|
3682
|
-
* @param {string=} passphrase - SRT passphrase for encrypted delivery
|
|
3683
|
-
* @param {boolean=} stripRtp - Whether to strip RTP headers (default: false)
|
|
3684
|
-
* @param {Object=} srtConfig - Additional SRT connection configuration (see openapi-bitcode.html#tocssrtconnectionconfig)
|
|
3866
|
+
* @param {string=} externalId - External identifier for the output
|
|
3867
|
+
* @param {OutputDelivery} delivery - Delivery configuration: `{type, settings}`
|
|
3685
3868
|
*
|
|
3686
3869
|
* @returns {Promise<Object>} - The created output
|
|
3687
3870
|
*/
|
|
@@ -3689,61 +3872,80 @@ exports.OutputsCreate = async function({
|
|
|
3689
3872
|
libraryId,
|
|
3690
3873
|
objectId,
|
|
3691
3874
|
streamObjectId,
|
|
3875
|
+
offering,
|
|
3692
3876
|
enabled,
|
|
3693
3877
|
name,
|
|
3694
3878
|
description,
|
|
3695
3879
|
externalId,
|
|
3696
|
-
|
|
3697
|
-
geos=[],
|
|
3698
|
-
passphrase,
|
|
3699
|
-
stripRtp=false,
|
|
3700
|
-
srtConfig
|
|
3880
|
+
delivery
|
|
3701
3881
|
}) {
|
|
3702
3882
|
ValidateObject(objectId);
|
|
3703
3883
|
|
|
3704
|
-
|
|
3705
|
-
|
|
3884
|
+
const {type, settings = {}} = delivery || {};
|
|
3885
|
+
|
|
3886
|
+
if(!DELIVERY_PROTOCOLS.includes(type)) {
|
|
3887
|
+
throw new Error(`delivery.type must be one of ${DELIVERY_PROTOCOLS.join(", ")}`);
|
|
3706
3888
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3889
|
+
|
|
3890
|
+
// srt_pull uses array node_ids/elvgeos; srt_push/rtp/udp use singular node_id/elvgeo/url
|
|
3891
|
+
const isPull = type === "srt_pull";
|
|
3892
|
+
|
|
3893
|
+
if(isPull && settings.node_ids?.length && settings.elvgeos?.length) {
|
|
3894
|
+
throw new Error("delivery.settings may specify either node_ids or elvgeos, not both");
|
|
3709
3895
|
}
|
|
3710
|
-
if(
|
|
3711
|
-
throw new Error(
|
|
3896
|
+
if(["srt_push", "rtp", "udp"].includes(type) && !settings.url) {
|
|
3897
|
+
throw new Error(`delivery.settings.url is required for ${type} outputs`);
|
|
3712
3898
|
}
|
|
3713
3899
|
|
|
3714
3900
|
if(!libraryId) {
|
|
3715
3901
|
libraryId = await this.ContentObjectLibraryId({objectId});
|
|
3716
3902
|
}
|
|
3717
3903
|
|
|
3718
|
-
|
|
3904
|
+
if(isPull) {
|
|
3905
|
+
// srt_pull delivers by node_ids only — resolve them (from elvgeos, or a default) then drop elvgeos
|
|
3906
|
+
if(!settings.node_ids?.length) {
|
|
3907
|
+
settings.node_ids = settings.elvgeos?.length
|
|
3908
|
+
? await Promise.all(
|
|
3909
|
+
settings.elvgeos.map(geo => RetrieveOutputNodeId({client: this, geos: [geo]}))
|
|
3910
|
+
)
|
|
3911
|
+
: [await RetrieveOutputNodeId({client: this})];
|
|
3912
|
+
}
|
|
3913
|
+
delete settings.elvgeos;
|
|
3914
|
+
} else {
|
|
3915
|
+
// Resolve a concrete node_id (from elvgeo, or a default), then replace elvgeo with it
|
|
3916
|
+
if(!settings.node_id) {
|
|
3917
|
+
settings.node_id = await RetrieveOutputNodeId({
|
|
3918
|
+
client: this,
|
|
3919
|
+
geos: settings.elvgeo ? [settings.elvgeo] : undefined
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3922
|
+
delete settings.elvgeo;
|
|
3923
|
+
}
|
|
3719
3924
|
|
|
3720
|
-
// Route to any live egress node
|
|
3721
3925
|
const {restore} = await RouteToLiveEgress({client: this});
|
|
3722
3926
|
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3927
|
+
if(
|
|
3928
|
+
(type === "srt_pull" || type === "srt_push") &&
|
|
3929
|
+
settings.connection?.enforced_encryption &&
|
|
3930
|
+
!settings.passphrase
|
|
3931
|
+
) {
|
|
3932
|
+
settings.passphrase = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(16)))
|
|
3933
|
+
.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
3934
|
+
settings.connection.pb_keylen = 16;
|
|
3727
3935
|
}
|
|
3728
3936
|
|
|
3729
3937
|
try {
|
|
3730
3938
|
const output = {
|
|
3731
|
-
enabled: streamObjectId ? enabled : false,
|
|
3939
|
+
enabled: streamObjectId ? enabled : false,
|
|
3732
3940
|
name,
|
|
3733
3941
|
description,
|
|
3734
3942
|
external_id: externalId,
|
|
3735
|
-
input: streamObjectId ? {stream: streamObjectId} : undefined,
|
|
3736
|
-
|
|
3737
|
-
connection: srtConfig ?? undefined,
|
|
3738
|
-
node_ids: [resolvedNodeId],
|
|
3739
|
-
passphrase,
|
|
3740
|
-
strip_rtp: stripRtp
|
|
3741
|
-
}
|
|
3943
|
+
input: streamObjectId ? {stream: streamObjectId, offering} : undefined,
|
|
3944
|
+
[type]: settings
|
|
3742
3945
|
};
|
|
3743
3946
|
|
|
3744
3947
|
const {writeToken} = await this.EditContentObject({libraryId, objectId});
|
|
3745
3948
|
|
|
3746
|
-
// Note - you may create multiple outputs here, then finalize the transaction below
|
|
3747
3949
|
const outputs = await this.CallBitcodeMethod({
|
|
3748
3950
|
libraryId,
|
|
3749
3951
|
objectId,
|
|
@@ -3809,8 +4011,9 @@ exports.OutputsModify = async function({
|
|
|
3809
4011
|
({writeToken} = await this.EditContentObject({libraryId, objectId}));
|
|
3810
4012
|
}
|
|
3811
4013
|
|
|
3812
|
-
|
|
3813
|
-
|
|
4014
|
+
const deliverySettings = OutputDeliverySettings(output);
|
|
4015
|
+
if(deliverySettings?.connection?.enforced_encryption && !deliverySettings.passphrase) {
|
|
4016
|
+
deliverySettings.passphrase = Buffer.from(
|
|
3814
4017
|
globalThis.crypto.getRandomValues(new Uint8Array(16))
|
|
3815
4018
|
).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
3816
4019
|
}
|
|
@@ -3935,13 +4138,52 @@ exports.OutputsStop = async function({libraryId, objectId, outputId}) {
|
|
|
3935
4138
|
try {
|
|
3936
4139
|
const {writeToken} = await this.EditContentObject({libraryId, objectId});
|
|
3937
4140
|
|
|
3938
|
-
|
|
4141
|
+
const response = await this.CallBitcodeMethod({
|
|
3939
4142
|
libraryId,
|
|
3940
4143
|
objectId,
|
|
3941
4144
|
writeToken,
|
|
3942
4145
|
method: UrlJoin("live", "outputs", outputId, "ctrl", "stop"),
|
|
3943
4146
|
constant: false
|
|
3944
4147
|
});
|
|
4148
|
+
|
|
4149
|
+
await this.DeleteWriteToken({writeToken});
|
|
4150
|
+
|
|
4151
|
+
return response;
|
|
4152
|
+
} finally {
|
|
4153
|
+
restore();
|
|
4154
|
+
}
|
|
4155
|
+
};
|
|
4156
|
+
|
|
4157
|
+
/**
|
|
4158
|
+
* Reset a live output.
|
|
4159
|
+
*
|
|
4160
|
+
* @methodGroup Live Stream
|
|
4161
|
+
* @namedParams
|
|
4162
|
+
* @param {string=} libraryId - Library ID of the output settings object. If not provided, it will be retrieved automatically.
|
|
4163
|
+
* @param {string} objectId - Object ID of the output settings object
|
|
4164
|
+
* @param {string} outputId - ID of the output to reset
|
|
4165
|
+
*
|
|
4166
|
+
* @returns {Promise<Object>} - Response from the reset call
|
|
4167
|
+
*/
|
|
4168
|
+
exports.OutputsReset = async function({libraryId, objectId, outputId}) {
|
|
4169
|
+
ValidateObject(objectId);
|
|
4170
|
+
ValidatePresence("outputId", outputId);
|
|
4171
|
+
|
|
4172
|
+
if(!libraryId) {
|
|
4173
|
+
libraryId = await this.ContentObjectLibraryId({objectId});
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
// Route to a live egress node, then to the specific output's node
|
|
4177
|
+
const {restore} = await RouteToLiveEgress({client: this});
|
|
4178
|
+
await RouteToOutputNode({client: this, libraryId, objectId, outputId});
|
|
4179
|
+
|
|
4180
|
+
try {
|
|
4181
|
+
return await this.CallBitcodeMethod({
|
|
4182
|
+
libraryId,
|
|
4183
|
+
objectId,
|
|
4184
|
+
method: UrlJoin("live", "outputs", outputId, "ctrl", "reset"),
|
|
4185
|
+
constant: false
|
|
4186
|
+
});
|
|
3945
4187
|
} finally {
|
|
3946
4188
|
restore();
|
|
3947
4189
|
}
|
|
@@ -4003,7 +4245,7 @@ exports.OutputsDelete = async function({libraryId, objectId, outputId}) {
|
|
|
4003
4245
|
* @param {string} objectId - Object ID of the output settings object
|
|
4004
4246
|
* @param {Array<string>} outputs - List of output IDs to delete
|
|
4005
4247
|
*
|
|
4006
|
-
* @returns {Promise<Object
|
|
4248
|
+
* @returns {Promise<Array<Object>>} - Responses from each delete call, in input order
|
|
4007
4249
|
*/
|
|
4008
4250
|
exports.OutputsDeleteBatch = async function({libraryId, objectId, outputs}) {
|
|
4009
4251
|
ValidateObject(objectId);
|
|
@@ -4018,14 +4260,17 @@ exports.OutputsDeleteBatch = async function({libraryId, objectId, outputs}) {
|
|
|
4018
4260
|
try {
|
|
4019
4261
|
const {writeToken} = await this.EditContentObject({libraryId, objectId});
|
|
4020
4262
|
|
|
4021
|
-
const
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4263
|
+
const results = [];
|
|
4264
|
+
for(let outputId of outputs) {
|
|
4265
|
+
results.push(await this.CallBitcodeMethod({
|
|
4266
|
+
libraryId,
|
|
4267
|
+
objectId,
|
|
4268
|
+
writeToken,
|
|
4269
|
+
method: UrlJoin("live", "outputs", outputId),
|
|
4270
|
+
verb: "DELETE",
|
|
4271
|
+
constant: false,
|
|
4272
|
+
}));
|
|
4273
|
+
}
|
|
4029
4274
|
|
|
4030
4275
|
await this.FinalizeContentObject({
|
|
4031
4276
|
libraryId,
|
|
@@ -4034,7 +4279,7 @@ exports.OutputsDeleteBatch = async function({libraryId, objectId, outputs}) {
|
|
|
4034
4279
|
commitMessage: "Remove outputs (batch)"
|
|
4035
4280
|
});
|
|
4036
4281
|
|
|
4037
|
-
return
|
|
4282
|
+
return results;
|
|
4038
4283
|
} finally {
|
|
4039
4284
|
restore();
|
|
4040
4285
|
}
|
package/utilities/LiveOutputs.js
CHANGED
|
@@ -54,10 +54,15 @@ const Create = async ({streamId, nodeId, geo, passphrase, name}) => {
|
|
|
54
54
|
streamObjectId: streamId,
|
|
55
55
|
enabled: true,
|
|
56
56
|
name,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
delivery: {
|
|
58
|
+
type: "srt_pull",
|
|
59
|
+
settings: {
|
|
60
|
+
node_ids: nodeId ? [nodeId] : undefined,
|
|
61
|
+
elvgeos: geo ? [geo] : undefined,
|
|
62
|
+
passphrase,
|
|
63
|
+
strip_rtp: true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
61
66
|
});
|
|
62
67
|
console.log(JSON.stringify(result, null, 2));
|
|
63
68
|
};
|