use comtyjs model

This commit is contained in:
SrGooglo 2025-03-06 04:02:07 +00:00
parent 98a04ee163
commit bdd85850e2

View File

@ -7,7 +7,7 @@ import UserPreview from "@components/UserPreview"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import LiveChat from "@components/LiveChat" import LiveChat from "@components/LiveChat"
import SpectrumAPI from "@classes/SpectrumAPI" import SpectrumModel from "@models/spectrum"
import Plyr from "plyr" import Plyr from "plyr"
import Hls from "hls.js" import Hls from "hls.js"
@ -17,386 +17,406 @@ import "plyr/dist/plyr.css"
import "./index.less" import "./index.less"
const DecodersEvents = { const DecodersEvents = {
[Hls.Events.FPS_DROP]: (event, data) => { [Hls.Events.FPS_DROP]: (event, data) => {
console.log("FPS_DROP Detected", data) console.log("FPS_DROP Detected", data)
}, },
} }
const StreamDecoders = { const StreamDecoders = {
flv: async (player, source, { onSourceEnd } = {}) => { flv: async (player, source, { onSourceEnd } = {}) => {
if (!source) { if (!source) {
console.error("Stream source is not defined") console.error("Stream source is not defined")
return false return false
} }
const decoderInstance = mpegts.createPlayer({ const decoderInstance = mpegts.createPlayer({
type: "flv", type: "flv",
isLive: true, isLive: true,
enableWorker: true, enableWorker: true,
url: source url: source,
}) })
if (typeof onSourceEnd === "function") { if (typeof onSourceEnd === "function") {
decoderInstance.on(mpegts.Events.ERROR, onSourceEnd) decoderInstance.on(mpegts.Events.ERROR, onSourceEnd)
} }
decoderInstance.attachMediaElement(player) decoderInstance.attachMediaElement(player)
decoderInstance.load() decoderInstance.load()
await decoderInstance.play().catch((error) => { await decoderInstance.play().catch((error) => {
console.error(error) console.error(error)
}) })
return decoderInstance return decoderInstance
}, },
hls: (player, source) => { hls: (player, source) => {
if (!source) { if (!source) {
console.error("Stream source is not defined") console.error("Stream source is not defined")
return false return false
} }
const hlsInstance = new Hls({ const hlsInstance = new Hls({
autoStartLoad: true, autoStartLoad: true,
}) })
hlsInstance.attachMedia(player.current) hlsInstance.attachMedia(player.current)
hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => { hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
hlsInstance.loadSource(source) hlsInstance.loadSource(source)
hlsInstance.on(Hls.Events.MANIFEST_PARSED, (event, data) => { hlsInstance.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
console.log(`${data.levels.length} quality levels found`) console.log(`${data.levels.length} quality levels found`)
}) })
}) })
hlsInstance.on(Hls.Events.ERROR, (event, data) => { hlsInstance.on(Hls.Events.ERROR, (event, data) => {
console.error(event, data) console.error(event, data)
switch (data.details) { switch (data.details) {
case Hls.ErrorDetails.FRAG_LOAD_ERROR: { case Hls.ErrorDetails.FRAG_LOAD_ERROR: {
console.error(`Error loading fragment ${data.frag.url}`) console.error(`Error loading fragment ${data.frag.url}`)
return return
} }
default: { default: {
return return
} }
} }
}) })
// register player decoder events // register player decoder events
Object.keys(DecodersEvents).forEach((event) => { Object.keys(DecodersEvents).forEach((event) => {
hlsInstance.on(event, DecodersEvents[event]) hlsInstance.on(event, DecodersEvents[event])
}) })
return hlsInstance return hlsInstance
} },
} }
export default class StreamViewer extends React.Component { export default class StreamViewer extends React.Component {
state = { state = {
isEnded: false, isEnded: false,
loading: true, loading: true,
cinemaMode: false, cinemaMode: false,
stream: null, stream: null,
spectators: 0, spectators: 0,
player: null, player: null,
decoderInstance: null, decoderInstance: null,
} }
videoPlayerRef = React.createRef() videoPlayerRef = React.createRef()
loadDecoder = async (decoder, ...args) => { loadDecoder = async (decoder, ...args) => {
if (typeof StreamDecoders[decoder] === "undefined") { if (typeof StreamDecoders[decoder] === "undefined") {
console.error("Protocol not supported") console.error("Protocol not supported")
return false return false
} }
await this.toggleLoading(true) await this.toggleLoading(true)
// check if decoder is already loaded // check if decoder is already loaded
if (this.state.decoderInstance) { if (this.state.decoderInstance) {
if (typeof this.state.decoderInstance.destroy === "function") { if (typeof this.state.decoderInstance.destroy === "function") {
this.state.decoderInstance.destroy() this.state.decoderInstance.destroy()
} }
this.setState({ decoderInstance: null }) this.setState({ decoderInstance: null })
} }
console.log(`Switching decoder to: ${decoder}`) console.log(`Switching decoder to: ${decoder}`)
const decoderInstance = await StreamDecoders[decoder](...args) const decoderInstance = await StreamDecoders[decoder](...args)
await this.setState({ await this.setState({
decoderInstance: decoderInstance decoderInstance: decoderInstance,
}) })
await this.toggleLoading(false) await this.toggleLoading(false)
return decoderInstance return decoderInstance
} }
loadStream = async (stream_id) => { loadStream = async (stream_id) => {
let stream = await SpectrumAPI.getLivestream(stream_id).catch((error) => { let stream = await SpectrumModel.getLivestream(stream_id).catch(
console.error(error) (error) => {
return null console.error(error)
}) return null
},
)
if (!stream) { if (!stream) {
return false return false
} }
if (Array.isArray(stream)) { if (Array.isArray(stream)) {
stream = stream[0] stream = stream[0]
} }
console.log("Stream data >", stream) console.log("Stream data >", stream)
this.setState({ this.setState({
stream: stream, stream: stream,
spectators: stream.viewers, spectators: stream.viewers,
}) })
return stream return stream
} }
onSourceEnd = () => { onSourceEnd = () => {
if (typeof this.state.decoderInstance?.destroy === "function") { if (typeof this.state.decoderInstance?.destroy === "function") {
this.state.decoderInstance.destroy() this.state.decoderInstance.destroy()
} }
this.state.player.destroy() this.state.player.destroy()
this.setState({ this.setState({
isEnded: true, isEnded: true,
loading: false, loading: false,
cinemaMode: false, cinemaMode: false,
}) })
} }
attachPlayer = () => { attachPlayer = () => {
// check if user has interacted with the page // check if user has interacted with the page
const player = new Plyr("#player", { const player = new Plyr("#player", {
clickToPlay: false, clickToPlay: false,
autoplay: true, autoplay: true,
muted: true, muted: true,
controls: ["mute", "volume", "fullscreen", "airplay", "options", "settings",], controls: [
settings: ["quality"], "mute",
}) "volume",
"fullscreen",
"airplay",
"options",
"settings",
],
settings: ["quality"],
})
player.muted = true player.muted = true
// insert a button to enter to cinema mode // insert a button to enter to cinema mode
player.elements.buttons.fullscreen.insertAdjacentHTML("beforeBegin", ` player.elements.buttons.fullscreen.insertAdjacentHTML(
"beforeBegin",
`
<button class="plyr__controls__item plyr__control" type="button" data-plyr="cinema"> <button class="plyr__controls__item plyr__control" type="button" data-plyr="cinema">
<span class="label">Cinema mode</span> <span class="label">Cinema mode</span>
</button> </button>
`) `,
)
player.elements.buttons.cinema = player.elements.container.querySelector("[data-plyr='cinema']") player.elements.buttons.cinema =
player.elements.container.querySelector("[data-plyr='cinema']")
player.elements.buttons.cinema.addEventListener("click", () => this.toggleCinemaMode()) player.elements.buttons.cinema.addEventListener("click", () =>
this.toggleCinemaMode(),
)
this.setState({ this.setState({
player, player,
}) })
} }
componentDidMount = async () => { componentDidMount = async () => {
this.enterPlayerAnimation() this.enterPlayerAnimation()
const stream_id = this.props.params.id const stream_id = this.props.params.id
console.log("Stream ID >", stream_id) console.log("Stream ID >", stream_id)
this.attachPlayer() this.attachPlayer()
// get stream info // get stream info
const stream = await this.loadStream(stream_id) const stream = await this.loadStream(stream_id)
if (!stream) { if (!stream) {
return this.onSourceEnd() return this.onSourceEnd()
} }
// load the flv decoder (by default) // load the flv decoder (by default)
if (stream) { if (stream) {
if (!stream.sources) { if (!stream.sources) {
console.error("Stream sources not found") console.error("Stream sources not found")
return return
} }
await this.loadDecoder("flv", await this.loadDecoder(
this.videoPlayerRef.current, "flv",
stream.sources.flv, this.videoPlayerRef.current,
{ stream.sources.flv,
onSourceEnd: this.onSourceEnd, {
} onSourceEnd: this.onSourceEnd,
) },
} )
} }
}
componentWillUnmount = () => { componentWillUnmount = () => {
if (this.state.player) { if (this.state.player) {
this.state.player.destroy() this.state.player.destroy()
} }
if (typeof this.state.decoderInstance?.unload === "function") { if (typeof this.state.decoderInstance?.unload === "function") {
this.state.decoderInstance.unload() this.state.decoderInstance.unload()
} }
this.exitPlayerAnimation() this.exitPlayerAnimation()
this.toggleCinemaMode(false) this.toggleCinemaMode(false)
if (this.streamInfoInterval) { if (this.streamInfoInterval) {
clearInterval(this.streamInfoInterval) clearInterval(this.streamInfoInterval)
} }
} }
enterPlayerAnimation = () => { enterPlayerAnimation = () => {
app.cores.style.applyTemporalVariant("dark") app.cores.style.applyTemporalVariant("dark")
app.layout.toggleCompactMode(true) app.layout.toggleCompactMode(true)
app.layout.toggleCenteredContent(false) app.layout.toggleCenteredContent(false)
app.controls.toggleUIVisibility(false) app.controls.toggleUIVisibility(false)
} }
exitPlayerAnimation = () => { exitPlayerAnimation = () => {
app.cores.style.applyVariant(app.cores.style.currentVariantKey) app.cores.style.applyVariant(app.cores.style.currentVariantKey)
app.layout.toggleCompactMode(false) app.layout.toggleCompactMode(false)
app.layout.toggleCenteredContent(true) app.layout.toggleCenteredContent(true)
app.controls.toggleUIVisibility(true) app.controls.toggleUIVisibility(true)
} }
updateQuality = (newQuality) => { updateQuality = (newQuality) => {
if (this.state.loadedProtocol !== "hls") { if (this.state.loadedProtocol !== "hls") {
console.error("Unsupported protocol") console.error("Unsupported protocol")
return false return false
} }
this.state.protocolInstance.levels.forEach((level, levelIndex) => { this.state.protocolInstance.levels.forEach((level, levelIndex) => {
if (level.height === newQuality) { if (level.height === newQuality) {
console.log("Found quality match with " + newQuality) console.log("Found quality match with " + newQuality)
this.state.protocolInstance.currentLevel = levelIndex this.state.protocolInstance.currentLevel = levelIndex
} }
}) })
} }
toggleLoading = (to) => { toggleLoading = (to) => {
this.setState({ loading: to ?? !this.state.loading }) this.setState({ loading: to ?? !this.state.loading })
} }
toggleCinemaMode = (to) => { toggleCinemaMode = (to) => {
if (typeof to === "undefined") { if (typeof to === "undefined") {
to = !this.state.cinemaMode to = !this.state.cinemaMode
} }
this.setState({ cinemaMode: to }) this.setState({ cinemaMode: to })
} }
render() { render() {
return <div return (
className={classnames( <div
"livestream", className={classnames("livestream", {
{ ["cinemaMode"]: this.state.cinemaMode,
["cinemaMode"]: this.state.cinemaMode, })}
} >
)} {this.props.query.id}
> <div className="livestream_player">
{this.props.query.id} <div className="livestream_player_header">
<div className="livestream_player"> <div
<div className="livestream_player_header"> className="livestream_player_header_exit"
<div onClick={() => app.location.back()}
className="livestream_player_header_exit" >
onClick={() => app.location.back()} <Icons.IoMdExit />
> </div>
<Icons.IoMdExit />
</div>
{ {this.state.stream ? (
this.state.stream <>
? <> <div className="livestream_player_header_user">
<div className="livestream_player_header_user"> <UserPreview
user={this.state.stream.user}
/>
<div className="livestream_player_indicators">
{!this.state.isEnded && (
<div className="livestream_player_header_user_spectators">
<antd.Tag
icon={<Icons.FiEye />}
>
{this.state.spectators}
</antd.Tag>
</div>
)}
</div>
</div>
<UserPreview user={this.state.stream.user} /> {this.state.stream.info && (
<div className="livestream_player_header_info">
<div className="livestream_player_header_info_title">
<h1>
{this.state.stream.info?.title}
</h1>
</div>
<div className="livestream_player_header_info_description">
<Marquee mode="smooth">
{({ index }) => {
return (
<h4>
{
this.state
.stream.info
?.description
}
</h4>
)
}}
</Marquee>
</div>
</div>
)}
</>
) : (
<antd.Skeleton active />
)}
</div>
<div className="livestream_player_indicators"> <video
{ ref={this.videoPlayerRef}
!this.state.isEnded && <div className="livestream_player_header_user_spectators"> id="player"
<antd.Tag style={{
icon={<Icons.FiEye />} display: this.state.isEnded ? "none" : "block",
> }}
{this.state.spectators} />
</antd.Tag>
</div>
}
</div>
</div>
{ {this.state.isEnded && (
this.state.stream.info && <div className="livestream_player_header_info"> <antd.Result>
<div className="livestream_player_header_info_title"> <h1>This stream is ended</h1>
<h1>{this.state.stream.info?.title}</h1> </antd.Result>
</div> )}
<div className="livestream_player_header_info_description">
<Marquee
mode="smooth"
>
{({ index }) => {
return <h4>{this.state.stream.info?.description}</h4>
}}
</Marquee>
</div>
</div>
}
</>
: <antd.Skeleton active />
}
</div>
<video <div
ref={this.videoPlayerRef} className={classnames("livestream_player_loading", {
id="player" ["active"]: this.state.loading,
style={{ })}
display: this.state.isEnded ? "none" : "block", >
}} <antd.Spin />
/> </div>
</div>
{ <div className="livestream_panel">
this.state.isEnded && <antd.Result> <div className="chatbox">
<h1> {!this.state.cinemaMode && (
This stream is ended <div className="chatbox_header">
</h1> <h4>
</antd.Result> <Icons.FiMessageCircle /> Live chat
} </h4>
</div>
<div )}
className={classnames( <LiveChat
"livestream_player_loading", id={`livestream:${this.props.params.id}`}
{ floatingMode={this.state.cinemaMode}
["active"]: this.state.loading, />
} </div>
)} </div>
> </div>
<antd.Spin /> )
</div> }
</div>
<div className="livestream_panel">
<div className="chatbox">
{
!this.state.cinemaMode && <div className="chatbox_header">
<h4><Icons.FiMessageCircle /> Live chat</h4>
</div>
}
<LiveChat
id={`livestream:${this.props.params.id}`}
floatingMode={this.state.cinemaMode}
/>
</div>
</div>
</div>
}
} }