livekit-client 0.15.3 → 0.15.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.
Files changed (74) hide show
  1. package/dist/api/SignalClient.d.ts +3 -1
  2. package/dist/api/SignalClient.js +59 -25
  3. package/dist/api/SignalClient.js.map +1 -1
  4. package/dist/options.d.ts +5 -0
  5. package/dist/proto/livekit_models.d.ts +30 -0
  6. package/dist/proto/livekit_models.js +219 -1
  7. package/dist/proto/livekit_models.js.map +1 -1
  8. package/dist/room/RTCEngine.d.ts +2 -0
  9. package/dist/room/RTCEngine.js +45 -2
  10. package/dist/room/RTCEngine.js.map +1 -1
  11. package/dist/room/Room.js +4 -0
  12. package/dist/room/Room.js.map +1 -1
  13. package/dist/room/participant/LocalParticipant.js +2 -1
  14. package/dist/room/participant/LocalParticipant.js.map +1 -1
  15. package/dist/room/participant/publishUtils.js +1 -1
  16. package/dist/room/participant/publishUtils.js.map +1 -1
  17. package/dist/room/participant/publishUtils.test.js +9 -0
  18. package/dist/room/participant/publishUtils.test.js.map +1 -1
  19. package/dist/room/track/RemoteTrackPublication.d.ts +1 -0
  20. package/dist/room/track/RemoteTrackPublication.js +15 -7
  21. package/dist/room/track/RemoteTrackPublication.js.map +1 -1
  22. package/dist/room/track/create.js +5 -0
  23. package/dist/room/track/create.js.map +1 -1
  24. package/dist/room/utils.d.ts +2 -0
  25. package/dist/room/utils.js +32 -1
  26. package/dist/room/utils.js.map +1 -1
  27. package/dist/version.d.ts +1 -1
  28. package/dist/version.js +1 -1
  29. package/package.json +4 -2
  30. package/src/api/SignalClient.ts +434 -0
  31. package/src/connect.ts +100 -0
  32. package/src/index.ts +47 -0
  33. package/src/logger.ts +22 -0
  34. package/src/options.ts +152 -0
  35. package/src/proto/livekit_models.ts +1863 -0
  36. package/src/proto/livekit_rtc.ts +3401 -0
  37. package/src/room/DeviceManager.ts +57 -0
  38. package/src/room/PCTransport.ts +86 -0
  39. package/src/room/RTCEngine.ts +484 -0
  40. package/src/room/Room.ts +785 -0
  41. package/src/room/errors.ts +65 -0
  42. package/src/room/events.ts +396 -0
  43. package/src/room/participant/LocalParticipant.ts +685 -0
  44. package/src/room/participant/Participant.ts +214 -0
  45. package/src/room/participant/ParticipantTrackPermission.ts +32 -0
  46. package/src/room/participant/RemoteParticipant.ts +238 -0
  47. package/src/room/participant/publishUtils.test.ts +105 -0
  48. package/src/room/participant/publishUtils.ts +180 -0
  49. package/src/room/stats.ts +130 -0
  50. package/src/room/track/LocalAudioTrack.ts +112 -0
  51. package/src/room/track/LocalTrack.ts +124 -0
  52. package/src/room/track/LocalTrackPublication.ts +63 -0
  53. package/src/room/track/LocalVideoTrack.test.ts +70 -0
  54. package/src/room/track/LocalVideoTrack.ts +416 -0
  55. package/src/room/track/RemoteAudioTrack.ts +58 -0
  56. package/src/room/track/RemoteTrack.ts +59 -0
  57. package/src/room/track/RemoteTrackPublication.ts +192 -0
  58. package/src/room/track/RemoteVideoTrack.ts +213 -0
  59. package/src/room/track/Track.ts +301 -0
  60. package/src/room/track/TrackPublication.ts +120 -0
  61. package/src/room/track/create.ts +120 -0
  62. package/src/room/track/defaults.ts +23 -0
  63. package/src/room/track/options.ts +229 -0
  64. package/src/room/track/types.ts +8 -0
  65. package/src/room/track/utils.test.ts +93 -0
  66. package/src/room/track/utils.ts +76 -0
  67. package/src/room/utils.ts +74 -0
  68. package/src/version.ts +2 -0
  69. package/.github/workflows/publish.yaml +0 -55
  70. package/.github/workflows/test.yaml +0 -36
  71. package/example/index.html +0 -247
  72. package/example/sample.ts +0 -632
  73. package/example/styles.css +0 -144
  74. package/example/webpack.config.js +0 -33
