support for spectrum 6.1

This commit is contained in:
SrGooglo 2025-04-09 20:48:52 +00:00
parent 710e67c481
commit b82f495ee7
5 changed files with 228 additions and 187 deletions

View File

@ -1,4 +1,5 @@
import React from "react" import React from "react"
import Plyr from "plyr"
import * as antd from "antd" import * as antd from "antd"
import Marquee from "react-fast-marquee" import Marquee from "react-fast-marquee"
import classnames from "classnames" import classnames from "classnames"
@ -9,121 +10,36 @@ import { Icons } from "@components/Icons"
import LiveChat from "@components/LiveChat" import LiveChat from "@components/LiveChat"
import SpectrumModel from "@models/spectrum" import SpectrumModel from "@models/spectrum"
import Plyr from "plyr" import * as Decoders from "./decoders"
import Hls from "hls.js"
import mpegts from "mpegts.js"
import "plyr/dist/plyr.css" import "plyr/dist/plyr.css"
import "./index.less" import "./index.less"
const DecodersEvents = { async function fetchStream(stream_id) {
[Hls.Events.FPS_DROP]: (event, data) => { let stream = await SpectrumModel.getStream(stream_id).catch((error) => {
console.log("FPS_DROP Detected", data)
},
}
const StreamDecoders = {
flv: async (player, source, { onSourceEnd } = {}) => {
if (!source) {
console.error("Stream source is not defined")
return false
}
const decoderInstance = mpegts.createPlayer({
type: "flv",
isLive: true,
enableWorker: true,
url: source,
})
if (typeof onSourceEnd === "function") {
decoderInstance.on(mpegts.Events.ERROR, onSourceEnd)
}
decoderInstance.attachMediaElement(player)
decoderInstance.load()
await decoderInstance.play().catch((error) => {
console.error(error) console.error(error)
return null
}) })
return decoderInstance if (!stream) {
},
hls: (player, source, options = {}) => {
if (!player) {
console.error("Player is not defined")
return false return false
} }
if (!source) { if (Array.isArray(stream)) {
console.error("Stream source is not defined") stream = stream[0]
}
if (!stream.sources) {
return false return false
} }
const hlsInstance = new Hls({ return stream
maxLiveSyncPlaybackRate: 1.5,
strategy: "bandwidth",
autoplay: true,
xhrSetup: (xhr) => {
if (options.authToken) {
xhr.setRequestHeader(
"Authorization",
`Bearer ${options.authToken}`,
)
}
},
})
if (options.authToken) {
source += `?token=${options.authToken}`
}
console.log("Loading media hls >", source, options)
hlsInstance.attachMedia(player)
// when media attached, load source
hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
hlsInstance.loadSource(source)
})
// process quality and tracks levels
hlsInstance.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
console.log(`${data.levels.length} quality levels found`)
})
// resume to the last position when player resume playback
player.addEventListener("play", () => {
console.log("Syncing to last position")
player.currentTime = hlsInstance.liveSyncPosition
})
// handle errors
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(DecodersEvents).forEach((event) => {
hlsInstance.on(event, DecodersEvents[event])
})
return hlsInstance
},
} }
export default class StreamViewer extends React.Component { export default class StreamViewer extends React.Component {
static defaultDecoder = "hls"
static stateSyncMs = 1 * 60 * 1000 // 1 minute
state = { state = {
isEnded: false, isEnded: false,
loading: true, loading: true,
@ -139,8 +55,8 @@ export default class StreamViewer extends React.Component {
videoPlayerRef = React.createRef() videoPlayerRef = React.createRef()
loadDecoder = async (decoder, ...args) => { loadDecoder = async (decoder, ...args) => {
if (typeof StreamDecoders[decoder] === "undefined") { if (typeof Decoders[decoder] === "undefined") {
console.error("Protocol not supported") console.error("[TV] Protocol not supported")
return false return false
} }
@ -155,11 +71,9 @@ export default class StreamViewer extends React.Component {
this.setState({ decoderInstance: null }) this.setState({ decoderInstance: null })
} }
console.log(`Switching decoder to: ${decoder}`) console.log(`[TV] Switching decoder to: ${decoder}`)
const decoderInstance = await StreamDecoders[decoder](...args) const decoderInstance = await Decoders[decoder](...args)
console.log(decoderInstance)
await this.setState({ await this.setState({
decoderInstance: decoderInstance, decoderInstance: decoderInstance,
@ -170,30 +84,6 @@ export default class StreamViewer extends React.Component {
return decoderInstance return decoderInstance
} }
loadStream = async (stream_id) => {
let stream = await SpectrumModel.getStream(stream_id).catch((error) => {
console.error(error)
return null
})
if (!stream) {
return false
}
if (Array.isArray(stream)) {
stream = stream[0]
}
console.log("Stream data >", stream)
this.setState({
stream: stream,
spectators: stream.viewers,
})
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()
@ -247,53 +137,48 @@ export default class StreamViewer extends React.Component {
}) })
} }
componentDidMount = async () => { joinStreamWebsocket = async (stream) => {
this.enterPlayerAnimation()
this.attachPlayer()
console.log("custom token> ", this.props.query["token"])
// load stream
const stream = await this.loadStream(this.props.params.id)
if (!stream) { if (!stream) {
return this.onSourceEnd() console.error(
} `[TV] Cannot connect to stream websocket if no stream provided`,
// load the flv decoder (by default)
if (stream) {
if (!stream.sources) {
console.error("Stream sources not found")
return
}
await this.loadDecoder(
"hls",
this.videoPlayerRef.current,
stream.sources.hls,
{
onSourceEnd: this.onSourceEnd,
authToken: this.props.query["token"],
},
) )
} return false
} }
componentWillUnmount = () => { const client = await SpectrumModel.createStreamWebsocket(stream._id, {
if (typeof this.state.decoderInstance?.unload === "function") { maxConnectRetries: 3,
this.state.decoderInstance.unload() refName: "/",
})
this.setState({
websocket: client,
})
await client.connect()
this.streamStateInterval = setInterval(() => {
this.syncWithStreamState()
}, StreamViewer.stateSyncMs)
setTimeout(this.syncWithStreamState, 1000)
return client
} }
if (typeof this.state.decoderInstance?.destroy === "function") { syncWithStreamState = async () => {
this.state.decoderInstance.destroy() if (!this.state.websocket || !this.state.stream) {
return false
} }
this.exitPlayerAnimation() const state = await this.state.websocket.requestState()
this.toggleCinemaMode(false)
if (this.streamInfoInterval) { return this.setState({
clearInterval(this.streamInfoInterval) spectators: state.viewers,
} stream: {
...this.state.stream,
...(state.profile ?? {}),
},
})
} }
enterPlayerAnimation = () => { enterPlayerAnimation = () => {
@ -314,19 +199,7 @@ export default class StreamViewer extends React.Component {
} }
} }
updateQuality = (newQuality) => { setStreamLevel = (level) => {}
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
}
})
}
toggleLoading = (to) => { toggleLoading = (to) => {
this.setState({ loading: to ?? !this.state.loading }) this.setState({ loading: to ?? !this.state.loading })
@ -343,6 +216,64 @@ export default class StreamViewer extends React.Component {
this.setState({ cinemaMode: to }) this.setState({ cinemaMode: to })
} }
componentDidMount = async () => {
this.enterPlayerAnimation()
this.attachPlayer()
// fetch stream data
const stream = await fetchStream(this.props.params.id)
// and error occurred or no stream available/online
if (!stream) {
return this.onSourceEnd()
}
console.log(`[TV] Stream data >`, stream)
// set data
this.setState({
stream: stream,
spectators: stream.viewers,
})
// joinStreamWebsocket
await this.joinStreamWebsocket(stream)
// load decoder with provided data
await this.loadDecoder(
StreamViewer.defaultDecoder,
this.videoPlayerRef.current,
stream.sources,
{
onSourceEnd: this.onSourceEnd,
authToken: this.props.query["token"],
},
)
}
componentWillUnmount = () => {
if (typeof this.state.decoderInstance?.unload === "function") {
this.state.decoderInstance.unload()
}
if (typeof this.state.decoderInstance?.destroy === "function") {
this.state.decoderInstance.destroy()
}
if (this.state.websocket) {
if (typeof this.state.websocket.destroy === "function") {
this.state.websocket.destroy()
}
}
this.exitPlayerAnimation()
this.toggleCinemaMode(false)
if (this.streamStateInterval) {
clearInterval(this.streamStateInterval)
}
}
render() { render() {
return ( return (
<div <div

View File

@ -0,0 +1,29 @@
import mpegts from "mpegts.js"
export default async (player, source, { onSourceEnd } = {}) => {
if (!source) {
console.error("Stream source is not defined")
return false
}
const decoderInstance = mpegts.createPlayer({
type: "flv",
isLive: true,
enableWorker: true,
url: source,
})
if (typeof onSourceEnd === "function") {
decoderInstance.on(mpegts.Events.ERROR, onSourceEnd)
}
decoderInstance.attachMediaElement(player)
decoderInstance.load()
await decoderInstance.play().catch((error) => {
console.error(error)
})
return decoderInstance
}

View File

@ -0,0 +1,79 @@
import Hls from "hls.js"
const Events = {
[Hls.Events.FPS_DROP]: (event, data) => {
console.warn("[HLS] FPS_DROP Detected", data)
},
[Hls.Events.ERROR]: (event, data) => {
console.error("[HLS] Error", data)
},
}
export default (player, sources = {}, options = {}) => {
if (!player) {
console.error("[HLS] player is not defined")
return false
}
if (!sources.hls) {
console.error("[HLS] an hls source is not provided")
return false
}
let source = sources.hls
const hlsInstance = new Hls({
maxLiveSyncPlaybackRate: 1.5,
strategy: "bandwidth",
autoplay: true,
xhrSetup: (xhr) => {
if (options.authToken) {
xhr.setRequestHeader(
"Authorization",
`Bearer ${options.authToken}`,
)
}
},
})
if (options.authToken) {
source += `?token=${options.authToken}`
}
console.log("[HLS] Instance options >", options)
console.log(`[HLS] Loading source [${source}]`)
hlsInstance.attachMedia(player)
// when media attached, load source
hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
hlsInstance.loadSource(source)
})
// handle when media ends
hlsInstance.on(Hls.Events.BUFFER_EOS, () => {
console.log("[HLS] Media ended")
if (typeof options.onSourceEnd === "function") {
options.onSourceEnd()
}
})
// process quality and tracks levels
hlsInstance.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
console.log("[HLS] Manifest parsed >", data)
})
// resume to the last position when player resume playback
player.addEventListener("play", () => {
console.log("[HLS] Syncing to last position")
player.currentTime = hlsInstance.liveSyncPosition
})
// register hls decoder events
Object.keys(Events).forEach((event) => {
hlsInstance.on(event, Events[event])
})
return hlsInstance
}

View File

@ -0,0 +1,2 @@
export { default as hls } from "./hls"
export { default as flv } from "./flv"

View File

@ -88,7 +88,7 @@ const LivestreamItem = (props) => {
export default (props) => { export default (props) => {
const [L_Streams, R_Streams, E_Streams] = app.cores.api.useRequest( const [L_Streams, R_Streams, E_Streams] = app.cores.api.useRequest(
SpectrumModel.getLivestreamsList, SpectrumModel.list,
) )
useCenteredContainer(false) useCenteredContainer(false)