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"
const EventsHandlers = {
"playback": () => {
playback: (state) => {
if (state.live) {
return false
}
return app.cores.player.playback.toggle()
},
"previous": () => {
previous: () => {
return app.cores.player.playback.previous()
},
"next": () => {
next: () => {
return app.cores.player.playback.next()
},
"volume": (ctx, value) => {
volume: (ctx, value) => {
return app.cores.player.controls.volume(value)
},
"mute": () => {
mute: () => {
return app.cores.player.controls.mute("toggle")
},
"like": async (ctx) => {
like: async (ctx) => {
if (!ctx.track_manifest) {
return false
}
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 <div
className={
props.className ?? "player-controls"
}
>
<AudioPlayerChangeModeButton
disabled={playerState.control_locked}
/>
return (
<div className={props.className ?? "player-controls"}>
<AudioPlayerChangeModeButton disabled={props.streamMode} />
<antd.Button
type="ghost"
shape="round"
icon={<Icons.FiChevronLeft />}
onClick={() => handleAction("previous")}
disabled={playerState.control_locked}
disabled={props.streamMode}
/>
<antd.Button
type="primary"
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")}
className="playButton"
disabled={playerState.control_locked}
>
{
playerState.loading && <div className="loadCircle">
{playerState.loading && (
<div className="loadCircle">
<UseAnimations
animation={LoadingAnimation}
size="100%"
/>
</div>
}
)}
</antd.Button>
<antd.Button
type="ghost"
shape="round"
icon={<Icons.FiChevronRight />}
onClick={() => handleAction("next")}
disabled={playerState.control_locked}
disabled={props.streamMode}
/>
{
!app.isMobile && <antd.Popover
content={React.createElement(
AudioVolume,
{
{!app.isMobile && (
<antd.Popover
content={React.createElement(AudioVolume, {
onChange: (value) => handleAction("volume", value),
defaultValue: playerState.volume
}
)}
defaultValue: playerState.volume,
})}
trigger="hover"
>
<button
className="muteButton"
onClick={() => handleAction("mute")}
>
{
playerState.muted
? <Icons.FiVolumeX />
: <Icons.FiVolume2 />
}
{playerState.muted ? (
<Icons.FiVolumeX />
) : (
<Icons.FiVolume2 />
)}
</button>
</antd.Popover>
}
)}
{
app.isMobile && <LikeButton
liked={playerState.track_manifest?.serviceOperations.fetchLikeStatus}
{app.isMobile && (
<LikeButton
liked={
playerState.track_manifest?.serviceOperations
.fetchLikeStatus
}
onClick={() => handleAction("like")}
/>
}
)}
</div>
)
}
export default Controls

View File

@ -17,30 +17,36 @@ const ExtraActions = (props) => {
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">
{
app.isMobile && <Button
return (
<div className="extra_actions">
{app.isMobile && (
<Button
type="ghost"
icon={<Icons.MdAbc />}
disabled={!playerState.track_manifest?.lyrics_enabled}
/>
}
)}
{
!app.isMobile && playerState.track_manifest?._id && <LikeButton
liked={playerState.track_manifest?.serviceOperations.fetchLikeStatus}
{!app.isMobile && (
<LikeButton
liked={
playerState.track_manifest?.serviceOperations
.fetchLikeStatus
}
onClick={handleClickLike}
disabled={!playerState.track_manifest?._id}
/>
}
)}
<Button
type="ghost"
icon={<Icons.MdQueueMusic />}
/>
<Button type="ghost" icon={<Icons.MdQueueMusic />} />
</div>
)
}
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) => {
// get current audio duration
const audioDuration = preCalculatedDuration ?? app.cores.player.controls.duration()
const audioDuration =
preCalculatedDuration ?? app.cores.player.controls.duration()
if (isNaN(audioDuration)) {
return
@ -40,7 +41,7 @@ export default class SeekBar extends React.Component {
// set duration
this.setState({
durationText: seekToTimeLabel(audioDuration)
durationText: seekToTimeLabel(audioDuration),
})
}
@ -50,7 +51,7 @@ export default class SeekBar extends React.Component {
// set time
this.setState({
timeText: seekToTimeLabel(seek)
timeText: seekToTimeLabel(seek),
})
}
@ -65,7 +66,7 @@ export default class SeekBar extends React.Component {
const percent = (seek / duration) * 100
this.setState({
sliderTime: percent
sliderTime: percent,
})
}
@ -112,7 +113,9 @@ export default class SeekBar extends React.Component {
sliderTime: 0,
})
this.calculateDuration(manifest.metadata?.duration ?? manifest.duration)
this.calculateDuration(
manifest.metadata?.duration ?? manifest.duration,
)
},
"player.seeked": (seek) => {
this.calculateTime()
@ -158,38 +161,37 @@ export default class SeekBar extends React.Component {
}
render() {
return <div
className={classnames(
"player-seek_bar",
{
return (
<div
className={classnames("player-seek_bar", {
["stopped"]: this.props.stopped,
}
)}
})}
>
<div
className={classnames(
"progress",
{
className={classnames("progress", {
["hidden"]: this.props.streamMode,
}
)}
})}
>
<Slider
size="small"
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}
max={100}
step={0.1}
onChange={(_, value) => {
this.setState({
sliderTime: value,
sliderLock: true
sliderLock: true,
})
}}
onChangeCommitted={() => {
this.setState({
sliderLock: false
sliderLock: false,
})
this.handleSeek(this.state.sliderTime)
@ -200,20 +202,24 @@ export default class SeekBar extends React.Component {
}}
valueLabelDisplay="auto"
valueLabelFormat={(value) => {
return seekToTimeLabel((value / 100) * app.cores.player.controls.duration())
return seekToTimeLabel(
(value / 100) *
app.cores.player.controls.duration(),
)
}}
/>
</div>
{!this.props.streamMode && (
<div className="timers">
<div>
<span>{this.state.timeText}</span>
</div>
<div>
{
this.props.streamMode ? <antd.Tag>Live</antd.Tag> : <span>{this.state.durationText}</span>
}
<span>{this.state.durationText}</span>
</div>
</div>
)}
</div>
)
}
}

View File

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