Compare commits
No commits in common. "a267f9e0dfdf5a3be7ba721ab78afc43323321be" and "d1f389afde2ef00ef2b3520d3b687eefb63c3125" have entirely different histories.
a267f9e0df
...
d1f389afde
@ -23,15 +23,9 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
#main p {
|
||||||
display: flex;
|
margin-top: 10em;
|
||||||
}
|
text-align: center;
|
||||||
.loader .spinner {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
.loader .loading-text {
|
|
||||||
margin-top: 0px;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-wrapper {
|
.video-wrapper {
|
||||||
@ -115,17 +109,3 @@ li.menu-selected {
|
|||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
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); }
|
|
||||||
}
|
|
236
src/js/media.tsx
236
src/js/media.tsx
@ -1,4 +1,4 @@
|
|||||||
import React, { ComponentProps, useRef, useEffect, useState } from "react"
|
import React, { ComponentProps } from "react"
|
||||||
import { MinistreamApiClient } from "./api"
|
import { MinistreamApiClient } from "./api"
|
||||||
import { resolve } from "path"
|
import { resolve } from "path"
|
||||||
|
|
||||||
@ -7,159 +7,141 @@ type MediaContainerProps = {
|
|||||||
api: MinistreamApiClient
|
api: MinistreamApiClient
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MediaContainer({ selectedStream, api }: MediaContainerProps) {
|
type MediaContainerState = {
|
||||||
const [iceReady, setICEReady] = useState(false)
|
iceReady: boolean
|
||||||
const [remoteReady, setRemoteReady] = useState(false)
|
remoteReady: boolean
|
||||||
const [pc, setPC] = useState(new RTCPeerConnection())
|
videoReady: boolean
|
||||||
const [ms, setMs] = useState(new MediaStream())
|
|
||||||
|
|
||||||
// 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" }]
|
|
||||||
}))
|
|
||||||
setICEReady(false)
|
|
||||||
setRemoteReady(false)
|
|
||||||
setMs(new MediaStream())
|
|
||||||
|
|
||||||
}, [selectedStream])
|
|
||||||
|
|
||||||
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)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pc.onicecandidate = (event) => {
|
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({
|
||||||
|
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
|
||||||
|
})
|
||||||
|
this.pc.addTransceiver("video")
|
||||||
|
this.pc.addTransceiver("audio")
|
||||||
|
|
||||||
|
this.ms = new MediaStream()
|
||||||
|
|
||||||
|
this.pc.ontrack = (event) => {
|
||||||
|
console.log("Got track(s).")
|
||||||
|
|
||||||
|
event.streams.forEach(st => {
|
||||||
|
st.getTracks().forEach(t => {
|
||||||
|
this.ms.addTrack(t)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.pc.oniceconnectionstatechange = () => {
|
||||||
|
console.log("ICE state changed to " + this.pc?.iceConnectionState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
this.pc.onicecandidate = (event) => {
|
||||||
if (!event.candidate) {
|
if (!event.candidate) {
|
||||||
console.log("ICE gathering complete.")
|
console.log("ICE gathering complete.")
|
||||||
setICEReady(true)
|
this.setState((state) => {
|
||||||
|
return { iceReady: true, remoteReady: state.remoteReady }
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log("Adding ICE candidate: " + event.candidate)
|
console.log("Adding ICE candidate: " + event.candidate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const offer = await this.pc?.createOffer()
|
||||||
pc.oniceconnectionstatechange = () => {
|
await this.pc?.setLocalDescription(offer)
|
||||||
console.log("ICE state changed to " + pc.iceConnectionState)
|
console.log("Set local description.")
|
||||||
}
|
}
|
||||||
|
|
||||||
pc.createOffer().then((offer) => {
|
async componentDidUpdate() {
|
||||||
pc.setLocalDescription(offer).then(() => { console.log("Local description set.") })
|
if (!this.state.iceReady) {
|
||||||
})
|
|
||||||
}, [pc])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("effect: iceReady")
|
|
||||||
if (!iceReady || !selectedStream) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const localOfferSdp = pc.localDescription?.sdp
|
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) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const localOfferSdp = this.pc?.localDescription?.sdp
|
||||||
if (!localOfferSdp) {
|
if (!localOfferSdp) {
|
||||||
console.log("Unable to get local description")
|
console.log("Could not get local description")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const localOffer = new RTCSessionDescription({ type: "offer", sdp: localOfferSdp })
|
const localOffer = new RTCSessionDescription({ type: "offer", sdp: localOfferSdp })
|
||||||
api.postOffer(selectedStream, localOffer).then((remote) => {
|
return this.api.postOffer(this.props.selectedStream, localOffer)
|
||||||
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 (
|
return (
|
||||||
<div className="video-wrapper">
|
<>
|
||||||
<VideoPlayer ms={ms} />
|
<p id="loading-text">Waiting for video to load.</p>
|
||||||
</div>
|
<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 (
|
return (
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<main>
|
<main>
|
||||||
{getElement()}
|
<div className="video-wrapper">
|
||||||
|
{this.renderVideo()}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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…
x
Reference in New Issue
Block a user