@furious.luke/argus-js 0.5.0 → 0.5.2

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/index.d.cts CHANGED
@@ -14,11 +14,23 @@ type TrackType = "camera" | "screen" | "audio";
14
14
  */
15
15
  type VideoTrackType = "camera" | "screen";
16
16
  /**
17
- * Declares the logical type of a single published track, keyed by the browser
18
- * `MediaStreamTrack.id`. The media server reads this to label the track
19
- * explicitly rather than guessing from the SDP msid.
17
+ * Declares the logical type and identity of a single published track.
18
+ *
19
+ * `mid` is the negotiated media-section id — the ROUTING key the server matches a
20
+ * track to its type by. It is used rather than the browser `MediaStreamTrack.id`
21
+ * because it is the only track identifier both peers agree on: Firefox mints an
22
+ * SDP msid track id unrelated to the `MediaStreamTrack.id`, so routing keyed on
23
+ * the latter cannot be matched server-side.
24
+ *
25
+ * `id` is the browser `MediaStreamTrack.id` — the GENERATION identity. The mid is
26
+ * stable across renegotiation and is reused when a track of the same type is
27
+ * republished on the same m-line, so it cannot distinguish a delayed end of a
28
+ * removed track from a failure of its replacement; the id changes with each new
29
+ * capture and can. The server echoes it in `media_track_ended`, and the publisher
30
+ * correlates that against the id it intentionally removed.
20
31
  */
21
32
  interface TrackLabel {
33
+ mid: string;
22
34
  id: string;
23
35
  type: TrackType;
24
36
  }
