Fix stream switching #3

Merged
torjus merged 1 commits from bugfix/1-stream-switching into master 2023-12-04 22:38:00 +00:00
2 changed files with 151 additions and 113 deletions

View File

@ -23,9 +23,15 @@ body {
font-weight: 600;
}
#main p {
margin-top: 10em;
text-align: center;
.loader {
display: flex;
}
.loader .spinner {
margin-top: 0px;
}
.loader .loading-text {
margin-top: 0px;
margin-left: 0.5em;
}
.video-wrapper {
@ -109,3 +115,17 @@ li.menu-selected {
display: flex;
height: 100%;
}
.spinner {
border: 4px solid var(--menu-header-text);
border-top: 4px solid var(--menu-selected-color);
border-radius: 50%;
width: 10px;
height: 10px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -1,4 +1,4 @@
import React, { ComponentProps } from "react"
import React, { ComponentProps, useRef, useEffect, useState } from "react"
import { MinistreamApiClient } from "./api"
import { resolve } from "path"
@ -7,141 +7,159 @@ type MediaContainerProps = {
api: MinistreamApiClient
}
type MediaContainerState = {
iceReady: boolean
remoteReady: boolean
videoReady: boolean
}
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())
export class MediaContainer extends React.Component<MediaContainerProps, MediaContainerState> {
api: MinistreamApiClient
pc: RTCPeerConnection
ms: MediaStream
constructor(props: MediaContainerProps) {
super(props)
this.api = props.api
this.state = {
iceReady: false,
remoteReady: false,
videoReady: false
}
this.pc = new RTCPeerConnection({
// if selected stream changed, recreate pc, and set all to not ready.
useEffect(() => {
console.log("effect: selectedStream")
setPC(new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
})
this.pc.addTransceiver("video")
this.pc.addTransceiver("audio")
}))
setICEReady(false)
setRemoteReady(false)
setMs(new MediaStream())
this.ms = new MediaStream()
}, [selectedStream])
this.pc.ontrack = (event) => {
console.log("Got track(s).")
event.streams.forEach(st => {
st.getTracks().forEach(t => {
this.ms.addTrack(t)
})
})
useEffect(() => {
console.log("effect: pc")
pc.addTransceiver("video")
pc.addTransceiver("audio")
pc.ontrack = (event) => {
event.streams.forEach((st) => st.getTracks().forEach((track) => {
ms.addTrack(track)
}))
}
this.pc.oniceconnectionstatechange = () => {
console.log("ICE state changed to " + this.pc?.iceConnectionState)
}
}
async componentDidMount() {
this.pc.onicecandidate = (event) => {
pc.onicecandidate = (event) => {
if (!event.candidate) {
console.log("ICE gathering complete.")
this.setState((state) => {
return { iceReady: true, remoteReady: state.remoteReady }
})
setICEReady(true)
} else {
console.log("Adding ICE candidate: " + event.candidate)
}
}
const offer = await this.pc?.createOffer()
await this.pc?.setLocalDescription(offer)
console.log("Set local description.")
pc.oniceconnectionstatechange = () => {
console.log("ICE state changed to " + pc.iceConnectionState)
}
async componentDidUpdate() {
if (!this.state.iceReady) {
return
}
if (!this.props.selectedStream) {
return
}
if (!this.state.remoteReady) {
console.log("Setting up remote for " + this.props.selectedStream + ".")
const remote = await this.setupRemote()
if (remote) {
this.pc.setRemoteDescription(remote)
console.log("Set up remote")
this.setState((state) => {
return { iceReady: state.iceReady, remoteReady: true }
pc.createOffer().then((offer) => {
pc.setLocalDescription(offer).then(() => { console.log("Local description set.") })
})
} else {
console.log("Could not set remote")
}
}
}
}, [pc])
private setupRemote() {
if (!this.props.selectedStream) {
useEffect(() => {
console.log("effect: iceReady")
if (!iceReady || !selectedStream) {
return
}
const localOfferSdp = this.pc?.localDescription?.sdp
const localOfferSdp = pc.localDescription?.sdp
if (!localOfferSdp) {
console.log("Could not get local description")
console.log("Unable to get local description")
return
}
const localOffer = new RTCSessionDescription({ type: "offer", sdp: localOfferSdp })
return this.api.postOffer(this.props.selectedStream, localOffer)
api.postOffer(selectedStream, localOffer).then((remote) => {
pc.setRemoteDescription(remote).then(() => {
setRemoteReady(true)
})
})
}, [iceReady])
useEffect(() => {
console.log("effect: 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." />
}
renderVideo() {
if (!this.props.selectedStream) {
return <p>Waiting for stream selection.</p>
}
if (!this.state.iceReady) {
return <p>Waiting for ICE gathering to complete.</p>
}
if (!this.state.remoteReady) {
return <p>Waiting for remote.</p>
}
return (
<>
<p id="loading-text">Waiting for video to load.</p>
<video ref={el => {
if (el && this.ms) {
el.srcObject = this.ms
el.hidden = true
el.addEventListener('loadeddata', () => {
const t = document.getElementById("loading-text")
el.hidden = false
if (t) {
t.hidden = true
}
})
}
}} id="video" autoPlay muted controls />
</>
<div className="video-wrapper">
<VideoPlayer ms={ms} />
</div>
)
}
render() {
return (
<div id="main">
<main>
<div className="video-wrapper">
{this.renderVideo()}
</div>
{getElement()}
</main>
</div>
)
}
interface VideoPlayerProps {
ms: MediaStream
}
export function VideoPlayer({ ms }: VideoPlayerProps) {
const [ready, setReady] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
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">
{spinnerElement}
<p className="loading-text">{text}</p>
</div>
</>
)
}
export function Spinner() {
return <div className="spinner" />
}