@@ -0,0 +1,192 @@
1
+ import log from '../../logger';
2
+ import { TrackInfo, VideoQuality } from '../../proto/livekit_models';
3
+ import {
4
+ UpdateSubscription,
5
+ UpdateTrackSettings,
6
+ } from '../../proto/livekit_rtc';
7
+ import { TrackEvent } from '../events';
8
+ import RemoteVideoTrack from './RemoteVideoTrack';
9
+ import { Track } from './Track';
10
+ import { TrackPublication } from './TrackPublication';
11
+ import { RemoteTrack } from './types';
12
+
13
+ export default class RemoteTrackPublication extends TrackPublication {
14
+ track?: RemoteTrack;
15
+
16
+ /** @internal */
17
+ _allowed = true;
18
+
19
+ // keeps track of client's desire to subscribe to a track
20
+ protected subscribed?: boolean;
21
+
22
+ protected disabled: boolean = false;
23
+
24
+ protected currentVideoQuality?: VideoQuality = VideoQuality.HIGH;
25
+
26
+ protected videoDimensions?: Track.Dimensions;
27
+
28
+ /**
29
+ * Subscribe or unsubscribe to this remote track
30
+ * @param subscribed true to subscribe to a track, false to unsubscribe
31
+ */
32
+ setSubscribed(subscribed: boolean) {
33
+ this.subscribed = subscribed;
34
+
35
+ const sub: UpdateSubscription = {
36
+ trackSids: [this.trackSid],
37
+ subscribe: this.subscribed,
38
+ participantTracks: [],
39
+ };
40
+ this.emit(TrackEvent.UpdateSubscription, sub);
41
+ }
42
+
43
+ get subscriptionStatus(): TrackPublication.SubscriptionStatus {
44
+ if (this.subscribed === false || !super.isSubscribed) {
45
+ return TrackPublication.SubscriptionStatus.Unsubscribed;
46
+ }
47
+ if (!this._allowed) {
48
+ return TrackPublication.SubscriptionStatus.NotAllowed;
49
+ }
50
+ return TrackPublication.SubscriptionStatus.Subscribed;
51
+ }
52
+
53
+ /**
54
+ * Returns true if track is subscribed, and ready for playback
55
+ */
56
+ get isSubscribed(): boolean {
57
+ if (this.subscribed === false) {
58
+ return false;
59
+ }
60
+ if (!this._allowed) {
61
+ return false;
62
+ }
63
+ return super.isSubscribed;
64
+ }
65
+
66
+ get isEnabled(): boolean {
67
+ return !this.disabled;
68
+ }
69
+
70
+ /**
71
+ * disable server from sending down data for this track. this is useful when
72
+ * the participant is off screen, you may disable streaming down their video
73
+ * to reduce bandwidth requirements
74
+ * @param enabled
75
+ */
76
+ setEnabled(enabled: boolean) {
77
+ if (!this.isManualOperationAllowed() || this.disabled === !enabled) {
78
+ return;
79
+ }
80
+ this.disabled = !enabled;
81
+
82
+ this.emitTrackUpdate();
83
+ }
84
+
85
+ /**
86
+ * for tracks that support simulcasting, adjust subscribed quality
87
+ *
88
+ * This indicates the highest quality the client can accept. if network
89
+ * bandwidth does not allow, server will automatically reduce quality to
90
+ * optimize for uninterrupted video
91
+ */
92
+ setVideoQuality(quality: VideoQuality) {
93
+ if (!this.isManualOperationAllowed() || this.currentVideoQuality === quality) {
94
+ return;
95
+ }
96
+ this.currentVideoQuality = quality;
97
+ this.videoDimensions = undefined;
98
+
99
+ this.emitTrackUpdate();
100
+ }
101
+
102
+ setVideoDimensions(dimensions: Track.Dimensions) {
103
+ if (!this.isManualOperationAllowed()) {
104
+ return;
105
+ }
106
+ if (this.videoDimensions?.width === dimensions.width
107
+ && this.videoDimensions?.height === dimensions.height) {
108
+ return;
109
+ }
110
+ if (this.track instanceof RemoteVideoTrack) { this.videoDimensions = dimensions; }
111
+ this.currentVideoQuality = undefined;
112
+
113
+ this.emitTrackUpdate();
114
+ }
115
+
116
+ get videoQuality(): VideoQuality | undefined {
117
+ return this.currentVideoQuality;
118
+ }
119
+
120
+ setTrack(track?: Track) {
121
+ if (this.track) {
122
+ // unregister listener
123
+ this.track.off(TrackEvent.VideoDimensionsChanged, this.handleVideoDimensionsChange);
124
+ this.track.off(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
125
+ this.track.off(TrackEvent.Ended, this.handleEnded);
126
+ }
127
+ super.setTrack(track);
128
+ if (track) {
129
+ track.sid = this.trackSid;
130
+ track.on(TrackEvent.VideoDimensionsChanged, this.handleVideoDimensionsChange);
131
+ track.on(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
132
+ track.on(TrackEvent.Ended, this.handleEnded);
133
+ }
134
+ }
135
+
136
+ /** @internal */
137
+ updateInfo(info: TrackInfo) {
138
+ super.updateInfo(info);
139
+ this.metadataMuted = info.muted;
140
+ this.track?.setMuted(info.muted);
141
+ }
142
+
143
+ private isManualOperationAllowed(): boolean {
144
+ if (this.isAdaptiveStream) {
145
+ log.warn('adaptive stream is enabled, cannot change track settings', this.trackSid);
146
+ return false;
147
+ }
148
+ if (!this.isSubscribed) {
149
+ log.warn('cannot update track settings when not subscribed', this.trackSid);
150
+ return false;
151
+ }
152
+ return true;
153
+ }
154
+
155
+ protected handleEnded = (track: RemoteTrack) => {
156
+ this.emit(TrackEvent.Ended, track);
157
+ };
158
+
159
+ protected get isAdaptiveStream(): boolean {
160
+ return this.track instanceof RemoteVideoTrack && this.track.isAdaptiveStream;
161
+ }
162
+
163
+ protected handleVisibilityChange = (visible: boolean) => {
164
+ log.debug('adaptivestream video visibility', this.trackSid, `visible=${visible}`);
165
+ this.disabled = !visible;
166
+ this.emitTrackUpdate();
167
+ };
168
+
169
+ protected handleVideoDimensionsChange = (dimensions: Track.Dimensions) => {
170
+ log.debug('adaptivestream video dimensions', this.trackSid, `${dimensions.width}x${dimensions.height}`);
171
+ this.videoDimensions = dimensions;
172
+ this.emitTrackUpdate();
173
+ };
174
+
175
+ protected emitTrackUpdate() {
176
+ const settings: UpdateTrackSettings = UpdateTrackSettings.fromPartial({
177
+ trackSids: [this.trackSid],
178
+ disabled: this.disabled,
179
+ });
180
+ if (this.videoDimensions) {
181
+ settings.width = this.videoDimensions.width;
182
+ settings.height = this.videoDimensions.height;
183
+ } else if (this.currentVideoQuality !== undefined) {
184
+ settings.quality = this.currentVideoQuality;
185
+ } else {
186
+ // defaults to high quality
187
+ settings.quality = VideoQuality.HIGH;
188
+ }
189
+
190
+ this.emit(TrackEvent.UpdateSettings, settings);
191
+ }
192
+ }
@@ -0,0 +1,213 @@
1
+ import { debounce } from 'ts-debounce';
2
+ import { TrackEvent } from '../events';
3
+ import { computeBitrate, monitorFrequency, VideoReceiverStats } from '../stats';
4
+ import { getIntersectionObserver, getResizeObserver, ObservableMediaElement } from '../utils';
5
+ import RemoteTrack from './RemoteTrack';
6
+ import { attachToElement, detachTrack, Track } from './Track';
7
+
8
+ const REACTION_DELAY = 100;
9
+
10
+ export default class RemoteVideoTrack extends RemoteTrack {
11
+ /** @internal */
12
+ receiver?: RTCRtpReceiver;
13
+
14
+ private prevStats?: VideoReceiverStats;
15
+
16
+ private elementInfos: ElementInfo[] = [];
17
+
18
+ private adaptiveStream?: boolean;
19
+
20
+ private lastVisible?: boolean;
21
+
22
+ private lastDimensions?: Track.Dimensions;
23
+
24
+ constructor(
25
+ mediaTrack: MediaStreamTrack,
26
+ sid: string,
27
+ receiver?: RTCRtpReceiver,
28
+ adaptiveStream?: boolean,
29
+ ) {
30
+ super(mediaTrack, sid, Track.Kind.Video, receiver);
31
+ this.adaptiveStream = adaptiveStream;
32
+ }
33
+
34
+ get isAdaptiveStream(): boolean {
35
+ return this.adaptiveStream ?? false;
36
+ }
37
+
38
+ /** @internal */
39
+ setMuted(muted: boolean) {
40
+ super.setMuted(muted);
41
+
42
+ this.attachedElements.forEach((element) => {
43
+ // detach or attach
44
+ if (muted) {
45
+ detachTrack(this.mediaStreamTrack, element);
46
+ } else {
47
+ attachToElement(this.mediaStreamTrack, element);
48
+ }
49
+ });
50
+ }
51
+
52
+ attach(): HTMLMediaElement;
53
+ attach(element: HTMLMediaElement): HTMLMediaElement;
54
+ attach(element?: HTMLMediaElement): HTMLMediaElement {
55
+ if (!element) {
56
+ element = super.attach();
57
+ } else {
58
+ super.attach(element);
59
+ }
60
+
61
+ if (this.adaptiveStream) {
62
+ this.elementInfos.push({
63
+ element,
64
+ visible: true, // default visible
65
+ });
66
+
67
+ (element as ObservableMediaElement)
68
+ .handleResize = this.debouncedHandleResize;
69
+ (element as ObservableMediaElement)
70
+ .handleVisibilityChanged = this.handleVisibilityChanged;
71
+
72
+ getIntersectionObserver().observe(element);
73
+ getResizeObserver().observe(element);
74
+ }
75
+ return element;
76
+ }
77
+
78
+ detach(): HTMLMediaElement[];
79
+ detach(element: HTMLMediaElement): HTMLMediaElement;
80
+ detach(element?: HTMLMediaElement): HTMLMediaElement | HTMLMediaElement[] {
81
+ let detachedElements: HTMLMediaElement[] = [];
82
+ if (element) {
83
+ this.stopObservingElement(element);
84
+ return super.detach(element);
85
+ }
86
+ detachedElements = super.detach();
87
+
88
+ for (const e of detachedElements) {
89
+ this.stopObservingElement(e);
90
+ }
91
+
92
+ return detachedElements;
93
+ }
94
+
95
+ protected monitorReceiver = async () => {
96
+ if (!this.receiver) {
97
+ this._currentBitrate = 0;
98
+ return;
99
+ }
100
+ const stats = await this.getReceiverStats();
101
+
102
+ if (stats && this.prevStats && this.receiver) {
103
+ this._currentBitrate = computeBitrate(stats, this.prevStats);
104
+ }
105
+
106
+ this.prevStats = stats;
107
+ setTimeout(() => {
108
+ this.monitorReceiver();
109
+ }, monitorFrequency);
110
+ };
111
+
112
+ private async getReceiverStats(): Promise<VideoReceiverStats | undefined> {
113
+ if (!this.receiver) {
114
+ return;
115
+ }
116
+
117
+ const stats = await this.receiver.getStats();
118
+ let receiverStats: VideoReceiverStats | undefined;
119
+ stats.forEach((v) => {
120
+ if (v.type === 'inbound-rtp') {
121
+ receiverStats = {
122
+ type: 'video',
123
+ framesDecoded: v.framesDecoded,
124
+ framesDropped: v.framesDropped,
125
+ framesReceived: v.framesReceived,
126
+ packetsReceived: v.packetsReceived,
127
+ packetsLost: v.packetsLost,
128
+ frameWidth: v.frameWidth,
129
+ frameHeight: v.frameHeight,
130
+ pliCount: v.pliCount,
131
+ firCount: v.firCount,
132
+ nackCount: v.nackCount,
133
+ jitter: v.jitter,
134
+ timestamp: v.timestamp,
135
+ bytesReceived: v.bytesReceived,
136
+ };
137
+ }
138
+ });
139
+ return receiverStats;
140
+ }
141
+
142
+ private stopObservingElement(element: HTMLMediaElement) {
143
+ getIntersectionObserver()?.unobserve(element);
144
+ getResizeObserver()?.unobserve(element);
145
+ this.elementInfos = this.elementInfos.filter((info) => info.element !== element);
146
+ }
147
+
148
+ private handleVisibilityChanged = (entry: IntersectionObserverEntry) => {
149
+ const { target, isIntersecting } = entry;
150
+ const elementInfo = this.elementInfos.find((info) => info.element === target);
151
+ if (elementInfo) {
152
+ elementInfo.visible = isIntersecting;
153
+ elementInfo.visibilityChangedAt = Date.now();
154
+ }
155
+ this.updateVisibility();
156
+ };
157
+
158
+ private readonly debouncedHandleResize = debounce(() => {
159
+ this.updateDimensions();
160
+ }, REACTION_DELAY);
161
+
162
+ private updateVisibility() {
163
+ const lastVisibilityChange = this.elementInfos.reduce(
164
+ (prev, info) => Math.max(prev, info.visibilityChangedAt || 0),
165
+ 0,
166
+ );
167
+ const isVisible = this.elementInfos.some((info) => info.visible);
168
+
169
+ if (this.lastVisible === isVisible) {
170
+ return;
171
+ }
172
+
173
+ if (!isVisible && Date.now() - lastVisibilityChange < REACTION_DELAY) {
174
+ // delay hidden events
175
+ setTimeout(() => {
176
+ this.updateVisibility();
177
+ }, Date.now() - lastVisibilityChange);
178
+ return;
179
+ }
180
+
181
+ this.lastVisible = isVisible;
182
+ this.emit(TrackEvent.VisibilityChanged, isVisible, this);
183
+ }
184
+
185
+ private updateDimensions() {
186
+ let maxWidth = 0;
187
+ let maxHeight = 0;
188
+ for (const info of this.elementInfos) {
189
+ if (info.visible) {
190
+ if (info.element.clientWidth + info.element.clientHeight > maxWidth + maxHeight) {
191
+ maxWidth = info.element.clientWidth;
192
+ maxHeight = info.element.clientHeight;
193
+ }
194
+ }
195
+ }
196
+
197
+ if (this.lastDimensions?.width === maxWidth && this.lastDimensions?.height === maxHeight) {
198
+ return;
199
+ }
200
+
201
+ this.lastDimensions = {
202
+ width: maxWidth,
203
+ height: maxHeight,
204
+ };
205
+ this.emit(TrackEvent.VideoDimensionsChanged, this.lastDimensions, this);
206
+ }
207
+ }
208
+
209
+ interface ElementInfo {
210
+ element: HTMLMediaElement;
211
+ visible: boolean;
212
+ visibilityChangedAt?: number;
213
+ }
@@ -0,0 +1,301 @@
1
+ import { EventEmitter } from 'events';
2
+ import { TrackSource, TrackType } from '../../proto/livekit_models';
3
+ import { StreamState as ProtoStreamState } from '../../proto/livekit_rtc';
4
+ import { TrackEvent } from '../events';
5
+ import { isFireFox } from '../utils';
6
+
7
+ // keep old audio elements when detached, we would re-use them since on iOS
8
+ // Safari tracks which audio elements have been "blessed" by the user.
9
+ const recycledElements: Array<HTMLAudioElement> = [];
10
+
11
+ export class Track extends EventEmitter {
12
+ kind: Track.Kind;
13
+
14
+ mediaStreamTrack: MediaStreamTrack;
15
+
16
+ attachedElements: HTMLMediaElement[] = [];
17
+
18
+ isMuted: boolean = false;
19
+
20
+ streamState: Track.StreamState = Track.StreamState.Active;
21
+
22
+ source: Track.Source;
23
+
24
+ /**
25
+ * sid is set after track is published to server, or if it's a remote track
26
+ */
27
+ sid?: Track.SID;
28
+
29
+ protected _currentBitrate: number = 0;
30
+
31
+ protected constructor(mediaTrack: MediaStreamTrack, kind: Track.Kind) {
32
+ super();
33
+ this.kind = kind;
34
+ this.mediaStreamTrack = mediaTrack;
35
+ this.source = Track.Source.Unknown;
36
+ }
37
+
38
+ /** current receive bits per second */
39
+ get currentBitrate(): number {
40
+ return this._currentBitrate;
41
+ }
42
+
43
+ /**
44
+ * creates a new HTMLAudioElement or HTMLVideoElement, attaches to it, and returns it
45
+ */
46
+ attach(): HTMLMediaElement;
47
+
48
+ /**
49
+ * attaches track to an existing HTMLAudioElement or HTMLVideoElement
50
+ */
51
+ attach(element: HTMLMediaElement): HTMLMediaElement;
52
+ attach(element?: HTMLMediaElement): HTMLMediaElement {
53
+ let elementType = 'audio';
54
+ if (this.kind === Track.Kind.Video) {
55
+ elementType = 'video';
56
+ }
57
+ if (!element) {
58
+ if (elementType === 'audio') {
59
+ recycledElements.forEach((e) => {
60
+ if (e.parentElement === null && !element) {
61
+ element = e;
62
+ }
63
+ });
64
+ if (element) {
65
+ // remove it from pool
66
+ recycledElements.splice(recycledElements.indexOf(element), 1);
67
+ }
68
+ }
69
+ if (!element) {
70
+ element = <HTMLMediaElement>document.createElement(elementType);
71
+ }
72
+ }
73
+
74
+ if (element instanceof HTMLVideoElement) {
75
+ element.playsInline = true;
76
+ element.autoplay = true;
77
+ }
78
+
79
+ // already attached
80
+ if (this.attachedElements.includes(element)) {
81
+ return element;
82
+ }
83
+
84
+ attachToElement(this.mediaStreamTrack, element);
85
+ this.attachedElements.push(element);
86
+
87
+ if (element instanceof HTMLAudioElement) {
88
+ // manually play audio to detect audio playback status
89
+ element.play()
90
+ .then(() => {
91
+ this.emit(TrackEvent.AudioPlaybackStarted);
92
+ })
93
+ .catch((e) => {
94
+ this.emit(TrackEvent.AudioPlaybackFailed, e);
95
+ });
96
+ }
97
+
98
+ return element;
99
+ }
100
+
101
+ /**
102
+ * Detaches from all attached elements
103
+ */
104
+ detach(): HTMLMediaElement[];
105
+
106
+ /**
107
+ * Detach from a single element
108
+ * @param element
109
+ */
110
+ detach(element: HTMLMediaElement): HTMLMediaElement;
111
+ detach(element?: HTMLMediaElement): HTMLMediaElement | HTMLMediaElement[] {
112
+ // detach from a single element
113
+ if (element) {
114
+ detachTrack(this.mediaStreamTrack, element);
115
+ const idx = this.attachedElements.indexOf(element);
116
+ if (idx >= 0) {
117
+ this.attachedElements.splice(idx, 1);
118
+ this.recycleElement(element);
119
+ }
120
+ return element;
121
+ }
122
+
123
+ const detached: HTMLMediaElement[] = [];
124
+ this.attachedElements.forEach((elm) => {
125
+ detachTrack(this.mediaStreamTrack, elm);
126
+ detached.push(elm);
127
+ this.recycleElement(elm);
128
+ });
129
+
130
+ // remove all tracks
131
+ this.attachedElements = [];
132
+ return detached;
133
+ }
134
+
135
+ stop() {
136
+ this.mediaStreamTrack.stop();
137
+ }
138
+
139
+ protected enable() {
140
+ this.mediaStreamTrack.enabled = true;
141
+ }
142
+
143
+ protected disable() {
144
+ this.mediaStreamTrack.enabled = false;
145
+ }
146
+
147
+ private recycleElement(element: HTMLMediaElement) {
148
+ if (element instanceof HTMLAudioElement) {
149
+ // we only need to re-use a single element
150
+ let shouldCache = true;
151
+ element.pause();
152
+ recycledElements.forEach((e) => {
153
+ if (!e.parentElement) {
154
+ shouldCache = false;
155
+ }
156
+ });
157
+ if (shouldCache) {
158
+ recycledElements.push(element);
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ /** @internal */
165
+ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaElement) {
166
+ let mediaStream: MediaStream;
167
+ if (element.srcObject instanceof MediaStream) {
168
+ mediaStream = element.srcObject;
169
+ } else {
170
+ mediaStream = new MediaStream();
171
+ element.srcObject = mediaStream;
172
+ }
173
+
174
+ // remove existing tracks of same type from stream
175
+ let existingTracks: MediaStreamTrack[];
176
+ if (track.kind === 'audio') {
177
+ existingTracks = mediaStream.getAudioTracks();
178
+ } else {
179
+ existingTracks = mediaStream.getVideoTracks();
180
+ }
181
+
182
+ existingTracks.forEach((et) => {
183
+ mediaStream.removeTrack(et);
184
+ });
185
+
186
+ mediaStream.addTrack(track);
187
+ if (isFireFox()) {
188
+ // sometimes firefox doesn't render local video on the first try.
189
+ // It needs to be re-attached after a timeout.
190
+ setTimeout(() => {
191
+ element.srcObject = mediaStream;
192
+ }, 1);
193
+ }
194
+ }
195
+
196
+ /** @internal */
197
+ export function detachTrack(
198
+ track: MediaStreamTrack,
199
+ element: HTMLMediaElement,
200
+ ) {
201
+ if (element.srcObject instanceof MediaStream) {
202
+ const mediaStream = element.srcObject;
203
+ mediaStream.removeTrack(track);
204
+ element.srcObject = null;
205
+ }
206
+ }
207
+
208
+ export namespace Track {
209
+ export enum Kind {
210
+ Audio = 'audio',
211
+ Video = 'video',
212
+ Unknown = 'unknown',
213
+ }
214
+ export type SID = string;
215
+ export enum Source {
216
+ Camera = 'camera',
217
+ Microphone = 'microphone',
218
+ ScreenShare = 'screen_share',
219
+ ScreenShareAudio = 'screen_share_audio',
220
+ Unknown = 'unknown',
221
+ }
222
+
223
+ export enum StreamState {
224
+ Active = 'active',
225
+ Paused = 'paused',
226
+ Unknown = 'unknown',
227
+ }
228
+
229
+ export interface Dimensions {
230
+ width: number;
231
+ height: number;
232
+ }
233
+
234
+ /** @internal */
235
+ export function kindToProto(k: Kind): TrackType {
236
+ switch (k) {
237
+ case Kind.Audio:
238
+ return TrackType.AUDIO;
239
+ case Kind.Video:
240
+ return TrackType.VIDEO;
241
+ default:
242
+ return TrackType.UNRECOGNIZED;
243
+ }
244
+ }
245
+
246
+ /** @internal */
247
+ export function kindFromProto(t: TrackType): Kind | undefined {
248
+ switch (t) {
249
+ case TrackType.AUDIO:
250
+ return Kind.Audio;
251
+ case TrackType.VIDEO:
252
+ return Kind.Video;
253
+ default:
254
+ return Kind.Unknown;
255
+ }
256
+ }
257
+
258
+ /** @internal */
259
+ export function sourceToProto(s: Source): TrackSource {
260
+ switch (s) {
261
+ case Source.Camera:
262
+ return TrackSource.CAMERA;
263
+ case Source.Microphone:
264
+ return TrackSource.MICROPHONE;
265
+ case Source.ScreenShare:
266
+ return TrackSource.SCREEN_SHARE;
267
+ case Source.ScreenShareAudio:
268
+ return TrackSource.SCREEN_SHARE_AUDIO;
269
+ default:
270
+ return TrackSource.UNRECOGNIZED;
271
+ }
272
+ }
273
+
274
+ /** @internal */
275
+ export function sourceFromProto(s: TrackSource): Source {
276
+ switch (s) {
277
+ case TrackSource.CAMERA:
278
+ return Source.Camera;
279
+ case TrackSource.MICROPHONE:
280
+ return Source.Microphone;
281
+ case TrackSource.SCREEN_SHARE:
282
+ return Source.ScreenShare;
283
+ case TrackSource.SCREEN_SHARE_AUDIO:
284
+ return Source.ScreenShareAudio;
285
+ default:
286
+ return Source.Unknown;
287
+ }
288
+ }
289
+
290
+ /** @internal */
291
+ export function streamStateFromProto(s: ProtoStreamState): StreamState {
292
+ switch (s) {
293
+ case ProtoStreamState.ACTIVE:
294
+ return StreamState.Active;
295
+ case ProtoStreamState.PAUSED:
296
+ return StreamState.Paused;
297
+ default:
298
+ return StreamState.Unknown;
299
+ }
300
+ }
301
+ }