173 lines
4.8 KiB
TypeScript
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" />
|
|
}
|