@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.
- package/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +128 -45
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +128 -45
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +128 -45
- package/dist/index.es.js.map +1 -1
- package/dist/src/StreamVideoClient.d.ts +2 -3
- package/dist/src/coordinator/connection/client.d.ts +11 -8
- package/dist/src/coordinator/connection/connection.d.ts +5 -4
- package/dist/src/helpers/promise.d.ts +18 -0
- package/dist/src/helpers/sdp-munging.d.ts +4 -0
- package/dist/src/rtc/Publisher.d.ts +1 -0
- package/dist/src/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/StreamVideoClient.ts +14 -19
- package/src/__tests__/Call.test.ts +2 -2
- package/src/coordinator/connection/client.ts +33 -14
- package/src/coordinator/connection/connection.ts +14 -19
- package/src/helpers/__tests__/sdp-munging.test.ts +168 -1
- package/src/helpers/promise.ts +47 -0
- package/src/helpers/sdp-munging.ts +55 -0
- package/src/rtc/Publisher.ts +24 -0
- package/src/rtc/codecs.ts +1 -1
- package/src/types.ts +6 -0
|
@@ -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
|
*
|
package/src/rtc/Publisher.ts
CHANGED
|
@@ -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=
|
|
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.
|