@@ -177,6 +189,16 @@ interface PublisherOptions {
177
189
  gatewayHandshakeTimeoutMs?: number;
178
190
  /** Deadline after the initial offer for WebRTC to reach connected. Defaults to 30 seconds. */
179
191
  peerConnectionTimeoutMs?: number;
192
+ /**
193
+ * Preferred video codecs, most-preferred first, as RTP MIME types (e.g.
194
+ * `"video/VP9"`, `"video/VP8"`, `"video/H264"`). Each published video track's
195
+ * transceiver offers these codecs ahead of the rest, so the browser sends the
196
+ * first one the media server also accepts. Codecs the browser supports but that
197
+ * are not listed keep their default order behind the preferred ones, so
198
+ * negotiation still falls back cleanly. Defaults to `["video/VP9"]`. Pass an
199
+ * empty array to leave the browser's native codec order untouched.
200
+ */
201
+ preferredVideoCodecs?: string[];
180
202
  /** How long to retry a dropped signaling connection to the selected gateway. Defaults to 20 seconds. */
181
203
  signalingReconnectTimeoutMs?: number;
182
204
  /** Callbacks for lifecycle events. */
@@ -435,6 +457,20 @@ declare class Publisher {
435
457
  * the server's outbound speech sender on the dedicated speech transceiver.
436
458
  */
437
459
  private addMicrophoneTrack;
460
+ /**
461
+ * Reorders the codecs a video sender offers so the browser prefers the
462
+ * configured codecs (VP9 by default). This is what actually controls the wire
463
+ * format: the browser is the offerer and sends its own top-of-offer codec, and
464
+ * the media server (Pion) answers by mirroring the offer's codec order — so the
465
+ * server's own registration order has no say. Floating VP9 to the front of the
466
+ * offer is therefore the lever. Codecs not in the preference keep their native
467
+ * order behind it, so anything VP9 can't satisfy still negotiates.
468
+ *
469
+ * Best-effort: browsers lacking `getCapabilities`/`setCodecPreferences` (older
470
+ * Safari) keep their default order, and any failure is swallowed — codec
471
+ * preference is an optimization, never a requirement for publishing.
472
+ */
473
+ private preferVideoCodecs;
438
474
  /** Registers one physical video track under its logical type and source stream. */
439
475
  private registerTrack;
440
476
  /** The live video tracks currently published under the given type. */
@@ -442,6 +478,13 @@ declare class Publisher {
442
478
  /** Builds the id → type label array declared to the server on every offer. */
443
479
  private buildTrackLabels;
444
480
  private labelsReplacingType;
481
+ /**
482
+ * The negotiated mid of the transceiver currently sending `track`, or null if
483
+ * none is found or it has not been negotiated yet. The mid is the identifier
484
+ * both peers agree on; it is assigned once setLocalDescription runs, which the
485
+ * publisher always does before sending an offer's labels.
486
+ */
487
+ private midForTrack;
445
488
  private stagePublish;
446
489
  private stageUnpublish;
447
490
  /** Stops every published local track and clears the published map. */
package/dist/index.d.ts CHANGED
@@ -14,11 +14,23 @@ type TrackType = "camera" | "screen" | "audio";
14
14
  */
15
15
  type VideoTrackType = "camera" | "screen";
16
16
  /**
17
- * Declares the logical type of a single published track, keyed by the browser
18
- * `MediaStreamTrack.id`. The media server reads this to label the track
19
- * explicitly rather than guessing from the SDP msid.
17
+ * Declares the logical type and identity of a single published track.
18
+ *
19
+ * `mid` is the negotiated media-section id — the ROUTING key the server matches a
20
+ * track to its type by. It is used rather than the browser `MediaStreamTrack.id`
21
+ * because it is the only track identifier both peers agree on: Firefox mints an
22
+ * SDP msid track id unrelated to the `MediaStreamTrack.id`, so routing keyed on
23
+ * the latter cannot be matched server-side.
24
+ *
25
+ * `id` is the browser `MediaStreamTrack.id` — the GENERATION identity. The mid is
26
+ * stable across renegotiation and is reused when a track of the same type is
27
+ * republished on the same m-line, so it cannot distinguish a delayed end of a
28
+ * removed track from a failure of its replacement; the id changes with each new
29
+ * capture and can. The server echoes it in `media_track_ended`, and the publisher
30
+ * correlates that against the id it intentionally removed.
20
31
  */
21
32
  interface TrackLabel {
33
+ mid: string;
22
34
  id: string;
23
35
  type: TrackType;
24
36
  }
@@ -177,6 +189,16 @@ interface PublisherOptions {
177
189
  gatewayHandshakeTimeoutMs?: number;
178
190
  /** Deadline after the initial offer for WebRTC to reach connected. Defaults to 30 seconds. */
179
191
  peerConnectionTimeoutMs?: number;
192
+ /**
193
+ * Preferred video codecs, most-preferred first, as RTP MIME types (e.g.
194
+ * `"video/VP9"`, `"video/VP8"`, `"video/H264"`). Each published video track's
195
+ * transceiver offers these codecs ahead of the rest, so the browser sends the
196
+ * first one the media server also accepts. Codecs the browser supports but that
197
+ * are not listed keep their default order behind the preferred ones, so
198
+ * negotiation still falls back cleanly. Defaults to `["video/VP9"]`. Pass an
199
+ * empty array to leave the browser's native codec order untouched.
200
+ */
201
+ preferredVideoCodecs?: string[];
180
202
  /** How long to retry a dropped signaling connection to the selected gateway. Defaults to 20 seconds. */
181
203
  signalingReconnectTimeoutMs?: number;
182
204
  /** Callbacks for lifecycle events. */
@@ -435,6 +457,20 @@ declare class Publisher {
435
457
  * the server's outbound speech sender on the dedicated speech transceiver.
436
458
  */
437
459
  private addMicrophoneTrack;
460
+ /**
461
+ * Reorders the codecs a video sender offers so the browser prefers the
462
+ * configured codecs (VP9 by default). This is what actually controls the wire
463
+ * format: the browser is the offerer and sends its own top-of-offer codec, and
464
+ * the media server (Pion) answers by mirroring the offer's codec order — so the
465
+ * server's own registration order has no say. Floating VP9 to the front of the
466
+ * offer is therefore the lever. Codecs not in the preference keep their native
467
+ * order behind it, so anything VP9 can't satisfy still negotiates.
468
+ *
469
+ * Best-effort: browsers lacking `getCapabilities`/`setCodecPreferences` (older
470
+ * Safari) keep their default order, and any failure is swallowed — codec
471
+ * preference is an optimization, never a requirement for publishing.
472
+ */
473
+ private preferVideoCodecs;
438
474
  /** Registers one physical video track under its logical type and source stream. */
439
475
  private registerTrack;
440
476
  /** The live video tracks currently published under the given type. */
@@ -442,6 +478,13 @@ declare class Publisher {
442
478
  /** Builds the id → type label array declared to the server on every offer. */
443
479
  private buildTrackLabels;
444
480
  private labelsReplacingType;
481
+ /**
482
+ * The negotiated mid of the transceiver currently sending `track`, or null if
483
+ * none is found or it has not been negotiated yet. The mid is the identifier
484
+ * both peers agree on; it is assigned once setLocalDescription runs, which the
485
+ * publisher always does before sending an offer's labels.
486
+ */
487
+ private midForTrack;
445
488
  private stagePublish;
446
489
  private stageUnpublish;
447
490
  /** Stops every published local track and clears the published map. */
package/dist/index.js CHANGED
@@ -151,10 +151,11 @@ var Publisher = class {
151
151
  // may later reuse any compatible inactive transceiver, not necessarily the one
152
152
  // that previously carried the same logical type.
153
153
  typeSenders = /* @__PURE__ */ new Map();
154
- // intentionalTrackEnds holds physical track ids removed by publish/unpublish
155
- // whose server-side `media_track_ended` has not yet arrived. Correlating by id
156
- // keeps a delayed end for an old screen track from being mistaken for failure
157
- // of a newly-published screen track.
154
+ // intentionalTrackEnds holds the browser track ids removed by publish/unpublish
155
+ // whose server-side `media_track_ended` has not yet arrived. Correlating by the
156
+ // track id (the generation identity the server echoes in track_id) keeps a
157
+ // delayed end for an old screen track from being mistaken for failure of a
158
+ // newly-published screen track.
158
159
  intentionalTrackEnds = /* @__PURE__ */ new Map();
159
160
  // negotiationChain serializes every offer/answer exchange, including recovery.
160
161
  // User operations do not resolve until their answer is applied, so no caller
@@ -340,6 +341,7 @@ var Publisher = class {
340
341
  };
341
342
  if (initialTrack) {
342
343
  const sender = initialTrack.type === "audio" ? this.addMicrophoneTrack(pc, initialTrack.track, initialTrack.stream) : pc.addTrack(initialTrack.track, initialTrack.stream);
344
+ if (initialTrack.type !== "audio") this.preferVideoCodecs(pc, sender);
343
345
  this.typeSenders.set(
344
346
  initialTrack.type,
345
347
  sender
@@ -1095,7 +1097,7 @@ var Publisher = class {
1095
1097
  sdp: local.sdp,
1096
1098
  sdp_type: "offer",
1097
1099
  negotiation_id: id,
1098
- tracks: change?.labels ?? this.buildTrackLabels(),
1100
+ tracks: change?.labels?.() ?? this.buildTrackLabels(),
1099
1101
  speech_enabled: this.speechEnabled || this.speechPending || void 0
1100
1102
  });
1101
1103
  this.releaseLocalCandidateBatch();
@@ -1504,7 +1506,7 @@ var Publisher = class {
1504
1506
  this.typeSenders.set(type, addedSender);
1505
1507
  }
1506
1508
  return {
1507
- labels: this.labelsReplacingType(type, track),
1509
+ labels: () => this.labelsReplacingType(type, track),
1508
1510
  commit: () => {
1509
1511
  for (const { track: oldTrack } of previous) {
1510
1512
  this.unwatchTrack(oldTrack);
@@ -1550,6 +1552,39 @@ var Publisher = class {
1550
1552
  this.microphoneTransceiver = transceiver;
1551
1553
  return transceiver.sender;
1552
1554
  }
1555
+ /**
1556
+ * Reorders the codecs a video sender offers so the browser prefers the
1557
+ * configured codecs (VP9 by default). This is what actually controls the wire
1558
+ * format: the browser is the offerer and sends its own top-of-offer codec, and
1559
+ * the media server (Pion) answers by mirroring the offer's codec order — so the
1560
+ * server's own registration order has no say. Floating VP9 to the front of the
1561
+ * offer is therefore the lever. Codecs not in the preference keep their native
1562
+ * order behind it, so anything VP9 can't satisfy still negotiates.
1563
+ *
1564
+ * Best-effort: browsers lacking `getCapabilities`/`setCodecPreferences` (older
1565
+ * Safari) keep their default order, and any failure is swallowed — codec
1566
+ * preference is an optimization, never a requirement for publishing.
1567
+ */
1568
+ preferVideoCodecs(pc, sender) {
1569
+ const preferred = this.opts.preferredVideoCodecs ?? ["video/VP9"];
1570
+ if (preferred.length === 0) return;
1571
+ if (sender.track && sender.track.kind !== "video") return;
1572
+ if (typeof RTCRtpSender === "undefined" || typeof RTCRtpSender.getCapabilities !== "function") return;
1573
+ if (typeof pc.getTransceivers !== "function") return;
1574
+ const caps = RTCRtpSender.getCapabilities("video");
1575
+ if (!caps?.codecs) return;
1576
+ const transceiver = pc.getTransceivers().find((t) => t.sender === sender);
1577
+ if (!transceiver || typeof transceiver.setCodecPreferences !== "function") return;
1578
+ const rank = (mimeType) => {
1579
+ const idx = preferred.findIndex((p) => p.toLowerCase() === mimeType.toLowerCase());
1580
+ return idx === -1 ? preferred.length : idx;
1581
+ };
1582
+ const ordered = caps.codecs.map((codec, index) => ({ codec, index })).sort((a, b) => rank(a.codec.mimeType) - rank(b.codec.mimeType) || a.index - b.index).map((entry) => entry.codec);
1583
+ try {
1584
+ transceiver.setCodecPreferences(ordered);
1585
+ } catch {
1586
+ }
1587
+ }
1553
1588
  /** Registers one physical video track under its logical type and source stream. */
1554
1589
  registerTrack(track, stream, type) {
1555
1590
  this.published.set(track, type);
@@ -1568,18 +1603,34 @@ var Publisher = class {
1568
1603
  buildTrackLabels() {
1569
1604
  const labels = [];
1570
1605
  for (const [track, type] of this.published) {
1571
- labels.push({ id: track.id, type });
1606
+ const mid = this.midForTrack(track);
1607
+ if (mid !== null) labels.push({ mid, id: track.id, type });
1572
1608
  }
1573
1609
  return labels;
1574
1610
  }
1575
1611
  labelsReplacingType(type, replacement) {
1576
1612
  const labels = [];
1577
1613
  for (const [track, publishedType] of this.published) {
1578
- if (publishedType !== type) labels.push({ id: track.id, type: publishedType });
1614
+ if (publishedType === type) continue;
1615
+ const mid = this.midForTrack(track);
1616
+ if (mid !== null) labels.push({ mid, id: track.id, type: publishedType });
1617
+ }
1618
+ if (replacement) {
1619
+ const mid = this.midForTrack(replacement);
1620
+ if (mid !== null) labels.push({ mid, id: replacement.id, type });
1579
1621
  }
1580
- if (replacement) labels.push({ id: replacement.id, type });
1581
1622
  return labels;
1582
1623
  }
1624
+ /**
1625
+ * The negotiated mid of the transceiver currently sending `track`, or null if
1626
+ * none is found or it has not been negotiated yet. The mid is the identifier
1627
+ * both peers agree on; it is assigned once setLocalDescription runs, which the
1628
+ * publisher always does before sending an offer's labels.
1629
+ */
1630
+ midForTrack(track) {
1631
+ const transceiver = this.pc?.getTransceivers().find((candidate) => candidate.sender.track === track);
1632
+ return transceiver?.mid ?? null;
1633
+ }
1583
1634
  async stagePublish(track, stream, type) {
1584
1635
  const pc = this.pc;
1585
1636
  if (!pc) throw new Error("publisher not started");
@@ -1607,10 +1658,11 @@ var Publisher = class {
1607
1658
  throw new SenderRestoreError(`inactive ${type} sender still has a track`);
1608
1659
  }
1609
1660
  addedSender = pc.addTrack(track, stream);
1661
+ this.preferVideoCodecs(pc, addedSender);
1610
1662
  this.typeSenders.set(type, addedSender);
1611
1663
  }
1612
1664
  return {
1613
- labels: this.labelsReplacingType(type, track),
1665
+ labels: () => this.labelsReplacingType(type, track),
1614
1666
  commit: () => {
1615
1667
  this.cancelMediaRecovery(type);
1616
1668
  this.recoveryState(type).required = false;
@@ -1658,7 +1710,7 @@ var Publisher = class {
1658
1710
  for (const { track } of previous) this.expectIntentionalTrackEnd(track.id, type);
1659
1711
  pc.removeTrack(typeSender);
1660
1712
  return {
1661
- labels: this.labelsReplacingType(type),
1713
+ labels: () => this.labelsReplacingType(type),
1662
1714
  commit: () => {
1663
1715
  this.cancelMediaRecovery(type);
1664
1716
  this.recoveryState(type).required = false;