2022-06-02 00:44:47 +02:00

291 lines
8.6 KiB
JavaScript

import React from "react"
import config from "config"
import * as antd from "antd"
import { Icons } from "components/Icons"
import moment from "moment"
import Plyr from "plyr"
import Hls from "hls.js"
import mpegts from "mpegts.js"
import "plyr/dist/plyr.css"
import "./index.less"
const streamsSource = config.remotes.streamingApi
export default class StreamViewer extends React.Component {
state = {
isEnded: false,
isLoading: true,
endResult: false,
userData: null,
streamInfo: null,
spectators: 0,
timeFromNow: "00:00:00",
player: null,
streamKey: null,
streamSource: null,
loadedProtocol: "flv",
protocolInstance: null,
plyrOptions: {},
}
videoPlayerRef = React.createRef()
componentDidMount = async () => {
window.StreamViewer = {
updateQuality: this.updateQuality,
switchProtocol: this.switchProtocol,
}
const query = new URLSearchParams(window.location.search)
const requestedUsername = query.get("key")
const source = `${streamsSource}/${requestedUsername}`
const player = new Plyr("#player", {
autoplay: true,
controls: ["play", "mute", "volume", "fullscreen", "options", "settings"],
...this.state.plyrOptions,
})
await this.setState({
player,
streamKey: requestedUsername,
streamSource: source,
})
await this.loadWithProtocol[this.state.loadedProtocol]()
// make the interface a bit confortable for a video player
app.style.applyVariant("dark")
app.eventBus.emit("toogleCompactMode", true)
app.SidebarController.toggleVisibility(false)
// fetch user info in the background
this.gatherUserInfo()
// fetch stream info in the background
// await for it
await this.gatherStreamInfo()
// // create timer
// if (this.state.streamInfo.connectCreated) {
// this.createTimerCounter()
// }
}
componentWillUnmount = () => {
app.style.applyVariant(app.settings.get("themeVariant"))
app.eventBus.emit("toogleCompactMode", false)
app.SidebarController.toggleVisibility(true)
if (this.timerCounterInterval) {
this.timerCounterInterval = clearInterval(this.timerCounterInterval)
}
}
gatherStreamInfo = async () => {
const result = await app.request.get.streamInfoFromUsername(undefined, {
username: this.state.streamKey,
}).catch((error) => {
console.error(error)
antd.message.error(error.message)
return false
})
if (result) {
this.setState({
streamInfo: result,
})
}
}
gatherUserInfo = async () => {
const result = await app.request.get.user(undefined, {
username: this.state.streamKey,
}).catch((error) => {
console.error(error)
antd.message.error(error)
return false
})
if (result) {
this.setState({
userData: result,
})
}
}
createTimerCounter = () => {
this.timerCounterInterval = setInterval(() => {
const secondsFromNow = moment().diff(moment(this.state.streamInfo.connectCreated), "seconds")
// calculate hours minutes and seconds
const hours = Math.floor(secondsFromNow / 3600)
const minutes = Math.floor((secondsFromNow - hours * 3600) / 60)
const seconds = secondsFromNow - hours * 3600 - minutes * 60
this.setState({
timeFromNow: `${hours}:${minutes}:${seconds}`,
})
}, 1000)
}
updateQuality = (newQuality) => {
if (loadedProtocol === "hls") {
this.state.protocolInstance.levels.forEach((level, levelIndex) => {
if (level.height === newQuality) {
console.log("Found quality match with " + newQuality)
this.state.protocolInstance.currentLevel = levelIndex
}
})
}
else {
console.error("Unsupported protocol")
}
}
switchProtocol = (protocol) => {
if (typeof this.state.protocolInstance.destroy === "function") {
this.state.protocolInstance.destroy()
}
this.setState({ protocolInstance: null })
console.log("Switching to " + protocol)
this.loadWithProtocol[protocol]()
}
loadWithProtocol = {
hls: () => {
const source = `${streamsSource}/media/${this.state.streamKey}/index.m3u8`
const hls = new Hls()
hls.loadSource(source)
hls.attachMedia(this.videoPlayerRef.current)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
const levels = hls.levels
const quality = {
default: levels[levels.length - 1].height,
options: levels.map((level) => level.height),
forced: true,
onChange: (newQuality) => {
console.log('changes', newQuality)
levels.forEach((level, levelIndex) => {
if (level.height === newQuality) {
hls.currentLevel = levelIndex
}
})
},
}
this.setState({
plyrOptions: {
quality,
...this.state.plyrOptions
}
})
})
this.setState({ protocolInstance: hls, loadedProtocol: "hls" })
},
flv: () => {
const source = `${streamsSource}/stream/flv/${this.state.streamKey}`
const instance = mpegts.createPlayer({
enableWorker: true,
type: "flv",
url: source,
cors: true,
isLive: true
})
instance.attachMediaElement(this.videoPlayerRef.current)
instance.load()
instance.play()
instance.on("error", (error) => {
console.error(error)
antd.message.error(error.toString())
//this.onSourceEnded(error)
})
// instance.on("onSourceOpen", this.onSourceLoading)
// instance.on(mpegts.Events["LOADING_COMPLETE"], this.onSourceLoadingDone)
// instance.on("onSourceEnded", this.onSourceEnded)
this.setState({ protocolInstance: instance, loadedProtocol: "flv" })
},
}
onSourceLoading = () => {
this.setState({
isLoading: true,
})
}
onSourceLoadingDone = () => {
console.log(`loaded ${this.state.loadedProtocol}`)
this.setState({
isLoading: false,
})
}
onSourceEnded = () => {
console.log("Source ended")
this.setState({ isEnded: true })
}
render() {
return <div className="stream">
{
this.state.isEnded && <div className="stream_end">
<antd.Result>
<h1>
This stream is ended
</h1>
</antd.Result>
</div>
}
<video ref={this.videoPlayerRef} id="player" />
<div className="panel">
<div className="info">
<div className="title">
<div>
<antd.Avatar
shape="square"
src={this.state.userData?.avatar}
/>
</div>
<div>
<h2>{this.state.userData?.username}</h2>
</div>
</div>
<div id="spectatorCount">
<Icons.Eye />
{this.state.spectators}
</div>
</div>
<div className="chatbox">
{/* TODO: Use chatbox component and join to stream channel using username */}
<antd.Result>
<h1>
Cannot connect with chat server
</h1>
</antd.Result>
</div>
</div>
</div>
}
}