@stream-io/video-client 1.11.3 → 1.11.5

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.
@@ -0,0 +1,47 @@
1
+ export interface SafePromise<T> {
2
+ (): Promise<T>;
3
+ checkPending(): boolean;
4
+ }
5
+
6
+ type Fulfillment<T> =
7
+ | {
8
+ status: 'resolved';
9
+ result: T;
10
+ }
11
+ | {
12
+ status: 'rejected';
13
+ error: unknown;
14
+ };
15
+
16
+ /**
17
+ * Saving a long-lived reference to a promise that can reject can be unsafe,
18
+ * since rejecting the promise causes an unhandled rejection error (even if the
19
+ * rejection is handled everywhere promise result is expected).
20
+ *
21
+ * To avoid that, we add both resolution and rejection handlers to the promise.
22
+ * That way, the saved promise never rejects. A callback is provided as return
23
+ * value to build a *new* promise, that resolves and rejects along with
24
+ * the original promise.
25
+ * @param promise Promise to wrap, which possibly rejects
26
+ * @returns Callback to build a new promise, which resolves and rejects along
27
+ * with the original promise
28
+ */
29
+ export function makeSafePromise<T>(promise: Promise<T>): SafePromise<T> {
30
+ let isPending = true;
31
+
32
+ const safePromise: Promise<Fulfillment<T>> = promise
33
+ .then(
34
+ (result) => ({ status: 'resolved' as const, result }),
35
+ (error) => ({ status: 'rejected' as const, error }),
36
+ )
37
+ .finally(() => (isPending = false));
38
+
39
+ const unwrapPromise = () =>
40
+ safePromise.then((fulfillment) => {
41
+ if (fulfillment.status === 'rejected') throw fulfillment.error;
42
+ return fulfillment.result;
43
+ });
44
+
45
+ unwrapPromise.checkPending = () => isPending;
46
+ return unwrapPromise;
47
+ }
@@ -129,6 +129,61 @@ export const toggleDtx = (sdp: string, enable: boolean): string => {
129
129
  return sdp.replace(opusFmtp.original, newFmtp);
130
130
  };
131
131
 
