@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.
@@ -226,7 +226,13 @@ exports.StreamCreate = async function({
226
226
  ]
227
227
  });
228
228
 
229
- const tenantContentAdminGroup = await this.ContentAdminGroup({tenantContractId: tenantId});
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 === "running" && videoLastFinalizationTimeEpochSec <= 0) {
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
- if(state === "running") {
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 {boolean=} reset - Whether to reset the output
3338
- * @property {Object=} input - Input stream configuration
3339
- * @property {string=} input.stream - Object ID of the input stream (null to disconnect)
3340
- * @property {Object=} srt_pull - SRT pull delivery configuration
3341
- * @property {Array<string>=} srt_pull.node_ids - Egress node IDs for SRT delivery (max 1)
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
- // Resolve egress node and replace SRT URL with the egress endpoint hostname.
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
- if(fabricUrl) {
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
- for(let [key, value] of Object.entries(outputs)) {
3402
- const streamId = value.input?.stream;
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
- if(streamId) {
3405
- const streamMetadata = await this.ContentObjectMetadata({
3406
- libraryId: await this.ContentObjectLibraryId({objectId: streamId}),
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
- const streamStatus = await this.StreamStatus({name: streamId});
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
- value.input.name = streamMetadata;
3414
- value.input.status = streamStatus?.state;
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
- if(includeState) {
3418
- await this.OutputsResolveSrtPullUrls({value});
3419
- try {
3420
- const nodeId = value.srt_pull?.node_ids?.[0];
3421
- const result = await this.OutputsState({outputId: key, objectId, libraryId, nodeId, includeState: true});
3422
- value.state = result.state;
3423
- } catch(error) {
3424
- this.Log(`Failed to retrieve state for output ${key}: ${error.message}`, true);
3425
- value.state = {};
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.srt_pull?.node_ids?.[0];
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
- * Currently node ID is retrieved from srt_pull.node_ids (only srt_pull outputs are supported).
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?.srt_pull?.node_ids?.[0];
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
- * At the current version of the live outputs API an output will be pinned to a node, by either:
3666
- * - specifying the node directly in 'nodeIds'
3667
- * - specifying an 'elvgeo' and use fabric config 'live_egress' services to pick a node ID
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 {Array<string>=} nodeIds - Explicit node ID(s) for SRT delivery (max 1)
3681
- * @param {Array<string>=} geos - Geo regions for SRT delivery (max 1) — used to resolve a node from live_egress endpoints
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
- nodeIds,
3697
- geos=[],
3698
- passphrase,
3699
- stripRtp=false,
3700
- srtConfig
3880
+ delivery
3701
3881
  }) {
3702
3882
  ValidateObject(objectId);
3703
3883
 
3704
- if(nodeIds && geos.length > 0) {
3705
- throw new Error("Specify either nodeIds or geos, not both");
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
- if(nodeIds && nodeIds.length > 1) {
3708
- throw new Error("Only one node ID is supported — nodeIds must have at most 1 element");
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(geos.length > 1) {
3711
- throw new Error("Only one geo is supported — geos must have at most 1 element");
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
- const resolvedNodeId = await RetrieveOutputNodeId({client: this, nodeIds, geos});
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
- // Auto-generate passphrase if encryption is truthy
3724
- if(srtConfig?.enforced_encryption && !passphrase) {
3725
- passphrase = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(16))).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3726
- srtConfig["pb_keylen"] = 16;
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, // Output must be disabled if no stream specified
3939
+ enabled: streamObjectId ? enabled : false,
3732
3940
  name,
3733
3941
  description,
3734
3942
  external_id: externalId,
3735
- input: streamObjectId ? {stream: streamObjectId} : undefined,
3736
- srt_pull: {
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
- if(output.srt_pull?.connection?.enforced_encryption && !output.srt_pull?.passphrase) {
3813
- output.srt_pull.passphrase = Buffer.from(
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
- return await this.CallBitcodeMethod({
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>} - Response from the delete call
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 result = await this.CallBitcodeMethod({
4022
- libraryId,
4023
- objectId,
4024
- writeToken,
4025
- method: UrlJoin("live", "outputs", outputId),
4026
- verb: "DELETE",
4027
- constant: false,
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 result;
4282
+ return results;
4038
4283
  } finally {
4039
4284
  restore();
4040
4285
  }
@@ -54,10 +54,15 @@ const Create = async ({streamId, nodeId, geo, passphrase, name}) => {
54
54
  streamObjectId: streamId,
55
55
  enabled: true,
56
56
  name,
57
- nodeIds: nodeId ? [nodeId] : undefined,
58
- geos: geo ? [geo] : [],
59
- passphrase,
60
- stripRtp: true
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
  };