Merge pull request 'Fix stream switching' (#3) from bugfix/1-stream-switching into master
Reviewed-on: #3
This commit is contained in:
commit
a267f9e0df
@ -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); }
|
||||
}
|
238
src/js/media.tsx
238
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<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.")
|
||||
}
|
||||
|
||||
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 <p>Waiting for stream selection.</p>
|
||||
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." />
|
||||
}
|
||||
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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="main">
|
||||
<main>
|
||||
<div className="video-wrapper">
|
||||
{this.renderVideo()}
|
||||
</div>
|
||||
</main>
|
||||
<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 (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" />
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user