132
+ /**
133
+ * Returns and SDP with all the codecs except the given codec removed.
134
+ */
135
+ export const preserveCodec = (
136
+ sdp: string,
137
+ mid: string,
138
+ codec: RTCRtpCodec,
139
+ ): string => {
140
+ const [kind, codecName] = codec.mimeType.toLowerCase().split('/');
141
+
142
+ const toSet = (fmtpLine: string) =>
143
+ new Set(fmtpLine.split(';').map((f) => f.trim().toLowerCase()));
144
+
145
+ const equal = (a: Set<string>, b: Set<string>) => {
146
+ if (a.size !== b.size) return false;
147
+ for (const item of a) if (!b.has(item)) return false;
148
+ return true;
149
+ };
150
+
151
+ const codecFmtp = toSet(codec.sdpFmtpLine || '');
152
+ const parsedSdp = SDP.parse(sdp);
153
+ for (const media of parsedSdp.media) {
154
+ if (media.type !== kind || String(media.mid) !== mid) continue;
155
+
156
+ // find the payload id of the desired codec
157
+ const payloads = new Set<number>();
158
+ for (const rtp of media.rtp) {
159
+ if (
160
+ rtp.codec.toLowerCase() === codecName &&
161
+ media.fmtp.some(
162
+ (f) => f.payload === rtp.payload && equal(toSet(f.config), codecFmtp),
163
+ )
164
+ ) {
165
+ payloads.add(rtp.payload);
166
+ }
167
+ }
168
+
169
+ // find the corresponding rtx codec by matching apt=<preserved-codec-payload>
170
+ for (const fmtp of media.fmtp) {
171
+ const match = fmtp.config.match(/(apt)=(\d+)/);
172
+ if (!match) continue;
173
+ const [, , preservedCodecPayload] = match;
174
+ if (payloads.has(Number(preservedCodecPayload))) {
175
+ payloads.add(fmtp.payload);
176
+ }
177
+ }
178
+
179
+ media.rtp = media.rtp.filter((r) => payloads.has(r.payload));
180
+ media.fmtp = media.fmtp.filter((f) => payloads.has(f.payload));
181
+ media.rtcpFb = media.rtcpFb?.filter((f) => payloads.has(f.payload));
182
+ media.payloads = Array.from(payloads).join(' ');
183
+ }
184
+ return SDP.write(parsedSdp);
185
+ };
186
+
132
187
  /**
133
188
  * Enables high-quality audio through SDP munging for the given trackMid.
134
189
  *
@@ -20,6 +20,7 @@ import { PublishOptions } from '../types';
20
20
  import {
21
21
  enableHighQualityAudio,
22
22
  extractMid,
23
+ preserveCodec,
23
24
  toggleDtx,
24
25
  } from '../helpers/sdp-munging';
25
26
  import { Logger } from '../coordinator/connection/types';
@@ -530,6 +531,12 @@ export class Publisher {
530
531
  if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
531
532
  offer.sdp = this.enableHighQualityAudio(offer.sdp);
532
533
  }
534
+ if (this.isPublishing(TrackType.VIDEO)) {
535
+ // Hotfix for platforms that don't respect the ordered codec list
536
+ // (Firefox, Android, Linux, etc...).
537
+ // We remove all the codecs from the SDP except the one we want to use.
538
+ offer.sdp = this.removeUnpreferredCodecs(offer.sdp, TrackType.VIDEO);
539
+ }
533
540
  }
534
541
 
535
542
  const trackInfos = this.getAnnouncedTracks(offer.sdp);
@@ -564,6 +571,23 @@ export class Publisher {
564
571
  );
565
572
  };
566
573
 
574
+ private removeUnpreferredCodecs(sdp: string, trackType: TrackType): string {
575
+ const opts = this.publishOptsForTrack.get(trackType);
576
+ if (!opts || !opts.forceSingleCodec) return sdp;
577
+
578
+ const codec = opts.forceCodec || opts.preferredCodec;
579
+ const orderedCodecs = this.getCodecPreferences(trackType, codec);
580
+ if (!orderedCodecs || orderedCodecs.length === 0) return sdp;
581
+
582
+ const transceiver = this.transceiverCache.get(trackType);
583
+ if (!transceiver) return sdp;
584
+
585
+ const index = this.transceiverInitOrder.indexOf(trackType);
586
+ const mid = extractMid(transceiver, index, sdp);
587
+ const [codecToPreserve] = orderedCodecs;
588
+ return preserveCodec(sdp, mid, codecToPreserve);
589
+ }
590
+
567
591
  private enableHighQualityAudio = (sdp: string) => {
568
592
  const transceiver = this.transceiverCache.get(TrackType.SCREEN_SHARE_AUDIO);
569
593
  if (!transceiver) return sdp;
package/src/rtc/codecs.ts CHANGED
@@ -50,7 +50,7 @@ export const getPreferredCodecs = (
50
50
  }
51
51
 
52
52
  const sdpFmtpLine = codec.sdpFmtpLine;
53
- if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=42e01f')) {
53
+ if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=42')) {
54
54
  // this is not the baseline h264 codec, prioritize it lower
55
55
  partiallyPreferred.push(codec);
56
56
  continue;
package/src/types.ts CHANGED
@@ -167,6 +167,12 @@ export type PublishOptions = {
167
167
  * Use with caution.
168
168
  */
169
169
  forceCodec?: PreferredCodec;
170
+ /**
171
+ * When using a preferred codec, force the use of a single codec.
172
+ * Enabling this, it will remove all other supported codecs from the SDP.
173
+ * Defaults to false.
174
+ */
175
+ forceSingleCodec?: boolean;
170
176
  /**
171
177
  * The preferred scalability to use when publishing the video stream.
172
178
  * Applicable only for SVC codecs.