2023-12-06 02:52:39 +01:00

173 lines
4.8 KiB
TypeScript

import { ComponentProps, useRef, useEffect, useState } from "react"
import { MinistreamApiClient } from "./api"
import { resolve } from "path"
import React from "react"
import { Log } from "./log"
type MediaContainerProps = {
selectedStream: string | null
api: MinistreamApiClient
}
export function MediaContainer({ selectedStream, api }: MediaContainerProps) {
const [iceReady, setICEReady] = useState(false)
const [remoteReady, setRemoteReady] = useState(false)
const [pc, setPC] = useState(new RTCPeerConnection())
const [ms, setMs] = useState(new MediaStream())
// if selected stream changed, recreate pc, and set all to not ready.
useEffect(() => {
Log.Debug("Ran useEffect.", { "because_state": "selectedStream" })
setPC(new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
}))
setICEReady(false)
setRemoteReady(false)
setMs(new MediaStream())
}, [selectedStream])
useEffect(() => {
Log.Debug("Ran useEffect.", { "because_state": "pc" })
pc.addTransceiver("video")
pc.addTransceiver("audio")
pc.ontrack = (event) => {
event.streams.forEach((st) => st.getTracks().forEach((track) => {
ms.addTrack(track)
}))
}
pc.onicecandidate = (event) => {
if (!event.candidate) {
Log.Info("ICE gathering complete.")
setICEReady(true)
} else {
Log.Debug("Adding ICE candidate.", { "candidate": event.candidate.candidate })
}
}
pc.oniceconnectionstatechange = () => {
Log.Info("ICE gathering complete.")
}
pc.createOffer().then((offer) => {
pc.setLocalDescription(offer).then(() => { Log.Debug("Local description set.") })
})
}, [pc])
useEffect(() => {
Log.Debug("Ran useEffect", { "because_state": "iceReady" })
if (!iceReady || !selectedStream) {
return
}
const localOfferSdp = pc.localDescription?.sdp
if (!localOfferSdp) {
Log.Error("Unable to get local description.")
return
}
const localOffer = new RTCSessionDescription({ type: "offer", sdp: localOfferSdp })
api.postOffer(selectedStream, localOffer).then((remote) => {
pc.setRemoteDescription(remote).then(() => {
setRemoteReady(true)
})
})
}, [iceReady])
useEffect(() => {
Log.Debug("Ran useEffect", { "because_state": "remoteReady" })
if (!iceReady || !selectedStream || !remoteReady) {
return
}
}, [remoteReady])
const getElement = () => {
if (!selectedStream) {
return <LoadingText spinner={true} text="Waiting for stream selection." />
}
if (!iceReady) {
return <LoadingText spinner={true} text="Waiting for ICE gathering." />
}
if (!remoteReady) {
return <LoadingText spinner={true} text="Waiting for remote server." />
}
return (
<div className="video-wrapper">
<VideoPlayer ms={ms} />
</div>
)
}
return (
<div id="main">
<main>
{getElement()}
</main>
</div>
)
}
interface VideoPlayerProps {
ms: MediaStream
}
export function VideoPlayer({ ms }: VideoPlayerProps) {
const [ready, setReady] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
if (ready) {
return
}
if (videoRef.current) {
if (videoRef.current) {
const ref = videoRef.current
ref.srcObject = ms
videoRef.current.addEventListener('loadeddata', () => {
ref.hidden = false
setReady(true)
})
}
}
})
if (ready) {
return (<video id="video" ref={videoRef} autoPlay muted controls />)
} else {
return (
<>
<video id="video" ref={videoRef} hidden autoPlay muted controls />
<LoadingText text="Waiting for video data." spinner={true} />
</>
)
}
}
interface LoadingText {
text: string
spinner?: boolean
}
export function LoadingText({ text, spinner = false }: LoadingText) {
var spinnerElement = (<></>)
if (spinner) {
spinnerElement = (<Spinner />)
}
return (
<>
<div className="loader-wrapper">
<div className="loader">
{spinnerElement}
<p className="loading-text">{text}</p>
</div>
</div>
</>
)
}
export function Spinner() {
return <div className="spinner" />
}