From eeb84add9e2a4ed7140ed253d36c229385a3c777 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 8 Apr 2025 15:16:53 +0000 Subject: [PATCH] support for spectrum 6 --- .../tv/components/ProfileData/index.jsx | 578 +++++++++--------- packages/app/src/pages/tv/live/[id].jsx | 55 +- 2 files changed, 340 insertions(+), 293 deletions(-) diff --git a/packages/app/src/pages/studio/tv/components/ProfileData/index.jsx b/packages/app/src/pages/studio/tv/components/ProfileData/index.jsx index 07112a99..63a5d4aa 100644 --- a/packages/app/src/pages/studio/tv/components/ProfileData/index.jsx +++ b/packages/app/src/pages/studio/tv/components/ProfileData/index.jsx @@ -15,334 +15,346 @@ import { FiLink } from "react-icons/fi" import "./index.less" const ProfileData = (props) => { - if (!props.profile_id) { - return null - } + if (!props.profile_id) { + return null + } - const [loading, setLoading] = React.useState(false) - const [fetching, setFetching] = React.useState(true) - const [error, setError] = React.useState(null) - const [profile, setProfile] = React.useState(null) + const [loading, setLoading] = React.useState(false) + const [fetching, setFetching] = React.useState(true) + const [error, setError] = React.useState(null) + const [profile, setProfile] = React.useState(null) - async function fetchData(profile_id) { - setFetching(true) + async function fetchData(profile_id) { + setFetching(true) - const result = await Streaming.getProfile({ profile_id }).catch((error) => { - console.error(error) - setError(error) - return null - }) + const result = await Streaming.getProfile({ profile_id }).catch( + (error) => { + console.error(error) + setError(error) + return null + }, + ) - if (result) { - setProfile(result) - } + if (result) { + setProfile(result) + } - setFetching(false) - } + setFetching(false) + } - async function handleChange(key, value) { - setLoading(true) + async function handleChange(key, value) { + setLoading(true) - const result = await Streaming.createOrUpdateStream({ - [key]: value, - _id: profile._id, - }).catch((error) => { - console.error(error) - antd.message.error("Failed to update") - return false - }) + const result = await Streaming.createOrUpdateStream({ + [key]: value, + _id: profile._id, + }).catch((error) => { + console.error(error) + antd.message.error("Failed to update") + return false + }) - if (result) { - antd.message.success("Updated") - setProfile(result) - } + if (result) { + antd.message.success("Updated") + setProfile(result) + } - setLoading(false) - } + setLoading(false) + } - async function handleDelete() { - setLoading(true) + async function handleDelete() { + setLoading(true) - const result = await Streaming.deleteProfile({ profile_id: profile._id }).catch((error) => { - console.error(error) - antd.message.error("Failed to delete") - return false - }) + const result = await Streaming.deleteProfile({ + profile_id: profile._id, + }).catch((error) => { + console.error(error) + antd.message.error("Failed to delete") + return false + }) - if (result) { - antd.message.success("Deleted") - app.eventBus.emit("app:profile_deleted", profile._id) - } + if (result) { + antd.message.success("Deleted") + app.eventBus.emit("app:profile_deleted", profile._id) + } - setLoading(false) - } + setLoading(false) + } - async function handleEditName() { - app.layout.modal.open("name_editor", ProfileCreator, { - props: { - editValue: profile.profile_name, - onEdit: async (value) => { - await handleChange("profile_name", value) - app.eventBus.emit("app:profiles_updated", profile._id) - }, - } - }) - } + async function handleEditName() { + app.layout.modal.open("name_editor", ProfileCreator, { + props: { + editValue: profile.profile_name, + onEdit: async (value) => { + await handleChange("profile_name", value) + app.eventBus.emit("app:profiles_updated", profile._id) + }, + }, + }) + } - React.useEffect(() => { - fetchData(props.profile_id) - }, [props.profile_id]) + React.useEffect(() => { + fetchData(props.profile_id) + }, [props.profile_id]) - if (error) { - return fetchData(props.profile_id)} - > - Retry - - ]} - /> - } + if (error) { + return ( + fetchData(props.profile_id)} + > + Retry + , + ]} + /> + ) + } - if (fetching) { - return - } + if (fetching) { + return + } - return
-
- -
- { - return handleChange("title", newValue) - }} - disabled={loading} - /> - { - return handleChange("description", newValue) - }} - disabled={loading} - /> -
-
+ return ( +
+
+ +
+ { + return handleChange("title", newValue) + }} + disabled={loading} + /> + { + return handleChange("description", newValue) + }} + disabled={loading} + /> +
+
-
-
- - Server -
+
+
+ + Server +
-
-
- Ingestion URL -
+
+
+ Ingestion URL +
-
- - {profile.ingestion_url} - -
-
+
+ {profile.ingestion_url} +
+
-
-
- Stream Key -
+
+
+ Stream Key +
-
- -
-
-
+
+ +
+
+
-
-
- - Configuration -
+
+
+ + Configuration +
-
-
- - Private Mode -
+
+
+ + Private Mode +
-
-

