diff --git a/src/css/style.css b/src/css/style.css index 755b12c..bb41558 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -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 { @@ -108,4 +114,18 @@ li.menu-selected { #app { 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); } } \ No newline at end of file diff --git a/src/js/media.tsx b/src/js/media.tsx index 78966b5..2ea5df7 100644 --- a/src/js/media.tsx +++ b/src/js/media.tsx @@ -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 { - 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.") - } - async componentDidUpdate() { - if (!this.state.iceReady) { - return + pc.oniceconnectionstatechange = () => { + console.log("ICE state changed to " + pc.iceConnectionState) } - 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 } - }) - } else { - console.log("Could not set remote") - } - } - } - private setupRemote() { - if (!this.props.selectedStream) { + pc.createOffer().then((offer) => { + pc.setLocalDescription(offer).then(() => { console.log("Local description set.") }) + }) + }, [pc]) + + 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]) - renderVideo() { - if (!this.props.selectedStream) { - return

Waiting for stream selection.

+ useEffect(() => { + console.log("effect: remoteReady") + if (!iceReady || !selectedStream || !remoteReady) { + return + } + }, [remoteReady]) + + + const getElement = () => { + if (!selectedStream) { + return + } + if (!iceReady) { + return + } + if (!remoteReady) { + return } - if (!this.state.iceReady) { - return

Waiting for ICE gathering to complete.

- } - if (!this.state.remoteReady) { - return

Waiting for remote.

- } - return ( - <> -

Waiting for video to load.

-