@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.
@@ -21,8 +21,7 @@ export declare class StreamVideoClient {
21
21
  protected readonly writeableStateStore: StreamVideoWriteableStateStore;
22
22
  streamClient: StreamClient;
23
23
  protected eventHandlersToUnregister: Array<() => void>;
24
- protected connectionPromise: Promise<void | ConnectedEvent> | undefined;
25
- protected disconnectionPromise: Promise<void> | undefined;
24
+ private readonly connectionConcurrencyTag;
26
25
  private static _instanceMap;
27
26
  /**
28
27
  * You should create only one instance of `StreamVideoClient`.
@@ -159,5 +158,5 @@ export declare class StreamVideoClient {
159
158
  * @param user the user to connect.
160
159
  * @param tokenOrProvider a token or a function that returns a token.
161
160
  */
162
- protected connectAnonymousUser: (user: UserWithId, tokenOrProvider: TokenOrProvider) => Promise<void | ConnectedEvent>;
161
+ protected connectAnonymousUser: (user: UserWithId, tokenOrProvider: TokenOrProvider) => Promise<void>;
163
162
  }
@@ -4,7 +4,7 @@ import { TokenManager } from './token_manager';
4
4
  import { WSConnectionFallback } from './connection_fallback';
5
5
  import { AllClientEvents, AllClientEventTypes, APIErrorResponse, ClientEventListener, ConnectAPIResponse, ErrorFromResponse, Logger, StreamClientOptions, StreamVideoEvent, TokenOrProvider, User, UserWithId } from './types';
6
6
  import { InsightMetrics } from './insights';