When this is enabled, only users with the livestream url can access the stream.

-
+
+

+ When this is enabled, only users with the livestream + url can access the stream. +

+
-
- handleChange("private", value)} - /> -
+
+ handleChange("private", value)} + /> +
-
-

Must restart the livestream to apply changes

-
-
+
+

+ Must restart the livestream to apply changes +

+
+
-
-
- - DVR [beta] -
+
+
+ + DVR [beta] +
-
-

Save a copy of your stream with its entire duration. You can download this copy after finishing this livestream.

-
+
+

+ Save a copy of your stream with its entire duration. + You can download this copy after finishing this + livestream. +

+
-
- -
-
-
+
+ +
+
+
- { - profile.sources &&
-
- - Media URL -
+ {profile.sources && ( +
+
+ + Media URL +
-
-
- HLS -
+
+
+ HLS +
-
-

This protocol is highly compatible with a multitude of devices and services. Recommended for general use.

-
+
+

+ This protocol is highly compatible with a + multitude of devices and services. Recommended + for general use. +

+
-
- - {profile.sources.hls} - -
-
-
-
- FLV -
+
+ {profile.sources.hls} +
+
+
+
+ RTSP [tcp] +
-
-

This protocol operates at better latency and quality than HLS, but is less compatible for most devices.

-
+
+

+ This protocol has the lowest possible latency + and the best quality. A compatible player is + required. +

+
-
- - {profile.sources.flv} - -
-
-
-
- RTSP [tcp] -
+
+ {profile.sources.rtsp} +
+
+
+
+ RTSPT [vrchat] +
-
-

This protocol has the lowest possible latency and the best quality. A compatible player is required.

-
+
+

+ This protocol has the lowest possible latency + and the best quality available. Only works for + VRChat video players. +

+
-
- - {profile.sources.rtsp} - -
-
-
-
- HTML Viewer -
+
+ + {profile.sources.rtsp.replace( + "rtsp://", + "rtspt://", + )} + +
+
+
+
+ HTML Viewer +
-
-

Share a link to easily view your stream on any device with a web browser.

-
+
+

+ Share a link to easily view your stream on any + device with a web browser. +

+
-
- - {profile.sources.html} - -
-
-
- } +
+ {profile.sources.html} +
+
+
+ )} -
-
- Other -
+
+
+ Other +
-
-
- Delete profile -
+
+
+ Delete profile +
-
- - - Delete - - -
-
+
+ + + Delete + + +
+
-
-
- Change profile name -
+
+
+ Change profile name +
-
- - Change - -
-
-
-
+
+ + Change + +
+
+
+
+ ) } -export default ProfileData \ No newline at end of file +export default ProfileData diff --git a/packages/app/src/pages/tv/live/[id].jsx b/packages/app/src/pages/tv/live/[id].jsx index a8761b9e..baa7adbd 100755 --- a/packages/app/src/pages/tv/live/[id].jsx +++ b/packages/app/src/pages/tv/live/[id].jsx @@ -50,26 +50,56 @@ const StreamDecoders = { return decoderInstance }, - hls: (player, source) => { + hls: (player, source, options = {}) => { + if (!player) { + console.error("Player is not defined") + return false + } + if (!source) { console.error("Stream source is not defined") return false } const hlsInstance = new Hls({ - autoStartLoad: true, + maxLiveSyncPlaybackRate: 1.5, + strategy: "bandwidth", + autoplay: true, + xhrSetup: (xhr) => { + if (options.authToken) { + xhr.setRequestHeader( + "Authorization", + `Bearer ${options.authToken}`, + ) + } + }, }) - hlsInstance.attachMedia(player.current) + 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) - - hlsInstance.on(Hls.Events.MANIFEST_PARSED, (event, data) => { - console.log(`${data.levels.length} quality levels found`) - }) }) + // 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) @@ -129,6 +159,8 @@ export default class StreamViewer extends React.Component { const decoderInstance = await StreamDecoders[decoder](...args) + console.log(decoderInstance) + await this.setState({ decoderInstance: decoderInstance, }) @@ -176,7 +208,7 @@ export default class StreamViewer extends React.Component { attachPlayer = () => { // check if user has interacted with the page - const player = new Plyr("#player", { + const player = new Plyr(this.videoPlayerRef.current, { clickToPlay: false, autoplay: true, muted: true, @@ -219,6 +251,8 @@ export default class StreamViewer extends React.Component { this.enterPlayerAnimation() this.attachPlayer() + console.log("custom token> ", this.props.query["token"]) + // load stream const stream = await this.loadStream(this.props.params.id) @@ -234,11 +268,12 @@ export default class StreamViewer extends React.Component { } await this.loadDecoder( - "flv", + "hls", this.videoPlayerRef.current, - stream.sources.flv, + stream.sources.hls, { onSourceEnd: this.onSourceEnd, + authToken: this.props.query["token"], }, ) }