2023-12-16 00:24:50 +01:00
|
|
|
import { baseUrl } from "@/api/baseUrl";
|
2024-03-15 19:46:17 +01:00
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
2023-12-16 00:24:50 +01:00
|
|
|
|
|
|
|
type WebRtcPlayerProps = {
|
2024-02-10 13:30:53 +01:00
|
|
|
className?: string;
|
2023-12-16 00:24:50 +01:00
|
|
|
camera: string;
|
2024-02-15 01:19:55 +01:00
|
|
|
playbackEnabled?: boolean;
|
2024-03-02 01:43:02 +01:00
|
|
|
audioEnabled?: boolean;
|
2024-03-13 00:19:02 +01:00
|
|
|
microphoneEnabled?: boolean;
|
2024-03-15 19:46:17 +01:00
|
|
|
iOSCompatFullScreen?: boolean; // ios doesn't support fullscreen divs so we must support the video element
|
2024-04-02 14:45:16 +02:00
|
|
|
pip?: boolean;
|
2024-02-10 13:30:53 +01:00
|
|
|
onPlaying?: () => void;
|
2023-12-16 00:24:50 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
export default function WebRtcPlayer({
|
2024-02-10 13:30:53 +01:00
|
|
|
className,
|
2023-12-16 00:24:50 +01:00
|
|
|
camera,
|
2024-02-15 01:19:55 +01:00
|
|
|
playbackEnabled = true,
|
2024-03-02 01:43:02 +01:00
|
|
|
audioEnabled = false,
|
2024-03-13 00:19:02 +01:00
|
|
|
microphoneEnabled = false,
|
2024-03-15 19:46:17 +01:00
|
|
|
iOSCompatFullScreen = false,
|
2024-04-02 14:45:16 +02:00
|
|
|
pip = false,
|
2024-02-10 13:30:53 +01:00
|
|
|
onPlaying,
|
2023-12-16 00:24:50 +01:00
|
|
|
}: WebRtcPlayerProps) {
|
2024-03-13 15:04:11 +01:00
|
|
|
// metadata
|
|
|
|
|
|
|
|
const wsURL = useMemo(() => {
|
|
|
|
return `${baseUrl.replace(/^http/, "ws")}live/webrtc/api/ws?src=${camera}`;
|
|
|
|
}, [camera]);
|
|
|
|
|
2024-02-15 01:19:55 +01:00
|
|
|
// camera states
|
|
|
|
|
2023-12-16 00:24:50 +01:00
|
|
|
const pcRef = useRef<RTCPeerConnection | undefined>();
|
|
|
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
2024-02-15 01:19:55 +01:00
|
|
|
|
2023-12-16 00:24:50 +01:00
|
|
|
const PeerConnection = useCallback(
|
|
|
|
async (media: string) => {
|
|
|
|
if (!videoRef.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const pc = new RTCPeerConnection({
|
|
|
|
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
|
|
|
});
|
|
|
|
|
|
|
|
const localTracks = [];
|
|
|
|
|
|
|
|
if (/camera|microphone/.test(media)) {
|
|
|
|
const tracks = await getMediaTracks("user", {
|
|
|
|
video: media.indexOf("camera") >= 0,
|
|
|
|
audio: media.indexOf("microphone") >= 0,
|
|
|
|
});
|
|
|
|
tracks.forEach((track) => {
|
|
|
|
pc.addTransceiver(track, { direction: "sendonly" });
|
|
|
|
if (track.kind === "video") localTracks.push(track);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (media.indexOf("display") >= 0) {
|
|
|
|
const tracks = await getMediaTracks("display", {
|
|
|
|
video: true,
|
|
|
|
audio: media.indexOf("speaker") >= 0,
|
|
|
|
});
|
|
|
|
tracks.forEach((track) => {
|
|
|
|
pc.addTransceiver(track, { direction: "sendonly" });
|
|
|
|
if (track.kind === "video") localTracks.push(track);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (/video|audio/.test(media)) {
|
|
|
|
const tracks = ["video", "audio"]
|
|
|
|
.filter((kind) => media.indexOf(kind) >= 0)
|
|
|
|
.map(
|
|
|
|
(kind) =>
|
2024-02-28 23:23:56 +01:00
|
|
|
pc.addTransceiver(kind, { direction: "recvonly" }).receiver.track,
|
2023-12-16 00:24:50 +01:00
|
|
|
);
|
|
|
|
localTracks.push(...tracks);
|
|
|
|
}
|
|
|
|
|
|
|
|
videoRef.current.srcObject = new MediaStream(localTracks);
|
|
|
|
return pc;
|
|
|
|
},
|
2024-02-28 23:23:56 +01:00
|
|
|
[videoRef],
|
2023-12-16 00:24:50 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
async function getMediaTracks(
|
|
|
|
media: string,
|
2024-02-28 23:23:56 +01:00
|
|
|
constraints: MediaStreamConstraints,
|
2023-12-16 00:24:50 +01:00
|
|
|
) {
|
|
|
|
try {
|
|
|
|
const stream =
|
|
|
|
media === "user"
|
|
|
|
? await navigator.mediaDevices.getUserMedia(constraints)
|
|
|
|
: await navigator.mediaDevices.getDisplayMedia(constraints);
|
|
|
|
return stream.getTracks();
|
|
|
|
} catch (e) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const connect = useCallback(
|
2024-03-13 15:04:11 +01:00
|
|
|
async (aPc: Promise<RTCPeerConnection | undefined>) => {
|
2023-12-16 00:24:50 +01:00
|
|
|
if (!aPc) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
pcRef.current = await aPc;
|
2024-03-13 15:04:11 +01:00
|
|
|
const ws = new WebSocket(wsURL);
|
2023-12-16 00:24:50 +01:00
|
|
|
|
|
|
|
ws.addEventListener("open", () => {
|
|
|
|
pcRef.current?.addEventListener("icecandidate", (ev) => {
|
|
|
|
if (!ev.candidate) return;
|
|
|
|
const msg = {
|
|
|
|
type: "webrtc/candidate",
|
|
|
|
value: ev.candidate.candidate,
|
|
|
|
};
|
|
|
|
ws.send(JSON.stringify(msg));
|
|
|
|
});
|
|
|
|
|
|
|
|
pcRef.current
|
|
|
|
?.createOffer()
|
|
|
|
.then((offer) => pcRef.current?.setLocalDescription(offer))
|
|
|
|
.then(() => {
|
|
|
|
const msg = {
|
|
|
|
type: "webrtc/offer",
|
|
|
|
value: pcRef.current?.localDescription?.sdp,
|
|
|
|
};
|
|
|
|
ws.send(JSON.stringify(msg));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
ws.addEventListener("message", (ev) => {
|
|
|
|
const msg = JSON.parse(ev.data);
|
|
|
|
if (msg.type === "webrtc/candidate") {
|
|
|
|
pcRef.current?.addIceCandidate({ candidate: msg.value, sdpMid: "0" });
|
|
|
|
} else if (msg.type === "webrtc/answer") {
|
|
|
|
pcRef.current?.setRemoteDescription({
|
|
|
|
type: "answer",
|
|
|
|
sdp: msg.value,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
2024-03-13 15:04:11 +01:00
|
|
|
[wsURL],
|
2023-12-16 00:24:50 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!videoRef.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-02-15 01:19:55 +01:00
|
|
|
if (!playbackEnabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-03-13 00:19:02 +01:00
|
|
|
const aPc = PeerConnection(
|
|
|
|
microphoneEnabled ? "video+audio+microphone" : "video+audio",
|
|
|
|
);
|
2024-03-13 15:04:11 +01:00
|
|
|
connect(aPc);
|
2023-12-16 00:24:50 +01:00
|
|
|
|
|
|
|
return () => {
|
|
|
|
if (pcRef.current) {
|
|
|
|
pcRef.current.close();
|
|
|
|
pcRef.current = undefined;
|
|
|
|
}
|
|
|
|
};
|
2024-03-13 00:19:02 +01:00
|
|
|
}, [
|
|
|
|
camera,
|
|
|
|
connect,
|
|
|
|
PeerConnection,
|
|
|
|
pcRef,
|
|
|
|
videoRef,
|
|
|
|
playbackEnabled,
|
|
|
|
microphoneEnabled,
|
|
|
|
]);
|
2023-12-16 00:24:50 +01:00
|
|
|
|
2024-03-15 19:46:17 +01:00
|
|
|
// ios compat
|
2024-04-02 14:45:16 +02:00
|
|
|
|
2024-03-15 19:46:17 +01:00
|
|
|
const [iOSCompatControls, setiOSCompatControls] = useState(false);
|
|
|
|
|
2024-04-02 14:45:16 +02:00
|
|
|
// control pip
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!videoRef.current || !pip) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
videoRef.current.requestPictureInPicture();
|
|
|
|
}, [pip, videoRef]);
|
|
|
|
|
2023-12-16 00:24:50 +01:00
|
|
|
return (
|
2024-02-15 01:19:55 +01:00
|
|
|
<video
|
|
|
|
ref={videoRef}
|
|
|
|
className={className}
|
2024-03-15 19:46:17 +01:00
|
|
|
controls={iOSCompatControls}
|
2024-02-15 01:19:55 +01:00
|
|
|
autoPlay
|
|
|
|
playsInline
|
2024-03-02 01:43:02 +01:00
|
|
|
muted={!audioEnabled}
|
2024-02-15 01:19:55 +01:00
|
|
|
onLoadedData={onPlaying}
|
2024-03-15 19:46:17 +01:00
|
|
|
onClick={
|
|
|
|
iOSCompatFullScreen
|
|
|
|
? () => setiOSCompatControls(!iOSCompatControls)
|
|
|
|
: undefined
|
|
|
|
}
|
2024-02-15 01:19:55 +01:00
|
|
|
/>
|
2023-12-16 00:24:50 +01:00
|
|
|
);
|
|
|
|
}
|