@stream-io/video-client 1.11.3 → 1.11.4
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 +7 -0
- package/dist/index.browser.es.js +69 -3
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +69 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +69 -3
- package/dist/index.es.js.map +1 -1
- 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/helpers/__tests__/sdp-munging.test.ts +168 -1
- 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
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Returns an SDP with DTX enabled or disabled.
|
|
3
3
|
*/
|
|
4
4
|
export declare const toggleDtx: (sdp: string, enable: boolean) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Returns and SDP with all the codecs except the given codec removed.
|
|
7
|
+
*/
|
|
8
|
+
export declare const preserveCodec: (sdp: string, mid: string, codec: RTCRtpCodec) => string;
|
|
5
9
|
/**
|
|
6
10
|
* Enables high-quality audio through SDP munging for the given trackMid.
|
|
7
11
|
*
|
|
@@ -124,6 +124,7 @@ export declare class Publisher {
|
|
|
124
124
|
* @param options the optional offer options to use.
|
|
125
125
|
*/
|
|
126
126
|
private negotiate;
|
|
127
|
+
private removeUnpreferredCodecs;
|
|
127
128
|
private enableHighQualityAudio;
|
|
128
129
|
/**
|
|
129
130
|
* Returns a list of tracks that are currently being published.
|
package/dist/src/types.d.ts
CHANGED
|
@@ -129,6 +129,12 @@ export type PublishOptions = {
|
|
|
129
129
|
* Use with caution.
|
|
130
130
|
*/
|
|
131
131
|
forceCodec?: PreferredCodec;
|
|
132
|
+
/**
|
|
133
|
+
* When using a preferred codec, force the use of a single codec.
|
|
134
|
+
* Enabling this, it will remove all other supported codecs from the SDP.
|
|
135
|
+
* Defaults to false.
|
|
136
|
+
*/
|
|
137
|
+
forceSingleCodec?: boolean;
|
|
132
138
|
/**
|
|
133
139
|
* The preferred scalability to use when publishing the video stream.
|
|
134
140
|
* Applicable only for SVC codecs.
|
package/package.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
enableHighQualityAudio,
|
|
4
|
+
preserveCodec,
|
|
5
|
+
toggleDtx,
|
|
6
|
+
} from '../sdp-munging';
|
|
3
7
|
import { initialSdp as HQAudioSDP } from './hq-audio-sdp';
|
|
4
8
|
|
|
5
9
|
describe('sdp-munging', () => {
|
|
@@ -21,4 +25,167 @@ a=maxptime:40`;
|
|
|
21
25
|
expect(sdpWithHighQualityAudio).toContain('maxaveragebitrate=510000');
|
|
22
26
|
expect(sdpWithHighQualityAudio).toContain('stereo=1');
|
|
23
27
|
});
|
|
28
|
+
|
|
29
|
+
it('preserves the preferred codec', () => {
|
|
30
|
+
const sdp = `v=0
|
|
31
|
+
o=- 8608371809202407637 2 IN IP4 127.0.0.1
|
|
32
|
+
s=-
|
|
33
|
+
t=0 0
|
|
34
|
+
a=extmap-allow-mixed
|
|
35
|
+
a=msid-semantic: WMS 52fafc21-b8bb-4f4f-8072-86a29cb6590e
|
|
36
|
+
a=group:BUNDLE 0
|
|
37
|
+
m=video 9 UDP/TLS/RTP/SAVPF 98 100 99 101
|
|
38
|
+
c=IN IP4 0.0.0.0
|
|
39
|
+
a=rtpmap:98 VP9/90000
|
|
40
|
+
a=rtpmap:99 rtx/90000
|
|
41
|
+
a=rtpmap:100 VP9/90000
|
|
42
|
+
a=rtpmap:101 rtx/90000
|
|
43
|
+
a=fmtp:98 profile-id=0
|
|
44
|
+
a=fmtp:99 apt=98
|
|
45
|
+
a=fmtp:100 profile-id=2
|
|
46
|
+
a=fmtp:101 apt=100
|
|
47
|
+
a=rtcp:9 IN IP4 0.0.0.0
|
|
48
|
+
a=rtcp-fb:98 goog-remb
|
|
49
|
+
a=rtcp-fb:98 transport-cc
|
|
50
|
+
a=rtcp-fb:98 ccm fir
|
|
51
|
+
a=rtcp-fb:98 nack
|
|
52
|
+
a=rtcp-fb:98 nack pli
|
|
53
|
+
a=rtcp-fb:100 goog-remb
|
|
54
|
+
a=rtcp-fb:100 transport-cc
|
|
55
|
+
a=rtcp-fb:100 ccm fir
|
|
56
|
+
a=rtcp-fb:100 nack
|
|
57
|
+
a=rtcp-fb:100 nack pli
|
|
58
|
+
a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
|
|
59
|
+
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
|
60
|
+
a=extmap:3 urn:3gpp:video-orientation
|
|
61
|
+
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
|
62
|
+
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
|
|
63
|
+
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
|
|
64
|
+
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
|
|
65
|
+
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
|
|
66
|
+
a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
|
|
67
|
+
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
|
|
68
|
+
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
|
|
69
|
+
a=extmap:12 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension
|
|
70
|
+
a=extmap:14 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00
|
|
71
|
+
a=setup:actpass
|
|
72
|
+
a=mid:0
|
|
73
|
+
a=msid:52fafc21-b8bb-4f4f-8072-86a29cb6590e 1bd1c5c2-d3cc-4490-ac0c-70b187242232
|
|
74
|
+
a=sendonly
|
|
75
|
+
a=ice-ufrag:LvRk
|
|
76
|
+
a=ice-pwd:IpBRr2Rrg9TkOgayjYqALhPY
|
|
77
|
+
a=fingerprint:sha-256 18:DE:8F:ED:E6:A2:0C:99:A8:25:AB:C9:F8:3D:91:4C:3E:9F:B4:1F:22:87:A7:3C:85:8F:F3:51:09:A7:E3:FA
|
|
78
|
+
a=ice-options:trickle
|
|
79
|
+
a=ssrc:3192778601 cname:yYSN5R+RG2j3luO7
|
|
80
|
+
a=ssrc:3192778601 msid:52fafc21-b8bb-4f4f-8072-86a29cb6590e 1bd1c5c2-d3cc-4490-ac0c-70b187242232
|
|
81
|
+
a=ssrc:283365205 cname:yYSN5R+RG2j3luO7
|
|
82
|
+
a=ssrc:283365205 msid:52fafc21-b8bb-4f4f-8072-86a29cb6590e 1bd1c5c2-d3cc-4490-ac0c-70b187242232
|
|
83
|
+
a=ssrc-group:FID 3192778601 283365205
|
|
84
|
+
a=rtcp-mux
|
|
85
|
+
a=rtcp-rsize`;
|
|
86
|
+
const target = preserveCodec(sdp, '0', {
|
|
87
|
+
mimeType: 'video/VP9',
|
|
88
|
+
clockRate: 90000,
|
|
89
|
+
sdpFmtpLine: 'profile-id=0',
|
|
90
|
+
});
|
|
91
|
+
expect(target).toContain('VP9');
|
|
92
|
+
expect(target).not.toContain('profile-id=2');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('handles ios munging', () => {
|
|
96
|
+
const sdp = `v=0
|
|
97
|
+
o=- 525780719364332676 2 IN IP4 127.0.0.1
|
|
98
|
+
s=-
|
|
99
|
+
t=0 0
|
|
100
|
+
a=group:BUNDLE 0
|
|
101
|
+
a=extmap-allow-mixed
|
|
102
|
+
a=msid-semantic: WMS BF3AFE62-88F8-4189-99D7-7CAE159205E3
|
|
103
|
+
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 103 35 36 104 105 106
|
|
104
|
+
c=IN IP4 0.0.0.0
|
|
105
|
+
a=rtcp:9 IN IP4 0.0.0.0
|
|
106
|
+
a=ice-ufrag:SAkq
|
|
107
|
+
a=ice-pwd:FYHHro0VWRO8CjI/M1VG5vRw
|
|
108
|
+
a=ice-options:trickle renomination
|
|
109
|
+
a=fingerprint:sha-256 03:5B:16:0E:E1:7B:FE:4F:9A:5C:AC:CF:08:21:4B:49:CE:53:79:E6:97:AE:4E:73:F8:43:34:C3:11:F7:6D:E7
|
|
110
|
+
a=setup:actpass
|
|
111
|
+
a=mid:0
|
|
112
|
+
a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
|
|
113
|
+
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
|
114
|
+
a=extmap:3 urn:3gpp:video-orientation
|
|
115
|
+
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
|
116
|
+
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
|
|
117
|
+
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
|
|
118
|
+
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
|
|
119
|
+
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
|
|
120
|
+
a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
|
|
121
|
+
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
|
|
122
|
+
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
|
|
123
|
+
a=extmap:12 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension
|
|
124
|
+
a=extmap:14 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00
|
|
125
|
+
a=sendonly
|
|
126
|
+
a=msid:BF3AFE62-88F8-4189-99D7-7CAE159205E3 6013DC02-A0A5-43A9-9D41-9D4A89648A42
|
|
127
|
+
a=rtcp-mux
|
|
128
|
+
a=rtcp-rsize
|
|
129
|
+
a=rtpmap:96 H264/90000
|
|
130
|
+
a=rtcp-fb:96 goog-remb
|
|
131
|
+
a=rtcp-fb:96 transport-cc
|
|
132
|
+
a=rtcp-fb:96 ccm fir
|
|
133
|
+
a=rtcp-fb:96 nack
|
|
134
|
+
a=rtcp-fb:96 nack pli
|
|
135
|
+
a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c29
|
|
136
|
+
a=rtpmap:97 rtx/90000
|
|
137
|
+
a=fmtp:97 apt=96
|
|
138
|
+
a=rtpmap:98 H264/90000
|
|
139
|
+
a=rtcp-fb:98 goog-remb
|
|
140
|
+
a=rtcp-fb:98 transport-cc
|
|
141
|
+
a=rtcp-fb:98 ccm fir
|
|
142
|
+
a=rtcp-fb:98 nack
|
|
143
|
+
a=rtcp-fb:98 nack pli
|
|
144
|
+
a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e029
|
|
145
|
+
a=rtpmap:99 rtx/90000
|
|
146
|
+
a=fmtp:99 apt=98
|
|
147
|
+
a=rtpmap:100 VP8/90000
|
|
148
|
+
a=rtcp-fb:100 goog-remb
|
|
149
|
+
a=rtcp-fb:100 transport-cc
|
|
150
|
+
a=rtcp-fb:100 ccm fir
|
|
151
|
+
a=rtcp-fb:100 nack
|
|
152
|
+
a=rtcp-fb:100 nack pli
|
|
153
|
+
a=rtpmap:101 rtx/90000
|
|
154
|
+
a=fmtp:101 apt=100
|
|
155
|
+
a=rtpmap:127 VP9/90000
|
|
156
|
+
a=rtcp-fb:127 goog-remb
|
|
157
|
+
a=rtcp-fb:127 transport-cc
|
|
158
|
+
a=rtcp-fb:127 ccm fir
|
|
159
|
+
a=rtcp-fb:127 nack
|
|
160
|
+
a=rtcp-fb:127 nack pli
|
|
161
|
+
a=rtpmap:103 rtx/90000
|
|
162
|
+
a=fmtp:103 apt=127
|
|
163
|
+
a=rtpmap:35 AV1/90000
|
|
164
|
+
a=rtcp-fb:35 goog-remb
|
|
165
|
+
a=rtcp-fb:35 transport-cc
|
|
166
|
+
a=rtcp-fb:35 ccm fir
|
|
167
|
+
a=rtcp-fb:35 nack
|
|
168
|
+
a=rtcp-fb:35 nack pli
|
|
169
|
+
a=rtpmap:36 rtx/90000
|
|
170
|
+
a=fmtp:36 apt=35
|
|
171
|
+
a=rtpmap:104 red/90000
|
|
172
|
+
a=rtpmap:105 rtx/90000
|
|
173
|
+
a=fmtp:105 apt=104
|
|
174
|
+
a=rtpmap:106 ulpfec/90000
|
|
175
|
+
a=rid:q send
|
|
176
|
+
a=rid:h send
|
|
177
|
+
a=rid:f send
|
|
178
|
+
a=simulcast:send q;h;f`;
|
|
179
|
+
const target = preserveCodec(sdp, '0', {
|
|
180
|
+
mimeType: 'video/H264',
|
|
181
|
+
clockRate: 90000,
|
|
182
|
+
sdpFmtpLine:
|
|
183
|
+
'profile-level-id=42e029;packetization-mode=1;level-asymmetry-allowed=1',
|
|
184
|
+
});
|
|
185
|
+
expect(target).toContain('H264');
|
|
186
|
+
expect(target).toContain('profile-level-id=42e029');
|
|
187
|
+
expect(target).not.toContain('profile-level-id=640c29');
|
|
188
|
+
expect(target).not.toContain('VP9');
|
|
189
|
+
expect(target).not.toContain('AV1');
|
|
190
|
+
});
|
|
24
191
|
});
|
|
@@ -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.
|