support for spectrum 6

This commit is contained in:
SrGooglo 2025-04-08 15:16:53 +00:00
parent 6e38d41293
commit eeb84add9e
2 changed files with 340 additions and 293 deletions

View File

@ -27,11 +27,13 @@ const ProfileData = (props) => {
async function fetchData(profile_id) {
setFetching(true)
const result = await Streaming.getProfile({ profile_id }).catch((error) => {
const result = await Streaming.getProfile({ profile_id }).catch(
(error) => {
console.error(error)
setError(error)
return null
})
},
)
if (result) {
setProfile(result)
@ -63,7 +65,9 @@ const ProfileData = (props) => {
async function handleDelete() {
setLoading(true)
const result = await Streaming.deleteProfile({ profile_id: profile._id }).catch((error) => {
const result = await Streaming.deleteProfile({
profile_id: profile._id,
}).catch((error) => {
console.error(error)
antd.message.error("Failed to delete")
return false
@ -85,7 +89,7 @@ const ProfileData = (props) => {
await handleChange("profile_name", value)
app.eventBus.emit("app:profiles_updated", profile._id)
},
}
},
})
}
@ -94,7 +98,8 @@ const ProfileData = (props) => {
}, [props.profile_id])
if (error) {
return <antd.Result
return (
<antd.Result
status="warning"
title="Error"
subTitle={error.message}
@ -104,21 +109,19 @@ const ProfileData = (props) => {
onClick={() => fetchData(props.profile_id)}
>
Retry
</antd.Button>
</antd.Button>,
]}
/>
)
}
if (fetching) {
return <antd.Skeleton
active
/>
return <antd.Skeleton active />
}
return <div className="tvstudio-profile-data">
<div
className="tvstudio-profile-data-header"
>
return (
<div className="tvstudio-profile-data">
<div className="tvstudio-profile-data-header">
<img
className="tvstudio-profile-data-header-image"
src={profile.info?.thumbnail}
@ -129,7 +132,7 @@ const ProfileData = (props) => {
className="tvstudio-profile-data-header-title"
style={{
"--fontSize": "2rem",
"--fontWeight": "800"
"--fontWeight": "800",
}}
onSave={(newValue) => {
return handleChange("title", newValue)
@ -162,9 +165,7 @@ const ProfileData = (props) => {
</div>
<div className="key-value-field-value">
<span>
{profile.ingestion_url}
</span>
<span>{profile.ingestion_url}</span>
</div>
</div>
@ -174,9 +175,7 @@ const ProfileData = (props) => {
</div>
<div className="key-value-field-value">
<HiddenText
value={profile.stream_key}
/>
<HiddenText value={profile.stream_key} />
</div>
</div>
</div>
@ -194,7 +193,10 @@ const ProfileData = (props) => {
</div>
<div className="key-value-field-description">
<p>When this is enabled, only users with the livestream url can access the stream.</p>
<p>
When this is enabled, only users with the livestream
url can access the stream.
</p>
</div>
<div className="key-value-field-content">
@ -206,7 +208,9 @@ const ProfileData = (props) => {
</div>
<div className="key-value-field-description">
<p style={{ fontWeight: "bold" }}>Must restart the livestream to apply changes</p>
<p style={{ fontWeight: "bold" }}>
Must restart the livestream to apply changes
</p>
</div>
</div>
@ -217,20 +221,21 @@ const ProfileData = (props) => {
</div>
<div className="key-value-field-description">
<p>Save a copy of your stream with its entire duration. You can download this copy after finishing this livestream.</p>
<p>
Save a copy of your stream with its entire duration.
You can download this copy after finishing this
livestream.
</p>
</div>
<div className="key-value-field-content">
<antd.Switch
disabled
loading={loading}
/>
<antd.Switch disabled loading={loading} />
</div>
</div>
</div>
{
profile.sources && <div className="tvstudio-profile-data-field">
{profile.sources && (
<div className="tvstudio-profile-data-field">
<div className="tvstudio-profile-data-field-header">
<FiLink />
<span>Media URL</span>
@ -242,28 +247,15 @@ const ProfileData = (props) => {
</div>
<div className="key-value-field-description">
<p>This protocol is highly compatible with a multitude of devices and services. Recommended for general use.</p>
<p>
This protocol is highly compatible with a
multitude of devices and services. Recommended
for general use.
</p>
</div>
<div className="key-value-field-value">
<span>
{profile.sources.hls}
</span>
</div>
</div>
<div className="key-value-field">
<div className="key-value-field-key">
<span>FLV</span>
</div>
<div className="key-value-field-description">
<p>This protocol operates at better latency and quality than HLS, but is less compatible for most devices.</p>
</div>
<div className="key-value-field-value">
<span>
{profile.sources.flv}
</span>
<span>{profile.sources.hls}</span>
</div>
</div>
<div className="key-value-field">
@ -272,12 +264,36 @@ const ProfileData = (props) => {
</div>
<div className="key-value-field-description">
<p>This protocol has the lowest possible latency and the best quality. A compatible player is required.</p>
<p>
This protocol has the lowest possible latency
and the best quality. A compatible player is
required.
</p>
</div>
<div className="key-value-field-value">
<span>{profile.sources.rtsp}</span>
</div>
</div>
<div className="key-value-field">
<div className="key-value-field-key">
<span>RTSPT [vrchat]</span>
</div>
<div className="key-value-field-description">
<p>
This protocol has the lowest possible latency
and the best quality available. Only works for
VRChat video players.
</p>
</div>
<div className="key-value-field-value">
<span>
{profile.sources.rtsp}
{profile.sources.rtsp.replace(
"rtsp://",
"rtspt://",
)}
</span>
</div>
</div>
@ -287,17 +303,18 @@ const ProfileData = (props) => {
</div>
<div className="key-value-field-description">
<p>Share a link to easily view your stream on any device with a web browser.</p>
<p>
Share a link to easily view your stream on any
device with a web browser.
</p>
</div>
<div className="key-value-field-value">
<span>
{profile.sources.html}
</span>
<span>{profile.sources.html}</span>
</div>
</div>
</div>
}
)}
<div className="tvstudio-profile-data-field">
<div className="tvstudio-profile-data-field-header">
@ -317,10 +334,7 @@ const ProfileData = (props) => {
okText="Yes"
cancelText="No"
>
<antd.Button
danger
loading={loading}
>
<antd.Button danger loading={loading}>
Delete
</antd.Button>
</antd.Popconfirm>
@ -333,16 +347,14 @@ const ProfileData = (props) => {
</div>
<div className="key-value-field-content">
<antd.Button
loading={loading}
onClick={handleEditName}
>
<antd.Button loading={loading} onClick={handleEditName}>
Change
</antd.Button>
</div>
</div>
</div>
</div>
)
}
export default ProfileData

View File

@ -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)
})
// 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"],
},
)
}