@stream-io/video-client 0.0.1-alpha.7
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/LICENSE +219 -0
- package/README.md +14 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +14663 -0
- package/dist/index.js.map +1 -0
- package/dist/src/Batcher.d.ts +12 -0
- package/dist/src/CallDropScheduler.d.ts +44 -0
- package/dist/src/StreamSfuClient.d.ts +25 -0
- package/dist/src/StreamVideoClient.d.ts +145 -0
- package/dist/src/__tests__/StreamVideoClient.test.d.ts +1 -0
- package/dist/src/config/defaultConfigs.d.ts +2 -0
- package/dist/src/config/types.d.ts +29 -0
- package/dist/src/coordinator/StreamCoordinatorClient.d.ts +19 -0
- package/dist/src/coordinator/connection/base64.d.ts +2 -0
- package/dist/src/coordinator/connection/client.d.ts +174 -0
- package/dist/src/coordinator/connection/connection.d.ts +139 -0
- package/dist/src/coordinator/connection/connection_fallback.d.ts +38 -0
- package/dist/src/coordinator/connection/errors.d.ts +16 -0
- package/dist/src/coordinator/connection/events.d.ts +7 -0
- package/dist/src/coordinator/connection/insights.d.ts +58 -0
- package/dist/src/coordinator/connection/signing.d.ts +30 -0
- package/dist/src/coordinator/connection/token_manager.d.ts +39 -0
- package/dist/src/coordinator/connection/types.d.ts +96 -0
- package/dist/src/coordinator/connection/utils.d.ts +25 -0
- package/dist/src/devices.d.ts +79 -0
- package/dist/src/events/call.d.ts +26 -0
- package/dist/src/events/internal.d.ts +8 -0
- package/dist/src/events/participant.d.ts +21 -0
- package/dist/src/events/speaker.d.ts +10 -0
- package/dist/src/gen/coordinator/index.d.ts +1664 -0
- package/dist/src/gen/google/protobuf/descriptor.d.ts +1650 -0
- package/dist/src/gen/google/protobuf/duration.d.ts +113 -0
- package/dist/src/gen/google/protobuf/struct.d.ts +184 -0
- package/dist/src/gen/google/protobuf/timestamp.d.ts +158 -0
- package/dist/src/gen/video/coordinator/broadcast_v1/broadcast.d.ts +66 -0
- package/dist/src/gen/video/coordinator/call_v1/call.d.ts +254 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.d.ts +351 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.d.ts +1488 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/envelopes.d.ts +143 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/websocket.d.ts +292 -0
- package/dist/src/gen/video/coordinator/edge_v1/edge.d.ts +183 -0
- package/dist/src/gen/video/coordinator/event_v1/event.d.ts +411 -0
- package/dist/src/gen/video/coordinator/geofence_v1/geofence.d.ts +63 -0
- package/dist/src/gen/video/coordinator/member_v1/member.d.ts +59 -0
- package/dist/src/gen/video/coordinator/participant_v1/participant.d.ts +103 -0
- package/dist/src/gen/video/coordinator/push_v1/push.d.ts +240 -0
- package/dist/src/gen/video/coordinator/stat_v1/stat.d.ts +308 -0
- package/dist/src/gen/video/coordinator/user_v1/user.d.ts +112 -0
- package/dist/src/gen/video/coordinator/utils_v1/utils.d.ts +47 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +736 -0
- package/dist/src/gen/video/sfu/models/models.d.ts +460 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +89 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +320 -0
- package/dist/src/helpers/browsers.d.ts +8 -0
- package/dist/src/helpers/sound-detector.d.ts +34 -0
- package/dist/src/rpc/createClient.d.ts +10 -0
- package/dist/src/rpc/index.d.ts +2 -0
- package/dist/src/rpc/latency.d.ts +9 -0
- package/dist/src/rtc/Call.d.ts +180 -0
- package/dist/src/rtc/CallMetadata.d.ts +9 -0
- package/dist/src/rtc/Dispatcher.d.ts +9 -0
- package/dist/src/rtc/IceTrickleBuffer.d.ts +11 -0
- package/dist/src/rtc/callEventHandlers.d.ts +5 -0
- package/dist/src/rtc/codecs.d.ts +2 -0
- package/dist/src/rtc/helpers/iceCandidate.d.ts +2 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -0
- package/dist/src/rtc/publisher.d.ts +53 -0
- package/dist/src/rtc/signal.d.ts +5 -0
- package/dist/src/rtc/subscriber.d.ts +7 -0
- package/dist/src/rtc/types.d.ts +84 -0
- package/dist/src/rtc/videoLayers.d.ts +17 -0
- package/dist/src/stats/coordinator-stats-reporter.d.ts +10 -0
- package/dist/src/stats/state-store-stats-reporter.d.ts +57 -0
- package/dist/src/stats/types.d.ts +42 -0
- package/dist/src/store/index.d.ts +2 -0
- package/dist/src/store/rxUtils.d.ts +18 -0
- package/dist/src/store/stateStore.d.ts +182 -0
- package/generate-openapi.sh +32 -0
- package/index.ts +30 -0
- package/openapitools.json +7 -0
- package/package.json +54 -0
- package/rollup.config.mjs +48 -0
- package/src/Batcher.ts +43 -0
- package/src/CallDropScheduler.ts +192 -0
- package/src/StreamSfuClient.ts +185 -0
- package/src/StreamVideoClient.ts +487 -0
- package/src/__tests__/StreamVideoClient.test.ts +83 -0
- package/src/config/defaultConfigs.ts +15 -0
- package/src/config/types.ts +30 -0
- package/src/coordinator/StreamCoordinatorClient.ts +111 -0
- package/src/coordinator/connection/base64.ts +80 -0
- package/src/coordinator/connection/client.ts +815 -0
- package/src/coordinator/connection/connection.ts +750 -0
- package/src/coordinator/connection/connection_fallback.ts +239 -0
- package/src/coordinator/connection/errors.ts +70 -0
- package/src/coordinator/connection/events.ts +10 -0
- package/src/coordinator/connection/insights.ts +88 -0
- package/src/coordinator/connection/signing.ts +104 -0
- package/src/coordinator/connection/token_manager.ts +160 -0
- package/src/coordinator/connection/types.ts +120 -0
- package/src/coordinator/connection/utils.ts +148 -0
- package/src/devices.ts +266 -0
- package/src/events/call.ts +166 -0
- package/src/events/internal.ts +47 -0
- package/src/events/participant.ts +97 -0
- package/src/events/speaker.ts +62 -0
- package/src/gen/coordinator/index.ts +1653 -0
- package/src/gen/google/protobuf/descriptor.ts +3466 -0
- package/src/gen/google/protobuf/duration.ts +232 -0
- package/src/gen/google/protobuf/struct.ts +481 -0
- package/src/gen/google/protobuf/timestamp.ts +291 -0
- package/src/gen/video/coordinator/broadcast_v1/broadcast.ts +154 -0
- package/src/gen/video/coordinator/call_v1/call.ts +651 -0
- package/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.ts +463 -0
- package/src/gen/video/coordinator/client_v1_rpc/client_rpc.ts +3819 -0
- package/src/gen/video/coordinator/client_v1_rpc/envelopes.ts +424 -0
- package/src/gen/video/coordinator/client_v1_rpc/websocket.ts +719 -0
- package/src/gen/video/coordinator/edge_v1/edge.ts +532 -0
- package/src/gen/video/coordinator/event_v1/event.ts +1171 -0
- package/src/gen/video/coordinator/geofence_v1/geofence.ts +128 -0
- package/src/gen/video/coordinator/member_v1/member.ts +138 -0
- package/src/gen/video/coordinator/participant_v1/participant.ts +261 -0
- package/src/gen/video/coordinator/push_v1/push.ts +651 -0
- package/src/gen/video/coordinator/stat_v1/stat.ts +656 -0
- package/src/gen/video/coordinator/user_v1/user.ts +277 -0
- package/src/gen/video/coordinator/utils_v1/utils.ts +98 -0
- package/src/gen/video/sfu/event/events.ts +1962 -0
- package/src/gen/video/sfu/models/models.ts +1062 -0
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +108 -0
- package/src/gen/video/sfu/signal_rpc/signal.ts +906 -0
- package/src/helpers/browsers.ts +13 -0
- package/src/helpers/sound-detector.ts +85 -0
- package/src/rpc/createClient.ts +50 -0
- package/src/rpc/index.ts +2 -0
- package/src/rpc/latency.ts +43 -0
- package/src/rtc/Call.ts +585 -0
- package/src/rtc/CallMetadata.ts +24 -0
- package/src/rtc/Dispatcher.ts +46 -0
- package/src/rtc/IceTrickleBuffer.ts +21 -0
- package/src/rtc/callEventHandlers.ts +37 -0
- package/src/rtc/codecs.ts +61 -0
- package/src/rtc/helpers/iceCandidate.ts +16 -0
- package/src/rtc/helpers/tracks.ts +18 -0
- package/src/rtc/publisher.ts +305 -0
- package/src/rtc/signal.ts +34 -0
- package/src/rtc/subscriber.ts +85 -0
- package/src/rtc/types.ts +105 -0
- package/src/rtc/videoLayers.ts +103 -0
- package/src/stats/coordinator-stats-reporter.ts +167 -0
- package/src/stats/state-store-stats-reporter.ts +364 -0
- package/src/stats/types.ts +46 -0
- package/src/store/index.ts +2 -0
- package/src/store/rxUtils.ts +42 -0
- package/src/store/stateStore.ts +341 -0
- package/tsconfig.json +25 -0
- package/typedoc.json +11 -0
- package/vite.config.ts +11 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Call } from './Call';
|
|
2
|
+
import { Dispatcher } from './Dispatcher';
|
|
3
|
+
import { StreamVideoWriteableStateStore } from '../store';
|
|
4
|
+
import {
|
|
5
|
+
watchParticipantJoined,
|
|
6
|
+
watchParticipantLeft,
|
|
7
|
+
watchTrackPublished,
|
|
8
|
+
watchTrackUnpublished,
|
|
9
|
+
} from '../events/participant';
|
|
10
|
+
import {
|
|
11
|
+
watchChangePublishQuality,
|
|
12
|
+
watchConnectionQualityChanged,
|
|
13
|
+
} from '../events/internal';
|
|
14
|
+
import {
|
|
15
|
+
watchAudioLevelChanged,
|
|
16
|
+
watchDominantSpeakerChanged,
|
|
17
|
+
} from '../events/speaker';
|
|
18
|
+
import { Batcher } from '../Batcher';
|
|
19
|
+
|
|
20
|
+
export const registerEventHandlers = (
|
|
21
|
+
call: Call,
|
|
22
|
+
store: StreamVideoWriteableStateStore,
|
|
23
|
+
dispatcher: Dispatcher,
|
|
24
|
+
userBatcher: Batcher<string>,
|
|
25
|
+
) => {
|
|
26
|
+
watchChangePublishQuality(dispatcher, call);
|
|
27
|
+
watchConnectionQualityChanged(dispatcher, store);
|
|
28
|
+
|
|
29
|
+
watchParticipantJoined(dispatcher, store, userBatcher);
|
|
30
|
+
watchParticipantLeft(dispatcher, store);
|
|
31
|
+
|
|
32
|
+
watchTrackPublished(dispatcher, store);
|
|
33
|
+
watchTrackUnpublished(dispatcher, store);
|
|
34
|
+
|
|
35
|
+
watchAudioLevelChanged(dispatcher, store);
|
|
36
|
+
watchDominantSpeakerChanged(dispatcher, store);
|
|
37
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const getPreferredCodecs = (
|
|
2
|
+
kind: 'audio' | 'video',
|
|
3
|
+
videoCodec: string,
|
|
4
|
+
) => {
|
|
5
|
+
if (!('getCapabilities' in RTCRtpSender)) {
|
|
6
|
+
console.warn('RTCRtpSender.getCapabilities is not supported');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const cap = RTCRtpSender.getCapabilities(kind);
|
|
10
|
+
console.log('s4e');
|
|
11
|
+
if (!cap) return;
|
|
12
|
+
const matched: RTCRtpCodecCapability[] = [];
|
|
13
|
+
const partialMatched: RTCRtpCodecCapability[] = [];
|
|
14
|
+
const unmatched: RTCRtpCodecCapability[] = [];
|
|
15
|
+
cap.codecs.forEach((c) => {
|
|
16
|
+
const codec = c.mimeType.toLowerCase();
|
|
17
|
+
if (codec === 'audio/opus') {
|
|
18
|
+
matched.push(c);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(c);
|
|
22
|
+
const matchesVideoCodec = codec === `video/${videoCodec}`;
|
|
23
|
+
if (!matchesVideoCodec) {
|
|
24
|
+
unmatched.push(c);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// for h264 codecs that have sdpFmtpLine available, use only if the
|
|
28
|
+
// profile-level-id is 42e01f for cross-browser compatibility
|
|
29
|
+
if (videoCodec === 'h264') {
|
|
30
|
+
if (c.sdpFmtpLine && c.sdpFmtpLine.includes('profile-level-id=42e01f')) {
|
|
31
|
+
matched.push(c);
|
|
32
|
+
} else {
|
|
33
|
+
partialMatched.push(c);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log('matched', matched);
|
|
38
|
+
matched.push(c);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return [
|
|
42
|
+
...matched,
|
|
43
|
+
...partialMatched,
|
|
44
|
+
...unmatched,
|
|
45
|
+
] as RTCRtpCodecCapability[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const getGenericSdp = async (direction: RTCRtpTransceiverDirection) => {
|
|
49
|
+
const tempPc = new RTCPeerConnection();
|
|
50
|
+
tempPc.addTransceiver('audio', { direction });
|
|
51
|
+
tempPc.addTransceiver('video', { direction });
|
|
52
|
+
|
|
53
|
+
const offer = await tempPc.createOffer();
|
|
54
|
+
const sdp = offer.sdp;
|
|
55
|
+
|
|
56
|
+
tempPc.getTransceivers().forEach((t) => {
|
|
57
|
+
t.stop();
|
|
58
|
+
});
|
|
59
|
+
tempPc.close();
|
|
60
|
+
return sdp;
|
|
61
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ICETrickle } from '../../gen/video/sfu/models/models';
|
|
2
|
+
|
|
3
|
+
export function getIceCandidate(
|
|
4
|
+
candidate: RTCIceCandidate,
|
|
5
|
+
): ICETrickle['iceCandidate'] {
|
|
6
|
+
if (!candidate.usernameFragment) {
|
|
7
|
+
// react-native-webrtc doesn't include usernameFragment in the candidate
|
|
8
|
+
const splittedCandidate = candidate.candidate.split(' ');
|
|
9
|
+
const ufragIndex =
|
|
10
|
+
splittedCandidate.findIndex((s: string) => s === 'ufrag') + 1;
|
|
11
|
+
const usernameFragment = splittedCandidate[ufragIndex];
|
|
12
|
+
return JSON.stringify({ ...candidate, usernameFragment });
|
|
13
|
+
} else {
|
|
14
|
+
return JSON.stringify(candidate.toJSON());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
2
|
+
import type { StreamVideoParticipant } from '../types';
|
|
3
|
+
|
|
4
|
+
export const trackTypeToParticipantStreamKey = (
|
|
5
|
+
trackType: TrackType,
|
|
6
|
+
): keyof StreamVideoParticipant | undefined => {
|
|
7
|
+
switch (trackType) {
|
|
8
|
+
case TrackType.SCREEN_SHARE:
|
|
9
|
+
return 'screenShareStream';
|
|
10
|
+
case TrackType.VIDEO:
|
|
11
|
+
return 'videoStream';
|
|
12
|
+
case TrackType.AUDIO:
|
|
13
|
+
return 'audioStream';
|
|
14
|
+
default:
|
|
15
|
+
console.error(`Unknown track type: ${trackType}`);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { StreamSfuClient } from '../StreamSfuClient';
|
|
2
|
+
import {
|
|
3
|
+
PeerType,
|
|
4
|
+
TrackInfo,
|
|
5
|
+
TrackType,
|
|
6
|
+
VideoLayer,
|
|
7
|
+
VideoQuality,
|
|
8
|
+
} from '../gen/video/sfu/models/models';
|
|
9
|
+
import { getIceCandidate } from './helpers/iceCandidate';
|
|
10
|
+
import {
|
|
11
|
+
findOptimalVideoLayers,
|
|
12
|
+
findOptimalScreenSharingLayers,
|
|
13
|
+
} from './videoLayers';
|
|
14
|
+
import { getPreferredCodecs } from './codecs';
|
|
15
|
+
import { PublishOptions } from './types';
|
|
16
|
+
|
|
17
|
+
export type PublisherOpts = {
|
|
18
|
+
rpcClient: StreamSfuClient;
|
|
19
|
+
connectionConfig?: RTCConfiguration;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @internal
|
|
24
|
+
* The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
|
|
25
|
+
*/
|
|
26
|
+
export class Publisher {
|
|
27
|
+
private readonly publisher: RTCPeerConnection;
|
|
28
|
+
private readonly rpcClient: StreamSfuClient;
|
|
29
|
+
private readonly transceiverRegistry: {
|
|
30
|
+
[key in TrackType]: RTCRtpTransceiver | undefined;
|
|
31
|
+
} = {
|
|
32
|
+
[TrackType.AUDIO]: undefined,
|
|
33
|
+
[TrackType.VIDEO]: undefined,
|
|
34
|
+
[TrackType.SCREEN_SHARE]: undefined,
|
|
35
|
+
[TrackType.SCREEN_SHARE_AUDIO]: undefined,
|
|
36
|
+
[TrackType.UNSPECIFIED]: undefined,
|
|
37
|
+
};
|
|
38
|
+
private readonly trackKindRegistry: {
|
|
39
|
+
[key in TrackType]: 'video' | 'audio' | undefined;
|
|
40
|
+
} = {
|
|
41
|
+
[TrackType.AUDIO]: 'audio',
|
|
42
|
+
[TrackType.VIDEO]: 'video',
|
|
43
|
+
[TrackType.SCREEN_SHARE]: 'video',
|
|
44
|
+
[TrackType.SCREEN_SHARE_AUDIO]: undefined,
|
|
45
|
+
[TrackType.UNSPECIFIED]: undefined,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
constructor({ connectionConfig, rpcClient }: PublisherOpts) {
|
|
49
|
+
const pc = new RTCPeerConnection(connectionConfig);
|
|
50
|
+
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
51
|
+
pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
52
|
+
|
|
53
|
+
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
54
|
+
pc.addEventListener(
|
|
55
|
+
'iceconnectionstatechange',
|
|
56
|
+
this.onIceConnectionStateChange,
|
|
57
|
+
);
|
|
58
|
+
pc.addEventListener(
|
|
59
|
+
'icegatheringstatechange',
|
|
60
|
+
this.onIceGatheringStateChange,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
this.publisher = pc;
|
|
64
|
+
this.rpcClient = rpcClient;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Starts publishing the given track of the given media stream.
|
|
69
|
+
*
|
|
70
|
+
* Consecutive calls to this method will replace the stream.
|
|
71
|
+
* The previous stream will be stopped.
|
|
72
|
+
* @param mediaStream
|
|
73
|
+
* @param track
|
|
74
|
+
* @param trackType
|
|
75
|
+
* @param opts
|
|
76
|
+
*/
|
|
77
|
+
publishStream = async (
|
|
78
|
+
mediaStream: MediaStream,
|
|
79
|
+
track: MediaStreamTrack,
|
|
80
|
+
trackType: TrackType,
|
|
81
|
+
opts: PublishOptions = {},
|
|
82
|
+
) => {
|
|
83
|
+
let transceiver = this.publisher
|
|
84
|
+
.getTransceivers()
|
|
85
|
+
.find(
|
|
86
|
+
(t) =>
|
|
87
|
+
t === this.transceiverRegistry[trackType] &&
|
|
88
|
+
t.sender.track &&
|
|
89
|
+
t.sender.track?.kind === this.trackKindRegistry[trackType],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (!transceiver) {
|
|
93
|
+
let videoEncodings: RTCRtpEncodingParameters[] | undefined;
|
|
94
|
+
if (trackType === TrackType.VIDEO) {
|
|
95
|
+
videoEncodings = findOptimalVideoLayers(track);
|
|
96
|
+
}
|
|
97
|
+
transceiver = this.publisher.addTransceiver(track, {
|
|
98
|
+
direction: 'sendonly',
|
|
99
|
+
streams:
|
|
100
|
+
trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
|
|
101
|
+
? [mediaStream]
|
|
102
|
+
: undefined,
|
|
103
|
+
sendEncodings:
|
|
104
|
+
trackType === TrackType.VIDEO ? videoEncodings : undefined,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this.transceiverRegistry[trackType] = transceiver;
|
|
108
|
+
|
|
109
|
+
if (trackType === TrackType.VIDEO) {
|
|
110
|
+
const codecPreferences = getPreferredCodecs(
|
|
111
|
+
'video',
|
|
112
|
+
opts.preferredCodec || 'vp8',
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if ('setCodecPreferences' in transceiver && codecPreferences) {
|
|
116
|
+
console.log(`set codec preferences`, codecPreferences);
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
transceiver.setCodecPreferences(codecPreferences);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
transceiver.sender.track?.stop();
|
|
123
|
+
await transceiver.sender.replaceTrack(track);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Stops publishing the given track type to the SFU, if it is currently being published.
|
|
129
|
+
* Underlying track will be stopped and removed from the publisher.
|
|
130
|
+
* @param trackType
|
|
131
|
+
* @returns `true` if track with the given track type was found, otherwise `false`
|
|
132
|
+
*/
|
|
133
|
+
unpublishStream = (trackType: TrackType) => {
|
|
134
|
+
const transceiver = this.publisher
|
|
135
|
+
.getTransceivers()
|
|
136
|
+
.find((t) => t === this.transceiverRegistry[trackType] && t.sender.track);
|
|
137
|
+
if (transceiver && transceiver.sender.track) {
|
|
138
|
+
transceiver.sender.track.stop();
|
|
139
|
+
return true;
|
|
140
|
+
} else {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Stops publishing all tracks and stop all tracks.
|
|
147
|
+
*/
|
|
148
|
+
stopPublishing = () => {
|
|
149
|
+
this.publisher.getSenders().forEach((s) => {
|
|
150
|
+
if (s.track) {
|
|
151
|
+
s.track.stop();
|
|
152
|
+
}
|
|
153
|
+
if (this.publisher.signalingState !== 'closed') {
|
|
154
|
+
this.publisher.removeTrack(s);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
this.publisher.close();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
updateVideoPublishQuality = async (enabledRids: string[]) => {
|
|
161
|
+
console.log(
|
|
162
|
+
'Updating publish quality, qualities requested by SFU:',
|
|
163
|
+
enabledRids,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
|
|
167
|
+
|
|
168
|
+
if (!videoSender) return;
|
|
169
|
+
|
|
170
|
+
const params = videoSender.getParameters();
|
|
171
|
+
let changed = false;
|
|
172
|
+
params.encodings.forEach((enc) => {
|
|
173
|
+
console.log(enc.rid, enc.active);
|
|
174
|
+
// flip 'active' flag only when necessary
|
|
175
|
+
const shouldEnable = enabledRids.includes(enc.rid!);
|
|
176
|
+
if (shouldEnable !== enc.active) {
|
|
177
|
+
enc.active = shouldEnable;
|
|
178
|
+
changed = true;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
if (changed) {
|
|
182
|
+
if (params.encodings.length === 0) {
|
|
183
|
+
console.warn('No suitable video encoding quality found');
|
|
184
|
+
}
|
|
185
|
+
await videoSender.setParameters(params);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Returns the result of the `RTCPeerConnection.getStats()` method
|
|
191
|
+
* @param selector
|
|
192
|
+
* @returns
|
|
193
|
+
*/
|
|
194
|
+
getStats(selector?: MediaStreamTrack | null | undefined) {
|
|
195
|
+
return this.publisher.getStats(selector);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private onIceCandidate = async (e: RTCPeerConnectionIceEvent) => {
|
|
199
|
+
const { candidate } = e;
|
|
200
|
+
if (!candidate) {
|
|
201
|
+
console.log('null ice candidate');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
await this.rpcClient.iceTrickle({
|
|
205
|
+
iceCandidate: getIceCandidate(candidate),
|
|
206
|
+
peerType: PeerType.PUBLISHER_UNSPECIFIED,
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
private onNegotiationNeeded = async () => {
|
|
211
|
+
console.log('AAA onNegotiationNeeded');
|
|
212
|
+
const offer = await this.publisher.createOffer();
|
|
213
|
+
await this.publisher.setLocalDescription(offer);
|
|
214
|
+
|
|
215
|
+
const trackInfos = this.publisher
|
|
216
|
+
.getTransceivers()
|
|
217
|
+
.filter((t) => t.direction === 'sendonly' && !!t.sender.track)
|
|
218
|
+
.map<TrackInfo>((transceiver) => {
|
|
219
|
+
const trackType = Number(
|
|
220
|
+
Object.keys(this.transceiverRegistry).find(
|
|
221
|
+
(key) =>
|
|
222
|
+
this.transceiverRegistry[key as any as TrackType] === transceiver,
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
const track = transceiver.sender.track!;
|
|
226
|
+
const optimalLayers =
|
|
227
|
+
trackType === TrackType.VIDEO
|
|
228
|
+
? findOptimalVideoLayers(track)
|
|
229
|
+
: trackType === TrackType.SCREEN_SHARE
|
|
230
|
+
? findOptimalScreenSharingLayers(track)
|
|
231
|
+
: [];
|
|
232
|
+
|
|
233
|
+
const layers = optimalLayers.map<VideoLayer>((optimalLayer) => ({
|
|
234
|
+
rid: optimalLayer.rid || '',
|
|
235
|
+
bitrate: optimalLayer.maxBitrate || 0,
|
|
236
|
+
fps: optimalLayer.maxFramerate || 0,
|
|
237
|
+
quality: this.ridToVideoQuality(optimalLayer.rid || ''),
|
|
238
|
+
videoDimension: {
|
|
239
|
+
width: optimalLayer.width,
|
|
240
|
+
height: optimalLayer.height,
|
|
241
|
+
},
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
trackId: track.id,
|
|
246
|
+
layers: layers,
|
|
247
|
+
trackType,
|
|
248
|
+
mid: transceiver.mid || '',
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// TODO debounce for 250ms
|
|
253
|
+
const response = await this.rpcClient.setPublisher({
|
|
254
|
+
sdp: offer.sdp || '',
|
|
255
|
+
tracks: trackInfos,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await this.publisher.setRemoteDescription({
|
|
259
|
+
type: 'answer',
|
|
260
|
+
sdp: response.response.sdp,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
this.rpcClient.iceTrickleBuffer.publisherCandidates.subscribe(
|
|
264
|
+
async (candidate) => {
|
|
265
|
+
try {
|
|
266
|
+
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
267
|
+
await this.publisher.addIceCandidate(iceCandidate);
|
|
268
|
+
} catch (e) {
|
|
269
|
+
console.error(`[Publisher] ICE candidate error`, e, candidate);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
private onIceCandidateError = (e: Event) => {
|
|
276
|
+
const errorMessage =
|
|
277
|
+
e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
278
|
+
`${e.errorCode}: ${e.errorText}`;
|
|
279
|
+
console.error(`Publisher: ICE Candidate error`, errorMessage, e);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
private onIceConnectionStateChange = (e: Event) => {
|
|
283
|
+
console.log(
|
|
284
|
+
`Publisher: ICE Connection state changed`,
|
|
285
|
+
this.publisher.iceConnectionState,
|
|
286
|
+
e,
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
private onIceGatheringStateChange = (e: Event) => {
|
|
291
|
+
console.log(
|
|
292
|
+
`Publisher: ICE Gathering State`,
|
|
293
|
+
this.publisher.iceGatheringState,
|
|
294
|
+
e,
|
|
295
|
+
);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
private ridToVideoQuality = (rid: string): VideoQuality => {
|
|
299
|
+
return rid === 'q'
|
|
300
|
+
? VideoQuality.LOW_UNSPECIFIED
|
|
301
|
+
: rid === 'h'
|
|
302
|
+
? VideoQuality.MID
|
|
303
|
+
: VideoQuality.HIGH; // default to HIGH
|
|
304
|
+
};
|
|
305
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { SfuEvent } from '../gen/video/sfu/event/events';
|
|
2
|
+
|
|
3
|
+
export const createWebSocketSignalChannel = (opts: {
|
|
4
|
+
endpoint: string;
|
|
5
|
+
onMessage?: (message: SfuEvent) => void;
|
|
6
|
+
}) => {
|
|
7
|
+
return new Promise<WebSocket>((resolve) => {
|
|
8
|
+
const { endpoint, onMessage } = opts;
|
|
9
|
+
const ws = new WebSocket(endpoint);
|
|
10
|
+
ws.binaryType = 'arraybuffer'; // do we need this?
|
|
11
|
+
ws.addEventListener('open', () => {
|
|
12
|
+
return resolve(ws);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
ws.addEventListener('error', (e) => {
|
|
16
|
+
console.error('Error', e);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
ws.addEventListener('close', (e) => {
|
|
20
|
+
console.warn('Signalling channel is closed', e);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (onMessage) {
|
|
24
|
+
ws.addEventListener('message', (e) => {
|
|
25
|
+
const message =
|
|
26
|
+
e.data instanceof ArrayBuffer
|
|
27
|
+
? SfuEvent.fromBinary(new Uint8Array(e.data))
|
|
28
|
+
: SfuEvent.fromJsonString(e.data);
|
|
29
|
+
|
|
30
|
+
onMessage(message);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { StreamSfuClient } from '../StreamSfuClient';
|
|
2
|
+
import { getIceCandidate } from './helpers/iceCandidate';
|
|
3
|
+
import { PeerType } from '../gen/video/sfu/models/models';
|
|
4
|
+
|
|
5
|
+
export type SubscriberOpts = {
|
|
6
|
+
rpcClient: StreamSfuClient;
|
|
7
|
+
connectionConfig?: RTCConfiguration;
|
|
8
|
+
onTrack?: (e: RTCTrackEvent) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const createSubscriber = ({
|
|
12
|
+
rpcClient,
|
|
13
|
+
connectionConfig,
|
|
14
|
+
onTrack,
|
|
15
|
+
}: SubscriberOpts) => {
|
|
16
|
+
const subscriber = new RTCPeerConnection(connectionConfig);
|
|
17
|
+
subscriber.addEventListener('icecandidate', async (e) => {
|
|
18
|
+
const { candidate } = e;
|
|
19
|
+
if (!candidate) {
|
|
20
|
+
console.log('null ice candidate');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await rpcClient.iceTrickle({
|
|
25
|
+
iceCandidate: getIceCandidate(candidate),
|
|
26
|
+
peerType: PeerType.SUBSCRIBER,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
subscriber.addEventListener('icecandidateerror', (e) => {
|
|
30
|
+
const errorMessage =
|
|
31
|
+
e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
32
|
+
`${e.errorCode}: ${e.errorText}`;
|
|
33
|
+
console.error(`Subscriber: ICE Candidate error`, errorMessage, e);
|
|
34
|
+
});
|
|
35
|
+
subscriber.addEventListener('iceconnectionstatechange', (e) => {
|
|
36
|
+
console.log(
|
|
37
|
+
`Subscriber: ICE Connection state changed`,
|
|
38
|
+
subscriber.iceConnectionState,
|
|
39
|
+
e,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
subscriber.addEventListener('icegatheringstatechange', (e) => {
|
|
43
|
+
console.log(
|
|
44
|
+
`Subscriber: ICE Gathering State`,
|
|
45
|
+
subscriber.iceGatheringState,
|
|
46
|
+
e,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (onTrack) {
|
|
51
|
+
subscriber.addEventListener('track', onTrack);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { dispatcher, iceTrickleBuffer } = rpcClient;
|
|
55
|
+
dispatcher.on('subscriberOffer', async (message) => {
|
|
56
|
+
if (message.eventPayload.oneofKind !== 'subscriberOffer') return;
|
|
57
|
+
const { subscriberOffer } = message.eventPayload;
|
|
58
|
+
console.log(`Received subscriberOffer`, subscriberOffer);
|
|
59
|
+
|
|
60
|
+
await subscriber.setRemoteDescription({
|
|
61
|
+
type: 'offer',
|
|
62
|
+
sdp: subscriberOffer.sdp,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
iceTrickleBuffer.subscriberCandidates.subscribe(async (candidate) => {
|
|
66
|
+
try {
|
|
67
|
+
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
68
|
+
await subscriber.addIceCandidate(iceCandidate);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error(`[Subscriber] ICE candidate error`, e, candidate);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// apply ice candidates
|
|
75
|
+
const answer = await subscriber.createAnswer();
|
|
76
|
+
await subscriber.setLocalDescription(answer);
|
|
77
|
+
|
|
78
|
+
await rpcClient.sendAnswer({
|
|
79
|
+
peerType: PeerType.SUBSCRIBER,
|
|
80
|
+
sdp: answer.sdp || '',
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return subscriber;
|
|
85
|
+
};
|
package/src/rtc/types.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { UserResponse } from '../gen/coordinator';
|
|
2
|
+
import type {
|
|
3
|
+
Participant,
|
|
4
|
+
VideoDimension,
|
|
5
|
+
} from '../gen/video/sfu/models/models';
|
|
6
|
+
|
|
7
|
+
export interface StreamVideoParticipant extends Participant {
|
|
8
|
+
/**
|
|
9
|
+
* The participant's audio stream, if they are publishing audio and
|
|
10
|
+
* we have subscribed to it.
|
|
11
|
+
*/
|
|
12
|
+
audioStream?: MediaStream;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The participant's video stream, if they are sharing their video,
|
|
16
|
+
* and we are subscribed to it.
|
|
17
|
+
*/
|
|
18
|
+
videoStream?: MediaStream;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The participant's screen share stream, if they are sharing their screen,
|
|
22
|
+
* and we are subscribed to it.
|
|
23
|
+
*/
|
|
24
|
+
screenShareStream?: MediaStream;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The preferred video dimensions for this participant.
|
|
28
|
+
* Set it to `undefined` to unsubscribe from this participant's video.
|
|
29
|
+
*/
|
|
30
|
+
videoDimension?: VideoDimension;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The preferred screen share dimensions for this participant.
|
|
34
|
+
* Set it to `undefined` to unsubscribe from this participant's screen share.
|
|
35
|
+
*/
|
|
36
|
+
screenShareDimension?: VideoDimension;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* True if the participant is the local participant.
|
|
40
|
+
*/
|
|
41
|
+
isLoggedInUser?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* True when the participant is pinned
|
|
45
|
+
*/
|
|
46
|
+
isPinned?: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* User metadata (profile picture, name...)
|
|
50
|
+
*/
|
|
51
|
+
user?: UserResponse;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface StreamVideoLocalParticipant extends StreamVideoParticipant {
|
|
55
|
+
/**
|
|
56
|
+
* The device ID of the currently selected audio input device of the local participant (returned by the [MediaDevices API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia))
|
|
57
|
+
*/
|
|
58
|
+
audioDeviceId?: string;
|
|
59
|
+
/**
|
|
60
|
+
* The device ID of the currently selected video input device of the local participant (returned by the [MediaDevices API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia))
|
|
61
|
+
*/
|
|
62
|
+
videoDeviceId?: string;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The device ID of the currently selected audio output device of the local participant (returned by the [MediaDevices API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia))
|
|
66
|
+
*
|
|
67
|
+
* If the value is not defined, the user hasn't selected any device (in these cases the default system audio output could be used)
|
|
68
|
+
*/
|
|
69
|
+
audioOutputDeviceId?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A partial representation of the StreamVideoParticipant.
|
|
74
|
+
*/
|
|
75
|
+
export type StreamVideoParticipantPatch = Partial<
|
|
76
|
+
StreamVideoParticipant | StreamVideoLocalParticipant
|
|
77
|
+
>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A collection of {@link StreamVideoParticipantPatch} organized by sessionId.
|
|
81
|
+
*/
|
|
82
|
+
export type StreamVideoParticipantPatches = {
|
|
83
|
+
[sessionId: string]: StreamVideoParticipantPatch;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type SubscriptionChange = {
|
|
87
|
+
/**
|
|
88
|
+
* The video dimension to request.
|
|
89
|
+
* Set it to `undefined` in case you want to unsubscribe.
|
|
90
|
+
*/
|
|
91
|
+
dimension: VideoDimension | undefined;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type SubscriptionChanges = {
|
|
95
|
+
[sessionId: string]: SubscriptionChange;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type CallOptions = {
|
|
99
|
+
connectionConfig?: RTCConfiguration;
|
|
100
|
+
edgeName?: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type PublishOptions = {
|
|
104
|
+
preferredCodec?: string | null;
|
|
105
|
+
};
|