Support for live radios

This commit is contained in:
SrGooglo 2025-02-19 16:32:44 +00:00
parent 5f3fb3a013
commit 6eef3480b1
6 changed files with 407 additions and 322 deletions

View File

@ -14,29 +14,35 @@ import { usePlayerStateContext } from "@contexts/WithPlayerContext"
import "./index.less" import "./index.less"
const EventsHandlers = { const EventsHandlers = {
"playback": () => { playback: (state) => {
if (state.live) {
return false
}
return app.cores.player.playback.toggle() return app.cores.player.playback.toggle()
}, },
"previous": () => { previous: () => {
return app.cores.player.playback.previous() return app.cores.player.playback.previous()
}, },
"next": () => { next: () => {
return app.cores.player.playback.next() return app.cores.player.playback.next()
}, },
"volume": (ctx, value) => { volume: (ctx, value) => {
return app.cores.player.controls.volume(value) return app.cores.player.controls.volume(value)
}, },
"mute": () => { mute: () => {
return app.cores.player.controls.mute("toggle") return app.cores.player.controls.mute("toggle")
}, },
"like": async (ctx) => { like: async (ctx) => {
if (!ctx.track_manifest) { if (!ctx.track_manifest) {
return false return false
} }
const track = app.cores.player.track() const track = app.cores.player.track()
return await track.manifest.serviceOperations.toggleItemFavourite("track", ctx.track_manifest._id) return await track.manifest.serviceOperations.toggleItemFavourite(
"track",
ctx.track_manifest._id,
)
}, },
} }
@ -51,76 +57,79 @@ const Controls = (props) => {
return EventsHandlers[event](playerState, ...args) return EventsHandlers[event](playerState, ...args)
} }
return <div return (
className={ <div className={props.className ?? "player-controls"}>
props.className ?? "player-controls" <AudioPlayerChangeModeButton disabled={props.streamMode} />
}
>
<AudioPlayerChangeModeButton
disabled={playerState.control_locked}
/>
<antd.Button <antd.Button
type="ghost" type="ghost"
shape="round" shape="round"
icon={<Icons.FiChevronLeft />} icon={<Icons.FiChevronLeft />}
onClick={() => handleAction("previous")} onClick={() => handleAction("previous")}
disabled={playerState.control_locked} disabled={props.streamMode}
/> />
<antd.Button <antd.Button
type="primary" type="primary"
shape="circle" shape="circle"
icon={playerState.livestream_mode ? <Icons.MdStop /> : playerState.playback_status === "playing" ? <Icons.MdPause /> : <Icons.MdPlayArrow />} icon={
props.streamMode ? (
<Icons.MdStop />
) : playerState.playback_status === "playing" ? (
<Icons.MdPause />
) : (
<Icons.MdPlayArrow />
)
}
onClick={() => handleAction("playback")} onClick={() => handleAction("playback")}
className="playButton" className="playButton"
disabled={playerState.control_locked}
> >
{ {playerState.loading && (
playerState.loading && <div className="loadCircle"> <div className="loadCircle">
<UseAnimations <UseAnimations
animation={LoadingAnimation} animation={LoadingAnimation}
size="100%" size="100%"
/> />
</div> </div>
} )}
</antd.Button> </antd.Button>
<antd.Button <antd.Button
type="ghost" type="ghost"
shape="round" shape="round"
icon={<Icons.FiChevronRight />} icon={<Icons.FiChevronRight />}
onClick={() => handleAction("next")} onClick={() => handleAction("next")}
disabled={playerState.control_locked} disabled={props.streamMode}
/> />
{ {!app.isMobile && (
!app.isMobile && <antd.Popover <antd.Popover
content={React.createElement( content={React.createElement(AudioVolume, {
AudioVolume,
{
onChange: (value) => handleAction("volume", value), onChange: (value) => handleAction("volume", value),
defaultValue: playerState.volume defaultValue: playerState.volume,
} })}
)}
trigger="hover" trigger="hover"
> >
<button <button
className="muteButton" className="muteButton"
onClick={() => handleAction("mute")} onClick={() => handleAction("mute")}
> >
{ {playerState.muted ? (
playerState.muted <Icons.FiVolumeX />
? <Icons.FiVolumeX /> ) : (
: <Icons.FiVolume2 /> <Icons.FiVolume2 />
} )}
</button> </button>
</antd.Popover> </antd.Popover>
} )}
{ {app.isMobile && (
app.isMobile && <LikeButton <LikeButton
liked={playerState.track_manifest?.serviceOperations.fetchLikeStatus} liked={
playerState.track_manifest?.serviceOperations
.fetchLikeStatus
}
onClick={() => handleAction("like")} onClick={() => handleAction("like")}
/> />
} )}
</div> </div>
)
} }
export default Controls export default Controls