7
- import { CreateGuestResponse } from '../../gen/coordinator';
7
+ import { ConnectedEvent, CreateGuestResponse } from '../../gen/coordinator';
8
8
  export declare class StreamClient {
9
9
  _user?: UserWithId;
10
10
  anonymous: boolean;
@@ -28,14 +28,14 @@ export declare class StreamClient {
28
28
  wsBaseURL?: string;
29
29
  wsConnection: StableWSConnection | null;
30
30
  wsFallback?: WSConnectionFallback;
31
- wsPromise: ConnectAPIResponse | null;
31
+ private wsPromiseSafe;
32
32
  consecutiveFailures: number;
33
33
  insightMetrics: InsightMetrics;
34
34
  defaultWSTimeoutWithFallback: number;
35
35
  defaultWSTimeout: number;
36
36
  resolveConnectionId?: Function;
37
37
  rejectConnectionId?: Function;
38
- connectionIdPromise?: Promise<string | undefined>;
38
+ private connectionIdPromiseSafe?;
39
39
  guestUserCreatePromise?: Promise<CreateGuestResponse>;
40
40
  /**
41
41
  * Initialize a client.
@@ -64,7 +64,7 @@ export declare class StreamClient {
64
64
  *
65
65
  * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup
66
66
  */
67
- connectUser: (user: UserWithId, userTokenOrProvider: TokenOrProvider) => Promise<void | import("../../gen/coordinator").ConnectedEvent>;
67
+ connectUser: (user: UserWithId, userTokenOrProvider: TokenOrProvider) => Promise<void | ConnectedEvent>;
68
68
  _setToken: (user: UserWithId, userTokenOrProvider: TokenOrProvider, isAnonymous: boolean) => Promise<void>;
69
69
  _setUser: (user: UserWithId) => void;
70
70
  /**
@@ -84,7 +84,7 @@ export declare class StreamClient {
84
84
  /**
85
85
  * Creates a new WebSocket connection with the current user. Returns empty promise, if there is an active connection
86
86
  */
87
- openConnection: () => Promise<void | import("../../gen/coordinator").ConnectedEvent>;
87
+ openConnection: () => Promise<ConnectedEvent | undefined>;
88
88
  /**
89
89
  * Disconnects the websocket and removes the user from client.
90
90
  *
@@ -94,7 +94,7 @@ export declare class StreamClient {
94
94
  disconnectUser: (timeout?: number) => Promise<void>;
95
95
  connectGuestUser: (user: User & {
96
96
  type: "guest";
97
- }) => Promise<void | import("../../gen/coordinator").ConnectedEvent>;
97
+ }) => Promise<void | ConnectedEvent>;
98
98
  /**
99
99
  * connectAnonymousUser - Set an anonymous user and open a WebSocket connection
100
100
  */
@@ -117,7 +117,10 @@ export declare class StreamClient {
117
117
  /**
118
118
  * sets up the this.connectionIdPromise
119
119
  */
120
- _setupConnectionIdPromise: () => Promise<void>;
120
+ _setupConnectionIdPromise: () => void;
121
+ get connectionIdPromise(): Promise<string | undefined> | undefined;
122
+ get isConnectionIsPromisePending(): boolean;
123
+ get wsPromise(): Promise<ConnectedEvent | undefined> | undefined;
121
124
  _logApiRequest: (type: string, url: string, data: unknown, config: AxiosRequestConfig & {
122
125
  config?: AxiosRequestConfig & {
123
126
  maxBodyLength?: number;
@@ -143,7 +146,7 @@ export declare class StreamClient {
143
146
  /**
144
147
  * @private
145
148
  */
146
- connect: () => Promise<void | import("../../gen/coordinator").ConnectedEvent>;
149
+ connect: () => Promise<ConnectedEvent | undefined>;
147
150
  /**
148
151
  * Check the connectivity with server for warmup purpose.
149
152
  *
@@ -1,6 +1,6 @@
1
1
  import WebSocket from 'isomorphic-ws';
2
2
  import { StreamClient } from './client';
3
- import type { ConnectAPIResponse, LogLevel, UR } from './types';
3
+ import type { LogLevel, UR } from './types';
4
4
  import type { ConnectedEvent } from '../../gen/coordinator';
5
5
  /**
6
6
  * StableWSConnection - A WS connection that reconnects upon failure.
@@ -21,7 +21,7 @@ import type { ConnectedEvent } from '../../gen/coordinator';
21
21
  */
22
22
  export declare class StableWSConnection {
23
23
  connectionID?: string;
24
- connectionOpen?: ConnectAPIResponse;
24
+ private connectionOpenSafe?;
25
25
  authenticationSent: boolean;
26
26
  consecutiveFailures: number;
27
27
  pingInterval: number;
@@ -52,13 +52,13 @@ export declare class StableWSConnection {
52
52
  * the default 15s timeout allows between 2~3 tries
53
53
  * @return {ConnectAPIResponse<ConnectedEvent>} Promise that completes once the first health check message is received
54
54
  */
55
- connect(timeout?: number): Promise<void | ConnectedEvent>;
55
+ connect(timeout?: number): Promise<ConnectedEvent | undefined>;
56
56
  /**
57
57
  * _waitForHealthy polls the promise connection to see if its resolved until it times out
58
58
  * the default 15s timeout allows between 2~3 tries
59
59
  * @param timeout duration(ms)
60
60
  */
61
- _waitForHealthy(timeout?: number): Promise<void | ConnectedEvent>;
61
+ _waitForHealthy(timeout?: number): Promise<ConnectedEvent | undefined>;
62
62
  /**
63
63
  * Builds and returns the url for websocket.
64
64
  * @private
@@ -126,6 +126,7 @@ export declare class StableWSConnection {
126
126
  * _setupPromise - sets up the this.connectOpen promise
127
127
  */
128
128
  _setupConnectionPromise: () => void;
129
+ get connectionOpen(): Promise<ConnectedEvent> | undefined;
129
130
  /**
130
131
  * Schedules a next health check ping for websocket.
131
132
  */
@@ -0,0 +1,18 @@
1
+ export interface SafePromise<T> {
2
+ (): Promise<T>;
3
+ checkPending(): boolean;
4
+ }
5
+ /**
6
+ * Saving a long-lived reference to a promise that can reject can be unsafe,
7
+ * since rejecting the promise causes an unhandled rejection error (even if the
8
+ * rejection is handled everywhere promise result is expected).
9
+ *
10
+ * To avoid that, we add both resolution and rejection handlers to the promise.
11
+ * That way, the saved promise never rejects. A callback is provided as return
12
+ * value to build a *new* promise, that resolves and rejects along with
13
+ * the original promise.
14
+ * @param promise Promise to wrap, which possibly rejects
15
+ * @returns Callback to build a new promise, which resolves and rejects along
16
+ * with the original promise
17
+ */
18
+ export declare function makeSafePromise<T>(promise: Promise<T>): SafePromise<T>;
@@ -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.
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.11.3",
3
+ "version": "1.11.5",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -30,6 +30,7 @@ import {
30
30
  import { getLogger, logToConsole, setLogger } from './logger';
31
31
  import { getSdkInfo } from './client-details';
32
32
  import { SdkType } from './gen/video/sfu/models/models';
33
+ import { withoutConcurrency } from './helpers/concurrency';
33
34
 
34
35
  /**
35
36
  * A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
@@ -51,8 +52,9 @@ export class StreamVideoClient {
51
52
  streamClient: StreamClient;
52
53
 
53
54
  protected eventHandlersToUnregister: Array<() => void> = [];
54
- protected connectionPromise: Promise<void | ConnectedEvent> | undefined;
55
- protected disconnectionPromise: Promise<void> | undefined;
55
+ private readonly connectionConcurrencyTag = Symbol(
56
+ 'connectionConcurrencyTag',
57
+ );
56
58
 
57
59
  private static _instanceMap: Map<string, StreamVideoClient> = new Map();
58
60
 
@@ -209,12 +211,11 @@ export class StreamVideoClient {
209
211
  return this.streamClient.connectGuestUser(user);
210
212
  };
211
213
  }
212
- this.connectionPromise = this.disconnectionPromise
213
- ? this.disconnectionPromise.then(() => connectUser())
214
- : connectUser();
215
214
 
216
- this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
217
- const connectUserResponse = await this.connectionPromise;
215
+ const connectUserResponse = await withoutConcurrency(
216
+ this.connectionConcurrencyTag,
217
+ () => connectUser(),
218
+ );
218
219
  // connectUserResponse will be void if connectUser called twice for the same user
219
220
  if (connectUserResponse?.me) {
220
221
  this.writeableStateStore.setConnectedUser(connectUserResponse.me);
@@ -316,19 +317,15 @@ export class StreamVideoClient {
316
317
  * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
317
318
  */
318
319
  disconnectUser = async (timeout?: number) => {
319
- if (!this.streamClient.user && !this.connectionPromise) {
320
+ if (!this.streamClient.user) {
320
321
  return;
321
322
  }
322
323
  const userId = this.streamClient.user?.id;
323
324
  const apiKey = this.streamClient.key;
324
325
  const disconnectUser = () => this.streamClient.disconnectUser(timeout);
325
- this.disconnectionPromise = this.connectionPromise
326
- ? this.connectionPromise.then(() => disconnectUser())
327
- : disconnectUser();
328
- this.disconnectionPromise.finally(
329
- () => (this.disconnectionPromise = undefined),
326
+ await withoutConcurrency(this.connectionConcurrencyTag, () =>
327
+ disconnectUser(),
330
328
  );
331
- await this.disconnectionPromise;
332
329
  if (userId) {
333
330
  StreamVideoClient._instanceMap.delete(apiKey + userId);
334
331
  }
@@ -556,10 +553,8 @@ export class StreamVideoClient {
556
553
  ) => {
557
554
  const connectAnonymousUser = () =>
558
555
  this.streamClient.connectAnonymousUser(user, tokenOrProvider);
559
- this.connectionPromise = this.disconnectionPromise
560
- ? this.disconnectionPromise.then(() => connectAnonymousUser())
561
- : connectAnonymousUser();
562
- this.connectionPromise.finally(() => (this.connectionPromise = undefined));
563
- return this.connectionPromise;
556
+ return await withoutConcurrency(this.connectionConcurrencyTag, () =>
557
+ connectAnonymousUser(),
558
+ );
564
559
  };
565
560
  }
@@ -271,8 +271,8 @@ describe('muting logic', () => {
271
271
  .mockImplementation(() => Promise.resolve({ duration: '0ms' }));
272
272
  });
273
273
 
274
- it('should mute self', () => {
275
- call.muteSelf('audio');
274
+ it('should mute self', async () => {
275
+ await call.muteSelf('audio');
276
276
 
277
277
  expect(spy).toHaveBeenCalledWith(userId, 'audio');
278
278
  });
@@ -38,7 +38,12 @@ import {
38
38
  } from './types';
39
39
  import { InsightMetrics, postInsights } from './insights';
40
40
  import { getLocationHint } from './location';
41
- import { CreateGuestRequest, CreateGuestResponse } from '../../gen/coordinator';
41
+ import {
42
+ ConnectedEvent,
43
+ CreateGuestRequest,
44
+ CreateGuestResponse,
45
+ } from '../../gen/coordinator';
46
+ import { makeSafePromise, type SafePromise } from '../../helpers/promise';
42
47
 
43
48
  export class StreamClient {
44
49
  _user?: UserWithId;
@@ -67,14 +72,14 @@ export class StreamClient {
67
72
  wsBaseURL?: string;
68
73
  wsConnection: StableWSConnection | null;
69
74
  wsFallback?: WSConnectionFallback;
70
- wsPromise: ConnectAPIResponse | null;
75
+ private wsPromiseSafe: SafePromise<ConnectedEvent | undefined> | null;
71
76
  consecutiveFailures: number;
72
77
  insightMetrics: InsightMetrics;
73
78
  defaultWSTimeoutWithFallback: number;
74
79
  defaultWSTimeout: number;
75
80
  resolveConnectionId?: Function;
76
81
  rejectConnectionId?: Function;
77
- connectionIdPromise?: Promise<string | undefined>;
82
+ private connectionIdPromiseSafe?: SafePromise<string | undefined>;
78
83
  guestUserCreatePromise?: Promise<CreateGuestResponse>;
79
84
 
80
85
  /**
@@ -155,7 +160,7 @@ export class StreamClient {
155
160
 
156
161
  // WS connection is initialized when setUser is called
157
162
  this.wsConnection = null;
158
- this.wsPromise = null;
163
+ this.wsPromiseSafe = null;
159
164
  this.setUserPromise = null;
160
165
 
161
166
  // mapping between channel groups and configs
@@ -340,12 +345,13 @@ export class StreamClient {
340
345
  );
341
346
  }
342
347
 
343
- if (this.wsConnection?.isConnecting && this.wsPromise) {
348
+ const wsPromise = this.wsPromiseSafe?.();
349
+ if (this.wsConnection?.isConnecting && wsPromise) {
344
350
  this.logger(
345
351
  'info',
346
352
  'client:openConnection() - connection already in progress',
347
353
  );
348
- return this.wsPromise;
354
+ return await wsPromise;
349
355
  }
350
356
 
351
357
  if (
@@ -357,14 +363,15 @@ export class StreamClient {
357
363
  'client:openConnection() - openConnection called twice, healthy connection already exists',
358
364
  );
359
365
 
360
- return Promise.resolve();
366
+ return;
361
367
  }
362
368
 
363
369
  this._setupConnectionIdPromise();
364
370
 
365
371
  this.clientID = `${this.userID}--${randomId()}`;
366
- this.wsPromise = this.connect();
367
- return this.wsPromise;
372
+ const newWsPromise = this.connect();
373
+ this.wsPromiseSafe = makeSafePromise(newWsPromise);
374
+ return await newWsPromise;
368
375
  };
369
376
 
370
377
  /**
@@ -388,7 +395,7 @@ export class StreamClient {
388
395
 
389
396
  this.tokenManager.reset();
390
397
 
391
- this.connectionIdPromise = undefined;
398
+ this.connectionIdPromiseSafe = undefined;
392
399
  this.rejectConnectionId = undefined;
393
400
  this.resolveConnectionId = undefined;
394
401
  };
@@ -481,16 +488,28 @@ export class StreamClient {
481
488
  /**
482
489
  * sets up the this.connectionIdPromise
483
490
  */
484
- _setupConnectionIdPromise = async () => {
491
+ _setupConnectionIdPromise = () => {
485
492
  /** a promise that is resolved once connection id is set */
486
- this.connectionIdPromise = new Promise<string | undefined>(
487
- (resolve, reject) => {
493
+ this.connectionIdPromiseSafe = makeSafePromise(
494
+ new Promise<string | undefined>((resolve, reject) => {
488
495
  this.resolveConnectionId = resolve;
489
496
  this.rejectConnectionId = reject;
490
- },
497
+ }),
491
498
  );
492
499
  };
493
500
 
501
+ get connectionIdPromise() {
502
+ return this.connectionIdPromiseSafe?.();
503
+ }
504
+
505
+ get isConnectionIsPromisePending() {
506
+ return this.connectionIdPromiseSafe?.checkPending() ?? false;
507
+ }
508
+
509
+ get wsPromise() {
510
+ return this.wsPromiseSafe?.();
511
+ }
512
+
494
513
  _logApiRequest = (
495
514
  type: string,
496
515
  url: string,
@@ -8,20 +8,15 @@ import {
8
8
  import {
9
9
  addConnectionEventListeners,
10
10
  convertErrorToJson,
11
- isPromisePending,
12
11
  KnownCodes,
13
12
  randomId,
14
13
  removeConnectionEventListeners,
15
14
  retryInterval,
16
15
  sleep,
17
16
  } from './utils';
18
- import type {
19
- ConnectAPIResponse,
20
- LogLevel,
21
- StreamVideoEvent,
22
- UR,
23
- } from './types';
17
+ import type { LogLevel, StreamVideoEvent, UR } from './types';
24
18
  import type { ConnectedEvent, WSAuthMessage } from '../../gen/coordinator';
19
+ import { makeSafePromise, type SafePromise } from '../../helpers/promise';
25
20
 
26
21
  // Type guards to check WebSocket error type
27
22
  const isCloseEvent = (
@@ -54,7 +49,7 @@ const isErrorEvent = (
54
49
  export class StableWSConnection {
55
50
  // local vars
56
51
  connectionID?: string;
57
- connectionOpen?: ConnectAPIResponse;
52
+ private connectionOpenSafe?: SafePromise<ConnectedEvent>;
58
53
  authenticationSent: boolean;
59
54
  consecutiveFailures: number;
60
55
  pingInterval: number;
@@ -338,13 +333,7 @@ export class StableWSConnection {
338
333
  await this.client.tokenManager.loadToken();
339
334
  }
340
335
 
341
- let mustSetupConnectionIdPromise = true;
342
- if (this.client.connectionIdPromise) {
343
- if (await isPromisePending(this.client.connectionIdPromise)) {
344
- mustSetupConnectionIdPromise = false;
345
- }
346
- }
347
- if (mustSetupConnectionIdPromise) {
336
+ if (!this.client.isConnectionIsPromisePending) {
348
337
  this.client._setupConnectionIdPromise();
349
338
  }
350
339
  this._setupConnectionPromise();
@@ -747,12 +736,18 @@ export class StableWSConnection {
747
736
  _setupConnectionPromise = () => {
748
737
  this.isResolved = false;
749
738
  /** a promise that is resolved once ws.open is called */
750
- this.connectionOpen = new Promise<ConnectedEvent>((resolve, reject) => {
751
- this.resolvePromise = resolve;
752
- this.rejectPromise = reject;
753
- });
739
+ this.connectionOpenSafe = makeSafePromise(
740
+ new Promise<ConnectedEvent>((resolve, reject) => {
741
+ this.resolvePromise = resolve;
742
+ this.rejectPromise = reject;
743
+ }),
744
+ );
754
745
  };
755
746
 
747
+ get connectionOpen() {
748
+ return this.connectionOpenSafe?.();
749
+ }
750
+
756
751
  /**
757
752
  * Schedules a next health check ping for websocket.
758
753
  */
@@ -1,5 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { enableHighQualityAudio, toggleDtx } from '../sdp-munging';
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
  });