import React from "react" import * as antd from "antd" import classnames from "classnames" import Livestream from "models/livestream" import { UserPreview, LiveChat } from "components" import { Icons } from "components/Icons" import Ticker from "react-ticker" import Plyr from "plyr" import Hls from "hls.js" import mpegts from "mpegts.js" import "plyr/dist/plyr.css" import "./index.less" export default class StreamViewer extends React.Component { state = { requestedUsername: null, isEnded: false, loading: true, cinemaMode: false, stream: null, spectators: 0, player: null, decoderInstance: null, } videoPlayerRef = React.createRef() playerDecoderEvents = { [Hls.Events.FPS_DROP]: (event, data) => { console.log("FPS_DROP Detected", data) }, } attachDecoder = { flv: async (source) => { if (!source) { console.error("Stream source is not defined") return false } this.toggleLoading(true) const decoderInstance = mpegts.createPlayer({ type: "flv", isLive: true, enableWorker: true, url: source }) decoderInstance.on(mpegts.Events.ERROR, this.onSourceEnd) decoderInstance.attachMediaElement(this.videoPlayerRef.current) decoderInstance.load() await decoderInstance.play().catch((error) => { console.error(error) }) this.toggleLoading(false) return decoderInstance }, hls: (source) => { if (!source) { console.error("Stream source is not defined") return false } const hlsInstance = new Hls({ autoStartLoad: true, }) hlsInstance.attachMedia(this.videoPlayerRef.current) hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => { hlsInstance.loadSource(source) hlsInstance.on(Hls.Events.MANIFEST_PARSED, (event, data) => { console.log(`${data.levels.length} quality levels found`) }) }) hlsInstance.on(Hls.Events.ERROR, (event, data) => { console.error(event, data) switch (data.details) { case Hls.ErrorDetails.FRAG_LOAD_ERROR: { console.error(`Error loading fragment ${data.frag.url}`) return } default: { return } } }) // register player decoder events Object.keys(this.playerDecoderEvents).forEach(event => { hlsInstance.on(event, this.playerDecoderEvents[event]) }) return hlsInstance } } onSourceEnd = () => { if (typeof this.state.decoderInstance?.destroy === "function") { this.state.decoderInstance.destroy() } this.state.player.destroy() this.setState({ isEnded: true, loading: false, cinemaMode: false, }) } loadStream = async (payload) => { const stream = await Livestream.getLivestream({ username: payload.username, profile_id: payload.profile, }).catch((error) => { console.error(error) if (this.streamInfoInterval) { this.streamInfoInterval = clearInterval(this.streamInfoInterval) } return null }) if (!stream) { return false } console.log("Stream data >", stream) this.setState({ stream: stream, spectators: stream.connectedClients, }) return stream } attachPlayer = () => { // check if user has interacted with the page const player = new Plyr("#player", { clickToPlay: false, autoplay: true, muted: true, controls: ["mute", "volume", "fullscreen", "airplay", "options", "settings",], settings: ["quality"], }) player.muted = true // insert a button to enter to cinema mode player.elements.buttons.fullscreen.insertAdjacentHTML("beforeBegin", ` `) // insert radio mode button player.elements.buttons.fullscreen.insertAdjacentHTML("beforeBegin", ` `) player.elements.buttons.cinema = player.elements.container.querySelector("[data-plyr='cinema']") player.elements.buttons.radio = player.elements.container.querySelector("[data-plyr='radio']") player.elements.buttons.cinema.addEventListener("click", () => this.toggleCinemaMode()) player.elements.buttons.radio.addEventListener("click", () => this.toggleRadioMode()) this.setState({ player, }) } componentDidMount = async () => { this.enterPlayerAnimation() const requestedUsername = this.props.params.key const profile = this.props.query.profile await this.setState({ requestedUsername, }) this.attachPlayer() // get stream info const stream = await this.loadStream({ username: requestedUsername, profile: profile, }) if (!stream) { return this.onSourceEnd() } // load the flv decoder (by default) if (this.state.stream) { if (!this.state.stream.sources) { console.error("Stream sources not found") return } await this.loadDecoder("flv", this.state.stream.sources.flv) } // TODO: Watch ws to get livestream:started event and load the decoder if it's not loaded // set a interval to update the stream info this.streamInfoInterval = setInterval(() => { this.loadStream({ username: this.state.requestedUsername, profile: profile, }) }, 1000 * 60) } componentWillUnmount = () => { if (this.state.player) { this.state.player.destroy() } if (typeof this.state.decoderInstance?.unload === "function") { this.state.decoderInstance.unload() } this.exitPlayerAnimation() this.toggleCinemaMode(false) if (this.streamInfoInterval) { clearInterval(this.streamInfoInterval) } } enterPlayerAnimation = () => { app.layout.toggleCenteredContent(false) // make the interface a bit confortable for a video player app.cores.style.applyVariant("dark") app.cores.style.compactMode(true) } exitPlayerAnimation = () => { app.cores.style.applyVariant(app.cores.settings.get("themeVariant")) app.cores.style.compactMode(false) } updateQuality = (newQuality) => { if (this.state.loadedProtocol !== "hls") { console.error("Unsupported protocol") return false } this.state.protocolInstance.levels.forEach((level, levelIndex) => { if (level.height === newQuality) { console.log("Found quality match with " + newQuality) this.state.protocolInstance.currentLevel = levelIndex } }) } loadDecoder = async (decoder, ...args) => { if (typeof this.attachDecoder[decoder] === "undefined") { console.error("Protocol not supported") return false } // check if decoder is already loaded if (this.state.decoderInstance) { if (typeof this.state.decoderInstance.destroy === "function") { this.state.decoderInstance.destroy() } this.setState({ decoderInstance: null }) } console.log(`Switching decoder to: ${decoder}`) const decoderInstance = await this.attachDecoder[decoder](...args) await this.setState({ decoderInstance: decoderInstance }) return decoderInstance } toggleLoading = (to) => { this.setState({ loading: to ?? !this.state.loading }) } toggleCinemaMode = (to) => { if (typeof to === "undefined") { to = !this.state.cinemaMode } if (app.layout.sidebar) { app.layout.sidebar.toggleVisibility(!to) } this.setState({ cinemaMode: to }) } toggleRadioMode = (to) => { if (typeof to === "undefined") { to = !this.state.radioMode } if (to) { app.cores.player.start({ src: this.state.stream?.sources.aac, title: this.state.stream?.info.title, artist: this.state.stream?.info.username, }) // setLocation to main page app.navigation.goMain() } } render() { return
{ this.state.stream ? <>
{ !this.state.isEnded &&
} > {this.state.spectators}
}
{ this.state.stream.info &&

{this.state.stream.info?.title}

{({ index }) => { return

{this.state.stream.info?.description}

}}
} : }
{ !this.state.cinemaMode &&

Live chat

}
} }