View File

@ -17,30 +17,36 @@ const ExtraActions = (props) => {
const track = app.cores.player.track() const track = app.cores.player.track()
await track.manifest.serviceOperations.toggleItemFavourite("track", playerState.track_manifest._id) await track.manifest.serviceOperations.toggleItemFavourite(
"track",
playerState.track_manifest._id,
)
} }
return <div className="extra_actions"> return (
{ <div className="extra_actions">
app.isMobile && <Button {app.isMobile && (
<Button
type="ghost" type="ghost"
icon={<Icons.MdAbc />} icon={<Icons.MdAbc />}
disabled={!playerState.track_manifest?.lyrics_enabled} disabled={!playerState.track_manifest?.lyrics_enabled}
/> />
} )}
{ {!app.isMobile && (
!app.isMobile && playerState.track_manifest?._id && <LikeButton <LikeButton
liked={playerState.track_manifest?.serviceOperations.fetchLikeStatus} liked={
playerState.track_manifest?.serviceOperations
.fetchLikeStatus
}
onClick={handleClickLike} onClick={handleClickLike}
disabled={!playerState.track_manifest?._id}
/> />
} )}
<Button <Button type="ghost" icon={<Icons.MdQueueMusic />} />
type="ghost"
icon={<Icons.MdQueueMusic />}
/>
</div> </div>
)
} }
export default ExtraActions export default ExtraActions

View File

@ -0,0 +1,51 @@
import React from "react"
import SSEEvents from "@classes/SSEEvents"
import { MdPlayCircle, MdHeadphones } from "react-icons/md"
import "./index.less"
const LiveInfo = ({ radioId, initialData }) => {
const [data, setData] = React.useState(initialData ?? {})
const eventManager = React.useMemo(
() =>
new SSEEvents(
`${app.cores.api.client().mainOrigin}/music/radio/sse/radio:${radioId}`,
{
update: (data) => {
if (typeof data.now_playing === "string") {
data.now_playing = JSON.parse(data.now_playing)
}
console.log(`Radio data updated`, data)
setData(data)
},
},
),
[],
)
React.useEffect(() => {
return () => {
eventManager.close()
}
}, [])
return (
<div className="live-info">
{data.now_playing && (
<>
<div className="live-info-title">
<MdPlayCircle /> {data.now_playing.song.text}
</div>
<div className="live-info-listeners">
<MdHeadphones /> {data.listeners}
</div>
</>
)}
</div>
)
}
export default LiveInfo

View File

@ -0,0 +1,8 @@
.live-info {
display: flex;
flex-direction: column;
gap: 7px;
height: 100%;
}

View File

