Skip to content

Commit 4349ca6

Browse files
authored
broadcast: add support for custom ICE servers (#593)
* feat: use google stun server * feat: support custom stun/turn servers * feat: add props to the Broadcast component * feat: update initialProps for broadcast * feat: support custom stun/turn servers for controller * feat: simplify interface
1 parent 49b11ab commit 4349ca6

File tree

8 files changed

+93
-38
lines changed

8 files changed

+93
-38
lines changed

packages/core-web/src/broadcast.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,21 @@ export type InitialBroadcastProps = {
142142
video: boolean | Omit<MediaTrackConstraints, "deviceId">;
143143

144144
/**
145+
* @deprecated in favor of `iceServers`
146+
*
145147
* Whether to disable ICE gathering.
146148
*
147149
* Set to true to disable ICE gathering. This is useful for testing purposes.
148150
*/
149151
noIceGathering?: boolean;
150152

153+
/**
154+
* The ICE servers to use.
155+
*
156+
* If not provided, the default ICE servers will be used.
157+
*/
158+
iceServers?: RTCIceServer | RTCIceServer[];
159+
151160
/**
152161
* Whether the video stream should be mirrored (horizontally flipped).
153162
*
@@ -335,6 +344,7 @@ export const createBroadcastStore = ({
335344
ingestUrl: ingestUrl ?? null,
336345
video: initialProps?.video ?? true,
337346
noIceGathering: initialProps?.noIceGathering ?? false,
347+
iceServers: initialProps?.iceServers,
338348
mirrored: initialProps?.mirrored ?? false,
339349
},
340350

@@ -475,7 +485,7 @@ export const createBroadcastStore = ({
475485
requestedVideoInputDeviceId:
476486
deviceIds?.videoinput === "screen"
477487
? "default"
478-
: (deviceIds?.videoinput ?? "default"),
488+
: deviceIds?.videoinput ?? "default",
479489
},
480490
})),
481491

@@ -488,10 +498,10 @@ export const createBroadcastStore = ({
488498
requestedVideoInputDeviceId: deviceId,
489499
}
490500
: type === "audioinput"
491-
? {
492-
requestedAudioInputDeviceId: deviceId,
493-
}
494-
: {}),
501+
? {
502+
requestedAudioInputDeviceId: deviceId,
503+
}
504+
: {}),
495505
},
496506
})),
497507

@@ -760,8 +770,9 @@ const addEffectsToStore = (
760770
__controls.requestedForceRenegotiateLastTime,
761771
mounted,
762772
noIceGathering: __initialProps.noIceGathering,
773+
iceServers: __initialProps.iceServers,
763774
}),
764-
async ({ enabled, ingestUrl, noIceGathering }) => {
775+
async ({ enabled, ingestUrl, noIceGathering, iceServers }) => {
765776
await cleanupWhip?.();
766777

767778
if (!enabled) {
@@ -803,6 +814,7 @@ const addEffectsToStore = (
803814
},
804815
sdpTimeout: null,
805816
noIceGathering,
817+
iceServers,
806818
});
807819

808820
cleanupWhip = () => {
@@ -891,10 +903,10 @@ const addEffectsToStore = (
891903
},
892904
}
893905
: audio
894-
? {
895-
...(audioConstraints ? audioConstraints : {}),
896-
}
897-
: false,
906+
? {
907+
...(audioConstraints ? audioConstraints : {}),
908+
}
909+
: false,
898910
video:
899911
video &&
900912
requestedVideoDeviceId &&
@@ -907,11 +919,11 @@ const addEffectsToStore = (
907919
...(mirrored ? { facingMode: "user" } : {}),
908920
}
909921
: video
910-
? {
911-
...(videoConstraints ? videoConstraints : {}),
912-
...(mirrored ? { facingMode: "user" } : {}),
913-
}
914-
: false,
922+
? {
923+
...(videoConstraints ? videoConstraints : {}),
924+
...(mirrored ? { facingMode: "user" } : {}),
925+
}
926+
: false,
915927
}));
916928

917929
if (stream) {
@@ -1133,8 +1145,8 @@ const addEffectsToStore = (
11331145
device.kind === "audioinput"
11341146
? "Audio Source"
11351147
: device.kind === "audiooutput"
1136-
? "Audio Output"
1137-
: "Video Source"
1148+
? "Audio Output"
1149+
: "Video Source"
11381150
} ${i + 1} (${
11391151
device.deviceId === "default"
11401152
? "default"

packages/core-web/src/media/controls/controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ const addEffectsToStore = (
470470
accessKey: store.getState().__initialProps.accessKey,
471471
},
472472
sdpTimeout: timeout,
473+
iceServers: store.getState().__initialProps.iceServers,
473474
});
474475

475476
const id = setTimeout(() => {

packages/core-web/src/webrtc/shared.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,41 @@ export const getRTCPeerConnectionConstructor = () => {
2020
);
2121
};
2222

23+
/**
24+
* Creates a new RTCPeerConnection instance with the given STUN and TURN servers.
25+
*/
2326
export function createPeerConnection(
2427
host: string | null,
28+
iceServers?: RTCIceServer | RTCIceServer[],
2529
): RTCPeerConnection | null {
2630
const RTCPeerConnectionConstructor = getRTCPeerConnectionConstructor();
2731

28-
if (RTCPeerConnectionConstructor) {
29-
// strip non-standard port number if present
30-
const hostNoPort = host?.split(":")[0];
31-
32-
const iceServers = host
33-
? [
34-
{
35-
urls: `stun:${hostNoPort}`,
36-
},
37-
{
38-
urls: `turn:${hostNoPort}`,
39-
username: "livepeer",
40-
credential: "livepeer",
41-
},
42-
]
43-
: [];
44-
45-
return new RTCPeerConnectionConstructor({ iceServers });
32+
if (!RTCPeerConnectionConstructor) {
33+
throw new Error("No RTCPeerConnection constructor found in this browser.");
4634
}
4735

48-
throw new Error("No RTCPeerConnection constructor found in this browser.");
36+
// Defaults to Mist behavior
37+
const hostNoPort = host?.split(":")[0];
38+
const defaultIceServers = host
39+
? [
40+
{
41+
urls: `stun:${hostNoPort}`,
42+
},
43+
{
44+
urls: `turn:${hostNoPort}`,
45+
username: "livepeer",
46+
credential: "livepeer",
47+
},
48+
]
49+
: [];
50+
51+
return new RTCPeerConnectionConstructor({
52+
iceServers: iceServers
53+
? Array.isArray(iceServers)
54+
? iceServers
55+
: [iceServers]
56+
: defaultIceServers,
57+
});
4958
}
5059

5160
const DEFAULT_TIMEOUT = 10000;

packages/core-web/src/webrtc/whep.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const createNewWHEP = <TElement extends HTMLMediaElement>({
2121
callbacks,
2222
accessControl,
2323
sdpTimeout,
24+
iceServers,
2425
}: {
2526
source: string;
2627
element: TElement;
@@ -32,6 +33,7 @@ export const createNewWHEP = <TElement extends HTMLMediaElement>({
3233
};
3334
accessControl: AccessControlParams;
3435
sdpTimeout: number | null;
36+
iceServers?: RTCIceServer | RTCIceServer[];
3537
}): {
3638
destroy: () => void;
3739
} => {
@@ -76,7 +78,7 @@ export const createNewWHEP = <TElement extends HTMLMediaElement>({
7678
* allowing the client to discover its own IP address.
7779
* https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols#ice
7880
*/
79-
peerConnection = createPeerConnection(redirectUrl.host);
81+
peerConnection = createPeerConnection(redirectUrl.host, iceServers);
8082

8183
if (peerConnection) {
8284
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTransceiver */

packages/core-web/src/webrtc/whip.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const createNewWHIP = <TElement extends HTMLMediaElement>({
2828
callbacks,
2929
sdpTimeout,
3030
noIceGathering,
31+
iceServers,
3132
}: {
3233
ingestUrl: string;
3334
element: TElement;
@@ -38,6 +39,7 @@ export const createNewWHIP = <TElement extends HTMLMediaElement>({
3839
};
3940
sdpTimeout: number | null;
4041
noIceGathering?: boolean;
42+
iceServers?: RTCIceServer | RTCIceServer[];
4143
}): {
4244
destroy: () => void;
4345
} => {
@@ -70,7 +72,7 @@ export const createNewWHIP = <TElement extends HTMLMediaElement>({
7072
* allowing the client to discover its own IP address.
7173
* https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols#ice
7274
*/
73-
peerConnection = createPeerConnection(redirectUrl.host);
75+
peerConnection = createPeerConnection(redirectUrl.host, iceServers);
7476

7577
if (peerConnection) {
7678
peerConnection.addEventListener("negotiationneeded", async (_ev) => {

packages/core/src/media/controller.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ export type InitialProps = {
156156
viewerId: string | null;
157157

158158
ingestPlayback: boolean;
159+
160+
/**
161+
* The ICE servers to use.
162+
*
163+
* If not provided, the default ICE servers will be used.
164+
*/
165+
iceServers?: RTCIceServer | RTCIceServer[];
159166
};
160167

161168
export type DeviceInformation = {
@@ -662,6 +669,7 @@ export const createControllerStore = ({
662669
viewerId: initialProps.viewerId ?? null,
663670
volume: initialVolume ?? null,
664671
ingestPlayback: initialProps.ingestPlayback ?? false,
672+
iceServers: initialProps.iceServers,
665673
},
666674

667675
__device: device,

packages/react/src/broadcast/Broadcast.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,21 @@ interface BroadcastProps
5656
*/
5757
metricsInterval?: number;
5858

59+
/**
60+
* @deprecated in favor of `iceServers`
61+
*
62+
* Whether to disable ICE gathering.
63+
*
64+
* Set to true to disable ICE gathering. This is useful for testing purposes.
65+
*/
5966
noIceGathering?: boolean;
67+
68+
/**
69+
* The ICE servers to use.
70+
*
71+
* If not provided, the default ICE servers will be used.
72+
*/
73+
iceServers?: RTCIceServer | RTCIceServer[];
6074
}
6175

6276
const Broadcast = (

packages/react/src/player/Player.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ interface PlayerProps
5353
* The interval at which metrics are sent, in ms. Defaults to 5000.
5454
*/
5555
metricsInterval?: number;
56+
57+
/**
58+
* The ICE servers to use.
59+
*
60+
* If not provided, the default ICE servers will be used.
61+
*/
62+
iceServers?: RTCIceServer | RTCIceServer[];
5663
}
5764

5865
const Player = React.memo((props: MediaScopedProps<PlayerProps>) => {

0 commit comments

Comments
 (0)