@ -32,7 +32,8 @@ export default class SeekBar extends React.Component {
calculateDuration = (preCalculatedDuration) => { calculateDuration = (preCalculatedDuration) => {
// get current audio duration // get current audio duration
const audioDuration = preCalculatedDuration ?? app.cores.player.controls.duration() const audioDuration =
preCalculatedDuration ?? app.cores.player.controls.duration()
if (isNaN(audioDuration)) { if (isNaN(audioDuration)) {
return return
@ -40,7 +41,7 @@ export default class SeekBar extends React.Component {
// set duration // set duration
this.setState({ this.setState({
durationText: seekToTimeLabel(audioDuration) durationText: seekToTimeLabel(audioDuration),
}) })
} }
@ -50,7 +51,7 @@ export default class SeekBar extends React.Component {
// set time // set time
this.setState({ this.setState({
timeText: seekToTimeLabel(seek) timeText: seekToTimeLabel(seek),
}) })
} }
@ -65,7 +66,7 @@ export default class SeekBar extends React.Component {
const percent = (seek / duration) * 100 const percent = (seek / duration) * 100
this.setState({ this.setState({
sliderTime: percent sliderTime: percent,
}) })
} }
@ -112,7 +113,9 @@ export default class SeekBar extends React.Component {
sliderTime: 0, sliderTime: 0,
}) })
this.calculateDuration(manifest.metadata?.duration ?? manifest.duration) this.calculateDuration(
manifest.metadata?.duration ?? manifest.duration,
)
}, },
"player.seeked": (seek) => { "player.seeked": (seek) => {
this.calculateTime() this.calculateTime()
@ -158,38 +161,37 @@ export default class SeekBar extends React.Component {
} }
render() { render() {
return <div return (
className={classnames( <div
"player-seek_bar", className={classnames("player-seek_bar", {
{
["stopped"]: this.props.stopped, ["stopped"]: this.props.stopped,
} })}
)}
> >
<div <div
className={classnames( className={classnames("progress", {
"progress",
{
["hidden"]: this.props.streamMode, ["hidden"]: this.props.streamMode,
} })}
)}
> >
<Slider <Slider
size="small" size="small"
value={this.state.sliderTime} value={this.state.sliderTime}
disabled={this.props.stopped || this.props.streamMode || this.props.disabled} disabled={
this.props.stopped ||
this.props.streamMode ||
this.props.disabled
}
min={0} min={0}
max={100} max={100}
step={0.1} step={0.1}
onChange={(_, value) => { onChange={(_, value) => {
this.setState({ this.setState({
sliderTime: value, sliderTime: value,
sliderLock: true sliderLock: true,
}) })
}} }}
onChangeCommitted={() => { onChangeCommitted={() => {
this.setState({ this.setState({
sliderLock: false sliderLock: false,
}) })
this.handleSeek(this.state.sliderTime) this.handleSeek(this.state.sliderTime)
@ -200,20 +202,24 @@ export default class SeekBar extends React.Component {
}} }}
valueLabelDisplay="auto" valueLabelDisplay="auto"
valueLabelFormat={(value) => { valueLabelFormat={(value) => {
return seekToTimeLabel((value / 100) * app.cores.player.controls.duration()) return seekToTimeLabel(
(value / 100) *
app.cores.player.controls.duration(),
)
}} }}
/> />
</div> </div>
{!this.props.streamMode && (
<div className="timers"> <div className="timers">
<div> <div>
<span>{this.state.timeText}</span> <span>{this.state.timeText}</span>
</div> </div>
<div> <div>
{ <span>{this.state.durationText}</span>
this.props.streamMode ? <antd.Tag>Live</antd.Tag> : <span>{this.state.durationText}</span>
}
</div> </div>
</div> </div>
)}
</div> </div>
)
} }
} }

View File

@ -5,6 +5,7 @@ import classnames from "classnames"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import { usePlayerStateContext } from "@contexts/WithPlayerContext" import { usePlayerStateContext } from "@contexts/WithPlayerContext"
import LiveInfo from "@components/Player/LiveInfo"
import SeekBar from "@components/Player/SeekBar" import SeekBar from "@components/Player/SeekBar"
import Controls from "@components/Player/Controls" import Controls from "@components/Player/Controls"
@ -25,9 +26,7 @@ function isOverflown(parent, element) {
return elementRect.width > parentRect.width return elementRect.width > parentRect.width
} }
const Indicators = (props) => { const Indicators = ({ track, playerState }) => {
const { track } = props
if (!track) { if (!track) {
return null return null
} }
@ -40,6 +39,12 @@ const Indicators = (props) => {
} }
} }
if (playerState.live) {
indicators.push(
<Icons.FiRadio style={{ color: "var(--colorPrimary)" }} />,
)
}
if (indicators.length === 0) { if (indicators.length === 0) {
return null return null
} }
@ -152,12 +157,6 @@ const Player = (props) => {
onClick={() => app.location.push("/lyrics")} onClick={() => app.location.push("/lyrics")}
/> />
{/* <antd.Button
icon={<Icons.MdOfflineBolt />}
>
HyperDrive
</antd.Button> */}
<antd.Button <antd.Button
icon={<Icons.FiX />} icon={<Icons.FiX />}
shape="circle" shape="circle"
@ -206,20 +205,26 @@ const Player = (props) => {
</p> </p>
</div> </div>
{playerState.radioId && (
<LiveInfo radioId={playerState.radioId} />
)}
<div className="toolbar_player_actions"> <div className="toolbar_player_actions">
<Controls /> <Controls streamMode={playerState.live} />
<SeekBar <SeekBar
stopped={playerState.playback_status === "stopped"} stopped={playerState.playback_status === "stopped"}
playing={playerState.playback_status === "playing"} playing={playerState.playback_status === "playing"}
streamMode={playerState.livestream_mode} streamMode={playerState.live}
disabled={playerState.control_locked}
/> />
<ExtraActions /> <ExtraActions streamMode={playerState.live} />
</div> </div>
<Indicators track={playerState.track_manifest} /> <Indicators
track={playerState.track_manifest}
playerState={playerState}
/>
</div> </div>
</div> </div>
</div> </div>