mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 18:44:16 +00:00
migrate player
to playerv2
This commit is contained in:
parent
664b33cd3a
commit
aa8e0864cc
@ -377,13 +377,13 @@ export class BottomBar extends React.Component {
|
||||
</div>
|
||||
|
||||
{
|
||||
this.context.currentManifest && <div
|
||||
this.context.track_manifest && <div
|
||||
className="item"
|
||||
>
|
||||
<PlayerButton
|
||||
manifest={this.context.currentManifest}
|
||||
playback={this.context.playbackStatus}
|
||||
colorAnalysis={this.context.coverColorAnalysis}
|
||||
manifest={this.context.track_manifest}
|
||||
playback={this.context.playback_status}
|
||||
colorAnalysis={this.context.track_manifest?.cover_analysis}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export default (props) => {
|
||||
const onClickPlay = (e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
app.cores.player.startPlaylist(playlist.list)
|
||||
app.cores.player.start(playlist.list)
|
||||
}
|
||||
|
||||
return <div
|
||||
|
@ -9,14 +9,17 @@ export default (props) => {
|
||||
const { data } = props
|
||||
|
||||
const startPlaylist = () => {
|
||||
app.cores.player.startPlaylist(data.list, 0)
|
||||
app.cores.player.start(data.list, 0)
|
||||
}
|
||||
|
||||
const navigateToPlaylist = () => {
|
||||
app.location.push(`/play/${data._id}`)
|
||||
}
|
||||
|
||||
return <div className="playlistTimelineEntry">
|
||||
return <div
|
||||
className="playlistTimelineEntry"
|
||||
style={props.style}
|
||||
>
|
||||
<div className="playlistTimelineEntry_content">
|
||||
<div className="playlistTimelineEntry_thumbnail">
|
||||
<Image
|
||||
|
@ -5,7 +5,7 @@
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
|
||||
height: 100%;
|
||||
height: fit-content;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
|
@ -35,7 +35,14 @@ export default (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
app.cores.player.startPlaylist(playlist.list, index)
|
||||
// check if is currently playing
|
||||
if (app.cores.player.state.track_manifest?._id === track._id) {
|
||||
app.cores.player.playback.toggle()
|
||||
} else {
|
||||
app.cores.player.start(playlist.list, {
|
||||
startIndex: index
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleTrackLike = async (track) => {
|
||||
|
@ -14,20 +14,24 @@ import "./index.less"
|
||||
export default (props) => {
|
||||
// use react context to get the current track
|
||||
const {
|
||||
currentManifest,
|
||||
playbackStatus,
|
||||
track_manifest,
|
||||
playback_status,
|
||||
} = React.useContext(Context)
|
||||
|
||||
const isLiked = props.track?.liked
|
||||
const isCurrent = currentManifest?._id === props.track._id
|
||||
const isPlaying = isCurrent && playbackStatus === "playing"
|
||||
const isCurrent = track_manifest?._id === props.track._id
|
||||
const isPlaying = isCurrent && playback_status === "playing"
|
||||
|
||||
const handleClickPlayBtn = React.useCallback(() => {
|
||||
if (typeof props.onClickPlayBtn === "function") {
|
||||
props.onClickPlayBtn(props.track)
|
||||
} else {
|
||||
console.warn("Searcher: onClick is not a function, using default action...")
|
||||
if (!isCurrent) {
|
||||
app.cores.player.start(props.track)
|
||||
} else {
|
||||
app.cores.player.playback.toggle()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -73,19 +73,19 @@ export class BackgroundMediaPlayer extends React.Component {
|
||||
className={classnames(
|
||||
"background_media_player",
|
||||
{
|
||||
["lightBackground"]: this.context.coverColorAnalysis?.isLight,
|
||||
["lightBackground"]: this.context.track_manifest?.cover_analysis?.isLight ?? false,
|
||||
["expanded"]: this.state.expanded,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: this.context.coverColorAnalysis?.rgba,
|
||||
"--averageColorValues": this.context.coverColorAnalysis?.rgba,
|
||||
backgroundColor: this.context.track_manifest?.cover_analysis?.rgba,
|
||||
"--averageColorValues": this.context.track_manifest?.cover_analysis?.rgba,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="background_media_player__background"
|
||||
style={{
|
||||
backgroundImage: `url(${this.context.currentManifest?.cover ?? this.context.currentManifest?.thumbnail})`
|
||||
backgroundImage: `url(${this.context.track_manifest?.cover ?? this.context.track_manifest?.thumbnail})`
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -98,12 +98,12 @@ export class BackgroundMediaPlayer extends React.Component {
|
||||
className={classnames(
|
||||
"background_media_player__icon",
|
||||
{
|
||||
["bounce"]: this.context.playbackStatus === "playing",
|
||||
["bounce"]: this.context.playback_status === "playing",
|
||||
}
|
||||
)}
|
||||
>
|
||||
{
|
||||
this.context.playbackStatus === "playing" ? <Icons.MdMusicNote /> : <Icons.MdPause />
|
||||
this.context.playback_status === "playing" ? <Icons.MdMusicNote /> : <Icons.MdPause />
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -113,14 +113,14 @@ export class BackgroundMediaPlayer extends React.Component {
|
||||
>
|
||||
{
|
||||
!this.state.expanded && <Marquee
|
||||
gradientColor={RGBStringToValues(this.state.thumbnailAnalysis?.rgb)}
|
||||
gradientColor={RGBStringToValues(this.context.track_cover_analysis?.rgb)}
|
||||
gradientWidth={20}
|
||||
play={this.context.playbackStatus !== "stopped"}
|
||||
play={this.context.playback_status !== "stopped"}
|
||||
>
|
||||
<h4>
|
||||
{
|
||||
this.context.playbackStatus === "stopped" ? "Nothing is playing" : <>
|
||||
{`${this.context.currentManifest?.title} - ${this.context.currentManifest?.artist}` ?? "Untitled"}
|
||||
this.context.playback_status === "stopped" ? "Nothing is playing" : <>
|
||||
{`${this.context.track_manifest?.title} - ${this.context.track_manifest?.artist}` ?? "Untitled"}
|
||||
</>
|
||||
}
|
||||
</h4>
|
||||
@ -131,8 +131,8 @@ export class BackgroundMediaPlayer extends React.Component {
|
||||
<Icons.MdAlbum />
|
||||
|
||||
{
|
||||
this.context.playbackStatus === "stopped" ? "Nothing is playing" : <>
|
||||
{this.context.currentManifest?.title ?? "Untitled"}
|
||||
this.context.playback_status === "stopped" ? "Nothing is playing" : <>
|
||||
{this.context.track_manifest?.title ?? "Untitled"}
|
||||
</>
|
||||
}
|
||||
</h4>
|
||||
@ -165,7 +165,7 @@ export class BackgroundMediaPlayer extends React.Component {
|
||||
size="small"
|
||||
type="ghost"
|
||||
shape="circle"
|
||||
icon={this.context.playbackStatus === "playing" ? <Icons.MdPause /> : <Icons.MdPlayArrow />}
|
||||
icon={this.context.playback_status === "playing" ? <Icons.MdPause /> : <Icons.MdPlayArrow />}
|
||||
onClick={app.cores.player.playback.toggle}
|
||||
/>
|
||||
|
||||
|
@ -36,12 +36,60 @@ const ServiceIndicator = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const MemeDancer = (props) => {
|
||||
const defaultBpm = 120
|
||||
const [currentBpm, setCurrentBpm] = React.useState(defaultBpm)
|
||||
|
||||
const videoRef = React.useRef()
|
||||
|
||||
const togglePlayback = (to) => {
|
||||
videoRef.current[to ? "play" : "pause"]()
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
app.cores.player.eventBus.on("bpm.change", (bpm) => {
|
||||
setCurrentBpm(bpm)
|
||||
})
|
||||
|
||||
app.cores.player.eventBus.on("player.state.update:playback_status", (status) => {
|
||||
if (status === "playing") {
|
||||
togglePlayback(true)
|
||||
}else {
|
||||
togglePlayback(false)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof currentBpm === "number" && isFinite(currentBpm)) {
|
||||
let playbackRate = currentBpm / 120;
|
||||
playbackRate = Math.min(4.0, Math.max(0.1, playbackRate)); // Limit the range between 0.1 and 4.0
|
||||
videoRef.current.playbackRate = playbackRate;
|
||||
}
|
||||
}, [currentBpm])
|
||||
|
||||
return <div className="meme_dancer">
|
||||
<video
|
||||
ref={videoRef}
|
||||
muted
|
||||
autoPlay
|
||||
loop
|
||||
controls={false}
|
||||
>
|
||||
<source
|
||||
src="https://media.tenor.com/-VG9cLwSYTcAAAPo/dancing-triangle-dancing.mp4"
|
||||
/>
|
||||
</video>
|
||||
</div>
|
||||
}
|
||||
|
||||
// TODO: Queue view
|
||||
export class AudioPlayer extends React.Component {
|
||||
static contextType = Context
|
||||
|
||||
state = {
|
||||
showControls: false,
|
||||
showDancer: false,
|
||||
}
|
||||
|
||||
onMouse = (event) => {
|
||||
@ -79,7 +127,7 @@ export class AudioPlayer extends React.Component {
|
||||
}
|
||||
|
||||
onClickPlayButton = () => {
|
||||
if (this.context.streamMode) {
|
||||
if (this.context.sync_mode) {
|
||||
return app.cores.player.playback.stop()
|
||||
}
|
||||
|
||||
@ -94,6 +142,10 @@ export class AudioPlayer extends React.Component {
|
||||
app.cores.player.playback.next()
|
||||
}
|
||||
|
||||
toggleDancer = () => {
|
||||
this.setState({ showDancer: !this.state.showDancer })
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
className={classnames(
|
||||
@ -116,7 +168,7 @@ export class AudioPlayer extends React.Component {
|
||||
/>
|
||||
|
||||
{
|
||||
!this.context.syncModeLocked && !this.context.syncMode && <antd.Button
|
||||
!this.context.control_locked && !this.context.sync_mode && <antd.Button
|
||||
icon={<Icons.MdShare />}
|
||||
onClick={this.inviteSync}
|
||||
shape="circle"
|
||||
@ -139,40 +191,44 @@ export class AudioPlayer extends React.Component {
|
||||
}
|
||||
|
||||
<div className="player">
|
||||
{
|
||||
this.state.showDancer && <MemeDancer />
|
||||
}
|
||||
|
||||
<ServiceIndicator
|
||||
service={this.context.currentManifest?.service}
|
||||
service={this.context.track_manifest?.service}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="cover"
|
||||
style={{
|
||||
backgroundImage: `url(${(this.context.currentManifest?.cover ?? this.context.currentManifest?.thumbnail) ?? "/assets/no_song.png"})`,
|
||||
backgroundImage: `url(${(this.context.track_manifest?.cover ?? this.context.track_manifest?.thumbnail) ?? "/assets/no_song.png"})`,
|
||||
}}
|
||||
/>
|
||||
<div className="header">
|
||||
<div className="info">
|
||||
<div className="title">
|
||||
<h2>
|
||||
<h2 onDoubleClick={this.toggleDancer}>
|
||||
{
|
||||
this.context.currentManifest?.title
|
||||
? this.context.currentManifest?.title
|
||||
: (this.context.loading ? "Loading..." : (this.context.currentPlaying?.title ?? "Untitled"))
|
||||
this.context.track_manifest?.title
|
||||
? this.context.track_manifest?.title
|
||||
: (this.context.loading ? "Loading..." : (this.context.track_manifest?.metadata?.title ?? "Untitled"))
|
||||
}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="subTitle">
|
||||
{
|
||||
this.context.currentManifest?.artist && <div className="artist">
|
||||
this.context.track_manifest?.metadata?.artist && <div className="artist">
|
||||
<h3>
|
||||
{this.context.currentManifest?.artist ?? "Unknown"}
|
||||
{this.context.track_manifest?.metadata?.artist ?? "Unknown"}
|
||||
</h3>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!app.isMobile && this.context.playbackStatus !== "stopped" && <LikeButton
|
||||
onClick={app.cores.player.toggleCurrentTrackLike}
|
||||
liked={this.context.liked}
|
||||
!app.isMobile && this.context.playback_status !== "stopped" && <LikeButton
|
||||
//onClick={app.cores.player.toggleCurrentTrackLike}
|
||||
liked={this.context.track_manifest?.metadata?.liked}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@ -180,20 +236,20 @@ export class AudioPlayer extends React.Component {
|
||||
</div>
|
||||
|
||||
<Controls
|
||||
syncModeLocked={this.context.syncModeLocked}
|
||||
syncMode={this.context.syncMode}
|
||||
playbackStatus={this.context.playbackStatus}
|
||||
audioMuted={this.context.audioMuted}
|
||||
audioVolume={this.context.audioVolume}
|
||||
syncModeLocked={this.context.control_locked}
|
||||
syncMode={this.context.sync_mode}
|
||||
playbackStatus={this.context.playback_status}
|
||||
audioMuted={this.context.muted}
|
||||
audioVolume={this.context.volume}
|
||||
onVolumeUpdate={this.updateVolume}
|
||||
onMuteUpdate={this.toggleMute}
|
||||
controls={{
|
||||
previous: this.onClickPreviousButton,
|
||||
toggle: this.onClickPlayButton,
|
||||
next: this.onClickNextButton,
|
||||
like: app.cores.player.toggleCurrentTrackLike,
|
||||
//like: app.cores.player.toggleCurrentTrackLike,
|
||||
}}
|
||||
liked={this.context.liked}
|
||||
liked={this.context.track_manifest?.metadata?.liked}
|
||||
/>
|
||||
|
||||
<SeekBar
|
||||
|
@ -238,6 +238,18 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
.meme_dancer {
|
||||
width: 100%;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
@ -8,6 +8,7 @@ import "./index.less"
|
||||
|
||||
export default class SeekBar extends React.Component {
|
||||
state = {
|
||||
playing: false,
|
||||
timeText: "00:00",
|
||||
durationText: "00:00",
|
||||
sliderTime: 0,
|
||||
@ -28,16 +29,14 @@ export default class SeekBar extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
calculateDuration = () => {
|
||||
calculateDuration = (preCalculatedDuration) => {
|
||||
// get current audio duration
|
||||
const audioDuration = app.cores.player.duration()
|
||||
const audioDuration = preCalculatedDuration || app.cores.player.duration()
|
||||
|
||||
if (isNaN(audioDuration)) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Audio duration: ${audioDuration}`)
|
||||
|
||||
// set duration
|
||||
this.setState({
|
||||
durationText: seekToTimeLabel(audioDuration)
|
||||
@ -74,9 +73,14 @@ export default class SeekBar extends React.Component {
|
||||
this.updateProgressBar()
|
||||
}
|
||||
|
||||
eventBus = app.cores.player.eventBus
|
||||
|
||||
events = {
|
||||
"player.status.update": (status) => {
|
||||
console.log(`Player status updated: ${status}`)
|
||||
// handle when player changes playback status
|
||||
"player.state.update:playback_status": (status) => {
|
||||
this.setState({
|
||||
playing: status === "playing",
|
||||
})
|
||||
|
||||
switch (status) {
|
||||
case "stopped":
|
||||
@ -96,9 +100,8 @@ export default class SeekBar extends React.Component {
|
||||
break
|
||||
}
|
||||
},
|
||||
"player.current.update": (currentAudioManifest) => {
|
||||
console.log(`Player current audio updated:`, currentAudioManifest)
|
||||
|
||||
// handle when player changes track
|
||||
"player.state.update:track_manifest": (manifest) => {
|
||||
this.updateAll()
|
||||
|
||||
this.setState({
|
||||
@ -106,23 +109,16 @@ export default class SeekBar extends React.Component {
|
||||
sliderTime: 0,
|
||||
})
|
||||
|
||||
this.calculateDuration()
|
||||
this.calculateDuration(manifest.metadata?.duration ?? manifest.duration)
|
||||
},
|
||||
"player.duration.update": (duration) => {
|
||||
console.log(`Player duration updated: ${duration}`)
|
||||
|
||||
this.calculateDuration()
|
||||
},
|
||||
"player.seek.update": (seek) => {
|
||||
console.log(`Player seek updated: ${seek}`)
|
||||
|
||||
"player.seeked": (seek) => {
|
||||
this.calculateTime()
|
||||
this.updateAll()
|
||||
},
|
||||
}
|
||||
|
||||
tick = () => {
|
||||
if (this.props.playing || this.props.streamMode) {
|
||||
if (this.state.playing) {
|
||||
this.interval = setInterval(() => {
|
||||
this.updateAll()
|
||||
}, 1000)
|
||||
@ -138,18 +134,18 @@ export default class SeekBar extends React.Component {
|
||||
this.tick()
|
||||
|
||||
for (const [event, callback] of Object.entries(this.events)) {
|
||||
app.eventBus.on(event, callback)
|
||||
this.eventBus.on(event, callback)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
for (const [event, callback] of Object.entries(this.events)) {
|
||||
app.eventBus.off(event, callback)
|
||||
this.eventBus.off(event, callback)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate = (prevProps, prevState) => {
|
||||
if (this.props.playing !== prevProps.playing) {
|
||||
if (this.state.playing !== prevState.playing) {
|
||||
this.tick()
|
||||
}
|
||||
}
|
||||
|
@ -1,86 +1,60 @@
|
||||
import React from "react"
|
||||
|
||||
export const DefaultContextValues = {
|
||||
currentManifest: null,
|
||||
playbackStatus: null,
|
||||
coverColorAnalysis: null,
|
||||
loading: false,
|
||||
audioMuted: false,
|
||||
audioVolume: 1,
|
||||
minimized: false,
|
||||
streamMode: false,
|
||||
bpm: 0,
|
||||
syncMode: false,
|
||||
syncModeLocked: false,
|
||||
liked: false,
|
||||
|
||||
muted: false,
|
||||
volume: 1,
|
||||
|
||||
sync_mode: false,
|
||||
livestream_mode: false,
|
||||
control_locked: false,
|
||||
|
||||
track_cover_analysis: null,
|
||||
track_metadata: null,
|
||||
|
||||
playback_mode: "repeat",
|
||||
playback_status: null,
|
||||
}
|
||||
|
||||
export const Context = React.createContext(DefaultContextValues)
|
||||
|
||||
export class WithPlayerContext extends React.Component {
|
||||
state = {
|
||||
currentManifest: app.cores.player.getState("currentAudioManifest"),
|
||||
playbackStatus: app.cores.player.getState("playbackStatus") ?? "stopped",
|
||||
coverColorAnalysis: app.cores.player.getState("coverColorAnalysis"),
|
||||
loading: app.cores.player.getState("loading") ?? false,
|
||||
audioMuted: app.cores.player.getState("audioMuted") ?? false,
|
||||
audioVolume: app.cores.player.getState("audioVolume") ?? 0.3,
|
||||
minimized: app.cores.player.getState("minimized") ?? false,
|
||||
streamMode: app.cores.player.getState("livestream") ?? false,
|
||||
bpm: app.cores.player.getState("trackBPM") ?? 0,
|
||||
syncMode: app.cores.player.getState("syncModeLocked"),
|
||||
syncModeLocked: app.cores.player.getState("syncMode"),
|
||||
liked: app.cores.player.getState("liked"),
|
||||
loading: app.cores.player.state["loading"],
|
||||
minimized: app.cores.player.state["minimized"],
|
||||
|
||||
muted: app.cores.player.state["muted"],
|
||||
volume: app.cores.player.state["volume"],
|
||||
|
||||
sync_mode: app.cores.player.state["sync_mode"],
|
||||
livestream_mode: app.cores.player.state["livestream_mode"],
|
||||
control_locked: app.cores.player.state["control_locked"],
|
||||
|
||||
track_manifest: app.cores.player.state["track_manifest"],
|
||||
|
||||
playback_mode: app.cores.player.state["playback_mode"],
|
||||
playback_status: app.cores.player.state["playback_status"],
|
||||
}
|
||||
|
||||
events = {
|
||||
"player.syncModeLocked.update": (to) => {
|
||||
this.setState({ syncModeLocked: to })
|
||||
"player.state.update": (state) => {
|
||||
this.setState(state)
|
||||
},
|
||||
"player.syncMode.update": (to) => {
|
||||
this.setState({ syncMode: to })
|
||||
},
|
||||
"player.livestream.update": (data) => {
|
||||
this.setState({ streamMode: data })
|
||||
},
|
||||
"player.bpm.update": (data) => {
|
||||
this.setState({ bpm: data })
|
||||
},
|
||||
"player.loading.update": (data) => {
|
||||
this.setState({ loading: data })
|
||||
},
|
||||
"player.status.update": (data) => {
|
||||
this.setState({ playbackStatus: data })
|
||||
},
|
||||
"player.current.update": (data) => {
|
||||
this.setState({ currentManifest: data })
|
||||
},
|
||||
"player.mute.update": (data) => {
|
||||
this.setState({ audioMuted: data })
|
||||
},
|
||||
"player.volume.update": (data) => {
|
||||
this.setState({ audioVolume: data })
|
||||
},
|
||||
"player.minimized.update": (minimized) => {
|
||||
this.setState({ minimized })
|
||||
},
|
||||
"player.coverColorAnalysis.update": (data) => {
|
||||
this.setState({ coverColorAnalysis: data })
|
||||
},
|
||||
"player.toggle.like": (data) => {
|
||||
this.setState({ liked: data })
|
||||
}
|
||||
}
|
||||
|
||||
eventBus = app.cores.player.eventBus
|
||||
|
||||
componentDidMount() {
|
||||
for (const [event, handler] of Object.entries(this.events)) {
|
||||
app.eventBus.on(event, handler)
|
||||
this.eventBus.on(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
for (const [event, handler] of Object.entries(this.events)) {
|
||||
app.eventBus.off(event, handler)
|
||||
this.eventBus.off(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,31 +15,13 @@ import EqProcessorNode from "./processors/eqNode"
|
||||
import GainProcessorNode from "./processors/gainNode"
|
||||
import CompressorProcessorNode from "./processors/compressorNode"
|
||||
|
||||
const servicesToManifestResolver = {
|
||||
"tidal": async (manifest) => {
|
||||
const resolvedManifest = await SyncModel.tidalCore.getTrackManifest(manifest.id)
|
||||
|
||||
console.log(resolvedManifest)
|
||||
|
||||
manifest.source = resolvedManifest.playback.url
|
||||
|
||||
manifest.title = resolvedManifest.metadata.title
|
||||
manifest.artist = resolvedManifest.metadata.artists.map(artist => artist.name).join(", ")
|
||||
manifest.album = resolvedManifest.metadata.album.title
|
||||
|
||||
const coverUID = resolvedManifest.metadata.album.cover.replace(/-/g, "/")
|
||||
|
||||
manifest.cover = `https://resources.tidal.com/images/${coverUID}/1280x1280.jpg`
|
||||
|
||||
return manifest
|
||||
}
|
||||
}
|
||||
import servicesToManifestResolver from "./servicesToManifestResolver"
|
||||
|
||||
function useMusicSync(event, data) {
|
||||
const currentRoomData = app.cores.sync.music.currentRoomData()
|
||||
|
||||
if (!currentRoomData) {
|
||||
console.warn("No room data available")
|
||||
this.console.warn("No room data available")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -55,124 +37,7 @@ const defaultAudioProccessors = [
|
||||
CompressorProcessorNode,
|
||||
]
|
||||
|
||||
class MediaSession {
|
||||
initialize() {
|
||||
CapacitorMusicControls.addListener("controlsNotification", (info) => {
|
||||
console.log(info)
|
||||
|
||||
this.handleControlsEvent(info)
|
||||
})
|
||||
|
||||
// ANDROID (13, see bug above as to why it's necessary)
|
||||
document.addEventListener("controlsNotification", (event) => {
|
||||
console.log(event)
|
||||
|
||||
const info = { message: event.message, position: 0 }
|
||||
|
||||
this.handleControlsEvent(info)
|
||||
})
|
||||
}
|
||||
|
||||
update(manifest) {
|
||||
if ("mediaSession" in navigator) {
|
||||
return navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: manifest.title,
|
||||
artist: manifest.artist,
|
||||
album: manifest.album,
|
||||
artwork: [
|
||||
{
|
||||
src: manifest.cover ?? manifest.thumbnail,
|
||||
sizes: "512x512",
|
||||
type: "image/jpeg",
|
||||
}
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return CapacitorMusicControls.create({
|
||||
track: manifest.title,
|
||||
artist: manifest.artist,
|
||||
album: manifest.album,
|
||||
cover: manifest.cover,
|
||||
|
||||
hasPrev: false,
|
||||
hasNext: false,
|
||||
hasClose: true,
|
||||
|
||||
isPlaying: true,
|
||||
dismissable: false,
|
||||
|
||||
playIcon: "media_play",
|
||||
pauseIcon: "media_pause",
|
||||
prevIcon: "media_prev",
|
||||
nextIcon: "media_next",
|
||||
closeIcon: "media_close",
|
||||
notificationIcon: "notification"
|
||||
})
|
||||
}
|
||||
|
||||
updateIsPlaying(to, timeElapsed = 0) {
|
||||
if ("mediaSession" in navigator) {
|
||||
return navigator.mediaSession.playbackState = to ? "playing" : "paused"
|
||||
}
|
||||
|
||||
return CapacitorMusicControls.updateIsPlaying({
|
||||
isPlaying: to,
|
||||
elapsed: timeElapsed,
|
||||
})
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.playbackState = "none"
|
||||
}
|
||||
|
||||
this.active = false
|
||||
|
||||
return CapacitorMusicControls.destroy()
|
||||
}
|
||||
|
||||
handleControlsEvent(action) {
|
||||
const message = action.message
|
||||
|
||||
switch (message) {
|
||||
case "music-controls-next": {
|
||||
return app.cores.player.playback.next()
|
||||
}
|
||||
case "music-controls-previous": {
|
||||
return app.cores.player.playback.previous()
|
||||
}
|
||||
case "music-controls-pause": {
|
||||
return app.cores.player.playback.pause()
|
||||
}
|
||||
case "music-controls-play": {
|
||||
return app.cores.player.playback.play()
|
||||
}
|
||||
case "music-controls-destroy": {
|
||||
return app.cores.player.playback.stop()
|
||||
}
|
||||
|
||||
// External controls (iOS only)
|
||||
case "music-controls-toggle-play-pause": {
|
||||
return app.cores.player.playback.toggle()
|
||||
}
|
||||
|
||||
// Headset events (Android only)
|
||||
// All media button events are listed below
|
||||
case "music-controls-media-button": {
|
||||
return app.cores.player.playback.toggle()
|
||||
}
|
||||
case "music-controls-headset-unplugged": {
|
||||
return app.cores.player.playback.pause()
|
||||
}
|
||||
case "music-controls-headset-plugged": {
|
||||
return app.cores.player.playback.play()
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
import MediaSession from "./mediaSession"
|
||||
|
||||
// TODO: Check if source playing is a stream. Also handle if it's a stream resuming after a pause will seek to the last position
|
||||
export default class Player extends Core {
|
||||
@ -183,9 +48,7 @@ export default class Player extends Core {
|
||||
|
||||
static websocketListen = "music"
|
||||
|
||||
static refName = "player"
|
||||
|
||||
static namespace = "player"
|
||||
static namespace = "player_dep"
|
||||
|
||||
// default statics
|
||||
static maxBufferLoadQueue = 2
|
||||
@ -240,7 +103,7 @@ export default class Player extends Core {
|
||||
start: this.start.bind(this),
|
||||
startPlaylist: this.startPlaylist.bind(this),
|
||||
isIdCurrent: function (id) {
|
||||
console.log("isIdCurrent", id, this.state.currentAudioManifest?._id === id)
|
||||
this.console.log("isIdCurrent", id, this.state.currentAudioManifest?._id === id)
|
||||
|
||||
return this.state.currentAudioManifest?._id === id
|
||||
}.bind(this),
|
||||
@ -292,12 +155,12 @@ export default class Player extends Core {
|
||||
}.bind(this),
|
||||
toggle: function () {
|
||||
if (!this.currentAudioInstance) {
|
||||
console.error("No audio instance")
|
||||
this.console.error("No audio instance")
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.state.syncModeLocked) {
|
||||
console.warn("Sync mode is locked, cannot do this action")
|
||||
this.console.warn("Sync mode is locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -345,10 +208,10 @@ export default class Player extends Core {
|
||||
|
||||
async initializeAudioProcessors() {
|
||||
if (this.audioProcessors.length > 0) {
|
||||
console.log("Destroying audio processors")
|
||||
this.console.log("Destroying audio processors")
|
||||
|
||||
this.audioProcessors.forEach((processor) => {
|
||||
console.log(`Destroying audio processor ${processor.constructor.name}`, processor)
|
||||
this.console.log(`Destroying audio processor ${processor.constructor.name}`, processor)
|
||||
processor._destroy()
|
||||
})
|
||||
|
||||
@ -360,13 +223,13 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
for await (const processor of this.audioProcessors) {
|
||||
console.log(`Initializing audio processor ${processor.constructor.name}`, processor)
|
||||
this.console.log(`Initializing audio processor ${processor.constructor.name}`, processor)
|
||||
|
||||
if (typeof processor._init === "function") {
|
||||
try {
|
||||
await processor._init(this.audioContext)
|
||||
} catch (error) {
|
||||
console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error)
|
||||
this.console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -431,7 +294,7 @@ export default class Player extends Core {
|
||||
this.state.coverColorAnalysis = color
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
this.console.error(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -539,7 +402,7 @@ export default class Player extends Core {
|
||||
this.native_controls.initialize()
|
||||
}
|
||||
|
||||
async initializeBeforeRuntimeInitialize() {
|
||||
async initializeAfterRuntimeInitialize() {
|
||||
for (const [eventName, eventHandler] of Object.entries(this.wsEvents)) {
|
||||
app.cores.api.listenEvent(eventName, eventHandler, Player.websocketListen)
|
||||
}
|
||||
@ -555,7 +418,7 @@ export default class Player extends Core {
|
||||
|
||||
async toggleCurrentTrackLike() {
|
||||
if (!this.currentAudioInstance) {
|
||||
console.error("No track playing")
|
||||
this.console.error("No track playing")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -572,12 +435,12 @@ export default class Player extends Core {
|
||||
|
||||
attachPlayerComponent() {
|
||||
if (this.currentDomWindow) {
|
||||
console.warn("EmbbededMediaPlayer already attached")
|
||||
this.console.warn("EmbbededMediaPlayer already attached")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!app.layout.floatingStack) {
|
||||
console.error("Floating stack not found")
|
||||
this.console.error("Floating stack not found")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -586,12 +449,12 @@ export default class Player extends Core {
|
||||
|
||||
detachPlayerComponent() {
|
||||
if (!this.currentDomWindow) {
|
||||
console.warn("EmbbededMediaPlayer not attached")
|
||||
this.console.warn("EmbbededMediaPlayer not attached")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!app.layout.floatingStack) {
|
||||
console.error("Floating stack not found")
|
||||
this.console.error("Floating stack not found")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -606,7 +469,7 @@ export default class Player extends Core {
|
||||
|
||||
enqueueLoadBuffer(audioElement) {
|
||||
if (!audioElement) {
|
||||
console.error("Audio element is required")
|
||||
this.console.error("Audio element is required")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -635,7 +498,7 @@ export default class Player extends Core {
|
||||
const audioElement = this.bufferLoadQueue.shift()
|
||||
|
||||
if (audioElement.signal.aborted) {
|
||||
console.warn("Aborted audio element")
|
||||
this.console.warn("Aborted audio element")
|
||||
|
||||
this.bufferLoadQueueLoading = false
|
||||
|
||||
@ -651,7 +514,7 @@ export default class Player extends Core {
|
||||
resolve()
|
||||
}, { once: true })
|
||||
|
||||
console.log("Preloading audio buffer", audioElement.src)
|
||||
this.console.log("Preloading audio buffer", audioElement.src)
|
||||
|
||||
audioElement.load()
|
||||
})
|
||||
@ -703,7 +566,7 @@ export default class Player extends Core {
|
||||
|
||||
async createInstance(manifest) {
|
||||
if (!manifest) {
|
||||
console.error("Manifest is required")
|
||||
this.console.error("Manifest is required")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -716,11 +579,11 @@ export default class Player extends Core {
|
||||
|
||||
// check if manifest has `manifest` property
|
||||
if (manifest.service) {
|
||||
if (manifest.service !== "inherit") {
|
||||
if (manifest.service !== "inherit" && !manifest.source) {
|
||||
const resolver = servicesToManifestResolver[manifest.service]
|
||||
|
||||
if (!resolver) {
|
||||
console.error(`Service ${manifest.service} is not supported`)
|
||||
this.console.error(`Service ${manifest.service} is not supported`)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -729,7 +592,7 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
if (!manifest.src && !manifest.source) {
|
||||
console.error("Manifest source is required")
|
||||
this.console.error("Manifest source is required")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -773,7 +636,7 @@ export default class Player extends Core {
|
||||
instanceObj.audioElement.addEventListener("loadeddata", () => {
|
||||
this.state.loading = false
|
||||
|
||||
console.log("Loaded audio data", instanceObj.audioElement.src)
|
||||
this.console.log("Loaded audio data", instanceObj.audioElement.src)
|
||||
})
|
||||
|
||||
instanceObj.audioElement.addEventListener("playing", () => {
|
||||
@ -854,7 +717,7 @@ export default class Player extends Core {
|
||||
async attachProcessorsToInstance(instance) {
|
||||
for await (const [index, processor] of this.audioProcessors.entries()) {
|
||||
if (typeof processor._attach !== "function") {
|
||||
console.error(`Processor ${processor.constructor.refName} not support attach`)
|
||||
this.console.error(`Processor ${processor.constructor.refName} not support attach`)
|
||||
|
||||
continue
|
||||
}
|
||||
@ -864,7 +727,7 @@ export default class Player extends Core {
|
||||
|
||||
const lastProcessor = instance.attachedProcessors[instance.attachedProcessors.length - 1].processor
|
||||
|
||||
console.log("Attached processors", instance.attachedProcessors)
|
||||
this.console.log("Attached processors", instance.attachedProcessors)
|
||||
|
||||
// now attach to destination
|
||||
lastProcessor.connect(this.audioContext.destination)
|
||||
@ -943,7 +806,7 @@ export default class Player extends Core {
|
||||
|
||||
// check if the audio is a live stream when metadata is loaded
|
||||
instance.audioElement.addEventListener("loadedmetadata", () => {
|
||||
console.log("loadedmetadata", instance.audioElement.duration)
|
||||
this.console.log("loadedmetadata", instance.audioElement.duration)
|
||||
|
||||
if (instance.audioElement.duration === Infinity) {
|
||||
instance.manifest.stream = true
|
||||
@ -962,7 +825,7 @@ export default class Player extends Core {
|
||||
|
||||
async startPlaylist(playlist, startIndex = 0, { sync = false } = {}) {
|
||||
if (this.state.syncModeLocked && !sync) {
|
||||
console.warn("Sync mode is locked, cannot do this action")
|
||||
this.console.warn("Sync mode is locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -971,13 +834,17 @@ export default class Player extends Core {
|
||||
throw new Error("Playlist is required")
|
||||
}
|
||||
|
||||
console.log("Starting playlist", playlist)
|
||||
|
||||
// check if the array has strings, if so its means that is the track id, then fetch the track
|
||||
if (playlist.some(item => typeof item === "string")) {
|
||||
if (playlist.some((item) => typeof item === "string")) {
|
||||
this.console.log("Resolving missing manifests by ids...")
|
||||
playlist = await this.getTracksByIds(playlist)
|
||||
}
|
||||
|
||||
this.console.log("Starting playlist", playlist)
|
||||
|
||||
this.state.loading = true
|
||||
|
||||
// !IMPORTANT: abort preloads before destroying current instance
|
||||
await this.abortPreloads()
|
||||
|
||||
@ -985,28 +852,36 @@ export default class Player extends Core {
|
||||
|
||||
// clear current queue
|
||||
this.audioQueue = []
|
||||
|
||||
this.audioQueueHistory = []
|
||||
|
||||
this.state.loading = true
|
||||
// sort playlist entries to prioritize instance creating from the startIndex
|
||||
playlist[startIndex].first = true
|
||||
|
||||
for await (const [index, manifest] of playlist.entries()) {
|
||||
const afterPlaylist = playlist.slice(startIndex)
|
||||
const beforePlaylist = playlist.slice(0, startIndex).reverse()
|
||||
|
||||
for await (const [index, manifest] of afterPlaylist.entries()) {
|
||||
const instance = await this.createInstance(manifest)
|
||||
|
||||
if (index < startIndex) {
|
||||
this.audioQueueHistory.push(instance)
|
||||
} else {
|
||||
this.audioQueue.push(instance)
|
||||
|
||||
if (index === 0) {
|
||||
this.play(this.audioQueue[0])
|
||||
}
|
||||
}
|
||||
|
||||
// play first audio
|
||||
this.play(this.audioQueue[0])
|
||||
for await (const [index, manifest] of beforePlaylist.entries()) {
|
||||
const instance = await this.createInstance(manifest)
|
||||
|
||||
this.audioQueueHistory.push(instance)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async start(manifest, { sync = false, time } = {}) {
|
||||
if (this.state.syncModeLocked && !sync) {
|
||||
console.warn("Sync mode is locked, cannot do this action")
|
||||
this.console.warn("Sync mode is locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1034,7 +909,7 @@ export default class Player extends Core {
|
||||
|
||||
next({ sync = false } = {}) {
|
||||
if (this.state.syncModeLocked && !sync) {
|
||||
console.warn("Sync mode is locked, cannot do this action")
|
||||
this.console.warn("Sync mode is locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1045,7 +920,7 @@ export default class Player extends Core {
|
||||
|
||||
// check if there is a next audio in queue
|
||||
if (this.audioQueue.length === 0) {
|
||||
console.log("no more audio on queue, stopping playback")
|
||||
this.console.log("no more audio on queue, stopping playback")
|
||||
|
||||
this.destroyCurrentInstance()
|
||||
|
||||
@ -1068,7 +943,7 @@ export default class Player extends Core {
|
||||
|
||||
previous({ sync = false } = {}) {
|
||||
if (this.state.syncModeLocked && !sync) {
|
||||
console.warn("Sync mode is locked, cannot do this action")
|
||||
this.console.warn("Sync mode is locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1090,7 +965,7 @@ export default class Player extends Core {
|
||||
async pausePlayback() {
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (!this.currentAudioInstance) {
|
||||
console.error("No audio instance")
|
||||
this.console.error("No audio instance")
|
||||
return null
|
||||
}
|
||||
|
||||
@ -1112,7 +987,7 @@ export default class Player extends Core {
|
||||
async resumePlayback() {
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (!this.currentAudioInstance) {
|
||||
console.error("No audio instance")
|
||||
this.console.error("No audio instance")
|
||||
return null
|
||||
}
|
||||
|
||||
@ -1155,7 +1030,7 @@ export default class Player extends Core {
|
||||
|
||||
toggleMute(to) {
|
||||
if (app.isMobile) {
|
||||
console.warn("Cannot mute on mobile")
|
||||
this.console.warn("Cannot mute on mobile")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1180,7 +1055,7 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
if (app.isMobile) {
|
||||
console.warn("Cannot change volume on mobile")
|
||||
this.console.warn("Cannot change volume on mobile")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1216,7 +1091,7 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
if (this.state.syncModeLocked && !sync) {
|
||||
console.warn("Sync mode is locked, cannot do this action")
|
||||
this.console.warn("Sync mode is locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1238,7 +1113,7 @@ export default class Player extends Core {
|
||||
|
||||
loop(to) {
|
||||
if (typeof to !== "boolean") {
|
||||
console.warn("Loop must be a boolean")
|
||||
this.console.warn("Loop must be a boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1253,12 +1128,12 @@ export default class Player extends Core {
|
||||
|
||||
velocity(to) {
|
||||
if (this.state.syncModeLocked) {
|
||||
console.warn("Sync mode is locked, cannot do this action")
|
||||
this.console.warn("Sync mode is locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof to !== "number") {
|
||||
console.warn("Velocity must be a number")
|
||||
this.console.warn("Velocity must be a number")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1273,7 +1148,7 @@ export default class Player extends Core {
|
||||
|
||||
collapse(to) {
|
||||
if (typeof to !== "boolean") {
|
||||
console.warn("Collapse must be a boolean")
|
||||
this.console.warn("Collapse must be a boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1284,7 +1159,7 @@ export default class Player extends Core {
|
||||
|
||||
toggleSyncMode(to, lock) {
|
||||
if (typeof to !== "boolean") {
|
||||
console.warn("Sync mode must be a boolean")
|
||||
this.console.warn("Sync mode must be a boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1292,7 +1167,7 @@ export default class Player extends Core {
|
||||
|
||||
this.state.syncModeLocked = lock ?? false
|
||||
|
||||
console.log(`Sync mode is now ${this.state.syncMode ? "enabled" : "disabled"} | Locked: ${this.state.syncModeLocked ? "yes" : "no"}`)
|
||||
this.console.log(`Sync mode is now ${this.state.syncMode ? "enabled" : "disabled"} | Locked: ${this.state.syncModeLocked ? "yes" : "no"}`)
|
||||
|
||||
return this.state.syncMode
|
||||
}
|
||||
@ -1312,7 +1187,7 @@ export default class Player extends Core {
|
||||
|
||||
async getTracksByIds(list) {
|
||||
if (!Array.isArray(list)) {
|
||||
console.warn("List must be an array")
|
||||
this.console.warn("List must be an array")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1329,7 +1204,7 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
const fetchedTracks = await PlaylistModel.getTracks(ids).catch((err) => {
|
||||
console.error(err)
|
||||
this.console.error(err)
|
||||
return false
|
||||
})
|
||||
|
||||
@ -1352,13 +1227,13 @@ export default class Player extends Core {
|
||||
async setSampleRate(to) {
|
||||
// must be a integer
|
||||
if (typeof to !== "number") {
|
||||
console.error("Sample rate must be a number")
|
||||
this.console.error("Sample rate must be a number")
|
||||
return this.audioContext.sampleRate
|
||||
}
|
||||
|
||||
// must be a integer
|
||||
if (!Number.isInteger(to)) {
|
||||
console.error("Sample rate must be a integer")
|
||||
this.console.error("Sample rate must be a integer")
|
||||
return this.audioContext.sampleRate
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export default class ProcessorNode {
|
||||
if (Array.isArray(this.constructor.dependsOnSettings)) {
|
||||
// check if the instance has the settings
|
||||
if (!this.constructor.dependsOnSettings.every((setting) => app.cores.settings.get(setting))) {
|
||||
console.warn(`Skipping attachment for [${this.constructor.refName ?? this.constructor.name}] node, cause is not passing the settings dependecy > ${this.constructor.dependsOnSettings.join(", ")}`)
|
||||
this.console.warn(`Skipping attachment for [${this.constructor.refName ?? this.constructor.name}] node, cause is not passing the settings dependecy > ${this.constructor.dependsOnSettings.join(", ")}`)
|
||||
|
||||
return instance
|
||||
}
|
||||
@ -55,7 +55,7 @@ export default class ProcessorNode {
|
||||
|
||||
// check if is already attached
|
||||
if (currentIndex !== false) {
|
||||
console.warn(`[${this.constructor.refName ?? this.constructor.name}] node is already attached`)
|
||||
this.console.warn(`[${this.constructor.refName ?? this.constructor.name}] node is already attached`)
|
||||
|
||||
return instance
|
||||
}
|
||||
@ -64,14 +64,14 @@ export default class ProcessorNode {
|
||||
// if has, disconnect it
|
||||
// if it not has, its means that is the first node, so connect to the media source
|
||||
if (prevNode && prevNode.processor._last.numberOfOutputs > 0) {
|
||||
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is already attached to the previous node, disconnecting...`)
|
||||
//this.console.log(`[${this.constructor.refName ?? this.constructor.name}] node is already attached to the previous node, disconnecting...`)
|
||||
// if has outputs, disconnect from the next node
|
||||
prevNode.processor._last.disconnect()
|
||||
|
||||
// now, connect to the processor
|
||||
prevNode.processor._last.connect(this.processor)
|
||||
} else {
|
||||
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is the first node, connecting to the media source...`)
|
||||
//this.console.log(`[${this.constructor.refName ?? this.constructor.name}] node is the first node, connecting to the media source...`)
|
||||
instance.media.connect(this.processor)
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ export default class ProcessorNode {
|
||||
}
|
||||
|
||||
if (!instance) {
|
||||
console.warn(`Instance is not defined`)
|
||||
this.console.warn(`Instance is not defined`)
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ export default class EqProcessorNode extends ProcessorNode {
|
||||
const gainValue = this.state.eqValues[processor.frequency.value].gain
|
||||
|
||||
if (processor.gain.value !== gainValue) {
|
||||
console.debug(`[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${gainValue}`)
|
||||
this.console.debug(`[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${gainValue}`)
|
||||
processor.gain.value = this.state.eqValues[processor.frequency.value].gain
|
||||
}
|
||||
})
|
||||
|
21
packages/app/src/cores/player/servicesToManifestResolver.js
Normal file
21
packages/app/src/cores/player/servicesToManifestResolver.js
Normal file
@ -0,0 +1,21 @@
|
||||
import SyncModel from "comty.js/models/sync"
|
||||
|
||||
export default {
|
||||
"tidal": async (manifest) => {
|
||||
const resolvedManifest = await SyncModel.tidalCore.getTrackManifest(manifest.id)
|
||||
|
||||
this.console.log(resolvedManifest)
|
||||
|
||||
manifest.source = resolvedManifest.playback.url
|
||||
|
||||
manifest.title = resolvedManifest.metadata.title
|
||||
manifest.artist = resolvedManifest.metadata.artists.map(artist => artist.name).join(", ")
|
||||
manifest.album = resolvedManifest.metadata.album.title
|
||||
|
||||
const coverUID = resolvedManifest.metadata.album.cover.replace(/-/g, "/")
|
||||
|
||||
manifest.cover = `https://resources.tidal.com/images/${coverUID}/1280x1280.jpg`
|
||||
|
||||
return manifest
|
||||
}
|
||||
}
|
120
packages/app/src/cores/playerv2/mediaSession.js
Normal file
120
packages/app/src/cores/playerv2/mediaSession.js
Normal file
@ -0,0 +1,120 @@
|
||||
import { CapacitorMusicControls } from "capacitor-music-controls-plugin-v3"
|
||||
|
||||
export default class MediaSession {
|
||||
initialize() {
|
||||
CapacitorMusicControls.addListener("controlsNotification", (info) => {
|
||||
this.console.log(info)
|
||||
|
||||
this.handleControlsEvent(info)
|
||||
})
|
||||
|
||||
// ANDROID (13, see bug above as to why it's necessary)
|
||||
document.addEventListener("controlsNotification", (event) => {
|
||||
this.console.log(event)
|
||||
|
||||
const info = { message: event.message, position: 0 }
|
||||
|
||||
this.handleControlsEvent(info)
|
||||
})
|
||||
}
|
||||
|
||||
update(manifest) {
|
||||
if ("mediaSession" in navigator) {
|
||||
return navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: manifest.title,
|
||||
artist: manifest.artist,
|
||||
album: manifest.album,
|
||||
artwork: [
|
||||
{
|
||||
src: manifest.cover ?? manifest.thumbnail,
|
||||
sizes: "512x512",
|
||||
type: "image/jpeg",
|
||||
}
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return CapacitorMusicControls.create({
|
||||
track: manifest.title,
|
||||
artist: manifest.artist,
|
||||
album: manifest.album,
|
||||
cover: manifest.cover,
|
||||
|
||||
hasPrev: false,
|
||||
hasNext: false,
|
||||
hasClose: true,
|
||||
|
||||
isPlaying: true,
|
||||
dismissable: false,
|
||||
|
||||
playIcon: "media_play",
|
||||
pauseIcon: "media_pause",
|
||||
prevIcon: "media_prev",
|
||||
nextIcon: "media_next",
|
||||
closeIcon: "media_close",
|
||||
notificationIcon: "notification"
|
||||
})
|
||||
}
|
||||
|
||||
updateIsPlaying(to, timeElapsed = 0) {
|
||||
if ("mediaSession" in navigator) {
|
||||
return navigator.mediaSession.playbackState = to ? "playing" : "paused"
|
||||
}
|
||||
|
||||
return CapacitorMusicControls.updateIsPlaying({
|
||||
isPlaying: to,
|
||||
elapsed: timeElapsed,
|
||||
})
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.playbackState = "none"
|
||||
}
|
||||
|
||||
this.active = false
|
||||
|
||||
return CapacitorMusicControls.destroy()
|
||||
}
|
||||
|
||||
handleControlsEvent(action) {
|
||||
const message = action.message
|
||||
|
||||
switch (message) {
|
||||
case "music-controls-next": {
|
||||
return app.cores.player.playback.next()
|
||||
}
|
||||
case "music-controls-previous": {
|
||||
return app.cores.player.playback.previous()
|
||||
}
|
||||
case "music-controls-pause": {
|
||||
return app.cores.player.playback.pause()
|
||||
}
|
||||
case "music-controls-play": {
|
||||
return app.cores.player.playback.play()
|
||||
}
|
||||
case "music-controls-destroy": {
|
||||
return app.cores.player.playback.stop()
|
||||
}
|
||||
|
||||
// External controls (iOS only)
|
||||
case "music-controls-toggle-play-pause": {
|
||||
return app.cores.player.playback.toggle()
|
||||
}
|
||||
|
||||
// Headset events (Android only)
|
||||
// All media button events are listed below
|
||||
case "music-controls-media-button": {
|
||||
return app.cores.player.playback.toggle()
|
||||
}
|
||||
case "music-controls-headset-unplugged": {
|
||||
return app.cores.player.playback.pause()
|
||||
}
|
||||
case "music-controls-headset-plugged": {
|
||||
return app.cores.player.playback.play()
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
935
packages/app/src/cores/playerv2/player.core.js
Executable file
935
packages/app/src/cores/playerv2/player.core.js
Executable file
@ -0,0 +1,935 @@
|
||||
import Core from "evite/src/core"
|
||||
import EventEmitter from "evite/src/internals/EventEmitter"
|
||||
import { Observable } from "object-observer"
|
||||
import { FastAverageColor } from "fast-average-color"
|
||||
|
||||
import PlaylistModel from "comty.js/models/playlists"
|
||||
|
||||
import EmbbededMediaPlayer from "components/Player/MediaPlayer"
|
||||
import BackgroundMediaPlayer from "components/Player/BackgroundMediaPlayer"
|
||||
|
||||
import AudioPlayerStorage from "./player.storage"
|
||||
|
||||
import defaultAudioProccessors from "./processors"
|
||||
|
||||
import MediaSession from "./mediaSession"
|
||||
import servicesToManifestResolver from "./servicesToManifestResolver"
|
||||
|
||||
export default class Player extends Core {
|
||||
static dependencies = [
|
||||
"api",
|
||||
"settings"
|
||||
]
|
||||
|
||||
static namespace = "player"
|
||||
|
||||
static bgColor = "aquamarine"
|
||||
static textColor = "black"
|
||||
|
||||
static defaultSampleRate = 48000
|
||||
|
||||
static gradualFadeMs = 150
|
||||
|
||||
// buffer & precomputation
|
||||
static maxManifestPrecompute = 3
|
||||
|
||||
native_controls = new MediaSession()
|
||||
|
||||
currentDomWindow = null
|
||||
|
||||
audioContext = new AudioContext({
|
||||
sampleRate: AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate,
|
||||
latencyHint: "playback"
|
||||
})
|
||||
|
||||
audioProcessors = []
|
||||
|
||||
eventBus = new EventEmitter()
|
||||
|
||||
fac = new FastAverageColor()
|
||||
|
||||
track_prev_instances = []
|
||||
track_instance = null
|
||||
track_next_instances = []
|
||||
|
||||
state = Observable.from({
|
||||
loading: false,
|
||||
minimized: false,
|
||||
|
||||
muted: app.isMobile ? false : (AudioPlayerStorage.get("mute") ?? false),
|
||||
volume: app.isMobile ? 1 : (AudioPlayerStorage.get("volume") ?? 0.3),
|
||||
|
||||
sync_mode: false,
|
||||
livestream_mode: false,
|
||||
control_locked: false,
|
||||
|
||||
track_manifest: null,
|
||||
|
||||
playback_mode: AudioPlayerStorage.get("mode") ?? "repeat",
|
||||
playback_status: "stopped",
|
||||
})
|
||||
|
||||
public = {
|
||||
audioContext: this.audioContext,
|
||||
setSampleRate: this.setSampleRate,
|
||||
start: this.start.bind(this),
|
||||
close: this.close.bind(this),
|
||||
playback: {
|
||||
mode: this.playbackMode.bind(this),
|
||||
stop: this.stop.bind(this),
|
||||
toggle: this.togglePlayback.bind(this),
|
||||
pause: this.pausePlayback.bind(this),
|
||||
play: this.resumePlayback.bind(this),
|
||||
next: this.next.bind(this),
|
||||
previous: this.previous.bind(this),
|
||||
seek: this.seek.bind(this),
|
||||
},
|
||||
duration: this.duration.bind(this),
|
||||
volume: this.volume.bind(this),
|
||||
mute: this.mute.bind(this),
|
||||
toggleMute: this.toggleMute.bind(this),
|
||||
seek: this.seek.bind(this),
|
||||
minimize: this.toggleMinimize.bind(this),
|
||||
collapse: this.toggleCollapse.bind(this),
|
||||
state: new Proxy(this.state, {
|
||||
get: (target, prop) => {
|
||||
return target[prop]
|
||||
},
|
||||
set: (target, prop, value) => {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
eventBus: new Proxy(this.eventBus, {
|
||||
get: (target, prop) => {
|
||||
return target[prop]
|
||||
},
|
||||
set: (target, prop, value) => {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async onInitialize() {
|
||||
this.initializeAudioProcessors()
|
||||
|
||||
Observable.observe(this.state, async (changes) => {
|
||||
try {
|
||||
changes.forEach((change) => {
|
||||
if (change.type === "update") {
|
||||
const stateKey = change.path[0]
|
||||
|
||||
this.eventBus.emit(`player.state.update:${stateKey}`, change.object[stateKey])
|
||||
this.eventBus.emit("player.state.update", change.object)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
this.console.error(`Failed to dispatch state updater >`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async initializeBeforeRuntimeInitialize() {
|
||||
for (const [eventName, eventHandler] of Object.entries(this.wsEvents)) {
|
||||
app.cores.api.listenEvent(eventName, eventHandler, Player.websocketListen)
|
||||
}
|
||||
|
||||
if (app.isMobile) {
|
||||
this.state.audioVolume = 1
|
||||
}
|
||||
}
|
||||
|
||||
async initializeAudioProcessors() {
|
||||
if (this.audioProcessors.length > 0) {
|
||||
this.console.log("Destroying audio processors")
|
||||
|
||||
this.audioProcessors.forEach((processor) => {
|
||||
this.console.log(`Destroying audio processor ${processor.constructor.name}`, processor)
|
||||
processor._destroy()
|
||||
})
|
||||
|
||||
this.audioProcessors = []
|
||||
}
|
||||
|
||||
for await (const defaultProccessor of defaultAudioProccessors) {
|
||||
this.audioProcessors.push(new defaultProccessor(this))
|
||||
}
|
||||
|
||||
for await (const processor of this.audioProcessors) {
|
||||
this.console.log(`Initializing audio processor ${processor.constructor.name}`, processor)
|
||||
|
||||
if (typeof processor._init === "function") {
|
||||
try {
|
||||
await processor._init(this.audioContext)
|
||||
} catch (error) {
|
||||
this.console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// check if processor has exposed public methods
|
||||
if (processor.exposeToPublic) {
|
||||
Object.entries(processor.exposeToPublic).forEach(([key, value]) => {
|
||||
const refName = processor.constructor.refName
|
||||
|
||||
if (typeof this.public[refName] === "undefined") {
|
||||
// by default create a empty object
|
||||
this.public[refName] = {}
|
||||
}
|
||||
|
||||
this.public[refName][key] = value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UI Methods
|
||||
//
|
||||
|
||||
attachPlayerComponent() {
|
||||
if (this.currentDomWindow) {
|
||||
this.console.warn("EmbbededMediaPlayer already attached")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!app.layout.floatingStack) {
|
||||
this.console.error("Floating stack not found")
|
||||
return false
|
||||
}
|
||||
|
||||
this.currentDomWindow = app.layout.floatingStack.add("mediaPlayer", EmbbededMediaPlayer)
|
||||
}
|
||||
|
||||
detachPlayerComponent() {
|
||||
if (!this.currentDomWindow) {
|
||||
this.console.warn("EmbbededMediaPlayer not attached")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!app.layout.floatingStack) {
|
||||
this.console.error("Floating stack not found")
|
||||
return false
|
||||
}
|
||||
|
||||
app.layout.floatingStack.remove("mediaPlayer")
|
||||
|
||||
this.currentDomWindow = null
|
||||
}
|
||||
|
||||
//
|
||||
// Instance managing methods
|
||||
//
|
||||
async abortPreloads() {
|
||||
for await (const instance of this.track_next_instances) {
|
||||
if (instance.abortController?.abort) {
|
||||
instance.abortController.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async preloadAudioInstance(instance) {
|
||||
const isIndex = typeof instance === "number"
|
||||
|
||||
let index = isIndex ? instance : 0
|
||||
|
||||
if (isIndex) {
|
||||
instance = this.track_next_instances[instance]
|
||||
}
|
||||
|
||||
if (!instance) {
|
||||
this.console.error("Instance not found to preload")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!instance.manifest.cover_analysis) {
|
||||
const img = new Image()
|
||||
|
||||
img.crossOrigin = "anonymous"
|
||||
img.src = `https://cors-anywhere.herokuapp.com/${instance.manifest.cover ?? instance.manifest.thumbnail}`
|
||||
|
||||
const cover_analysis = await this.fac.getColorAsync(img)
|
||||
.catch((err) => {
|
||||
this.console.error(err)
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
instance.manifest.cover_analysis = cover_analysis
|
||||
}
|
||||
|
||||
if (!instance._preloaded) {
|
||||
instance.media.preload = "metadata"
|
||||
instance._preloaded = true
|
||||
}
|
||||
|
||||
if (isIndex) {
|
||||
this.track_next_instances[index] = instance
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
async destroyCurrentInstance({ sync = false } = {}) {
|
||||
if (!this.track_instance) {
|
||||
return false
|
||||
}
|
||||
|
||||
// stop playback
|
||||
if (this.track_instance.media) {
|
||||
this.track_instance.media.pause()
|
||||
}
|
||||
|
||||
// reset track_instance
|
||||
this.track_instance = null
|
||||
|
||||
// reset livestream mode
|
||||
this.state.livestream_mode = false
|
||||
}
|
||||
|
||||
async createInstance(manifest) {
|
||||
if (!manifest) {
|
||||
this.console.error("Manifest is required")
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof manifest === "string") {
|
||||
manifest = {
|
||||
src: manifest,
|
||||
}
|
||||
}
|
||||
|
||||
// check if manifest has `manifest` property, if is and not inherit or missing source, resolve
|
||||
if (manifest.service) {
|
||||
if (manifest.service !== "inherit" && !manifest.source) {
|
||||
const resolver = servicesToManifestResolver[manifest.service]
|
||||
|
||||
if (!resolver) {
|
||||
this.console.error(`Service ${manifest.service} is not supported`)
|
||||
return false
|
||||
}
|
||||
|
||||
manifest = await resolver(manifest)
|
||||
}
|
||||
}
|
||||
|
||||
if (!manifest.src && !manifest.source) {
|
||||
this.console.error("Manifest source is required")
|
||||
return false
|
||||
}
|
||||
|
||||
const source = manifest.src ?? manifest.source
|
||||
|
||||
if (!manifest.metadata) {
|
||||
manifest.metadata = {}
|
||||
}
|
||||
|
||||
// if title is not set, use the audio source filename
|
||||
if (!manifest.metadata.title) {
|
||||
manifest.metadata.title = source.split("/").pop()
|
||||
}
|
||||
|
||||
let instance = {
|
||||
manifest: manifest,
|
||||
attachedProcessors: [],
|
||||
abortController: new AbortController(),
|
||||
source: source,
|
||||
media: new Audio(source),
|
||||
duration: null,
|
||||
seek: 0,
|
||||
track: null,
|
||||
}
|
||||
|
||||
instance.media.signal = instance.abortController.signal
|
||||
instance.media.crossOrigin = "anonymous"
|
||||
instance.media.preload = "none"
|
||||
|
||||
instance.media.loop = this.state.playback_mode === "repeat"
|
||||
instance.media.volume = this.state.volume
|
||||
|
||||
// handle on end
|
||||
instance.media.addEventListener("ended", () => {
|
||||
this.next()
|
||||
})
|
||||
|
||||
instance.media.addEventListener("loadeddata", () => {
|
||||
this.state.loading = false
|
||||
})
|
||||
|
||||
// update playback status
|
||||
instance.media.addEventListener("play", () => {
|
||||
this.state.playback_status = "playing"
|
||||
})
|
||||
|
||||
instance.media.addEventListener("playing", () => {
|
||||
this.state.loading = false
|
||||
|
||||
this.state.playback_status = "playing"
|
||||
|
||||
if (this.waitUpdateTimeout) {
|
||||
clearTimeout(this.waitUpdateTimeout)
|
||||
this.waitUpdateTimeout = null
|
||||
}
|
||||
})
|
||||
|
||||
instance.media.addEventListener("pause", () => {
|
||||
this.state.playback_status = "paused"
|
||||
})
|
||||
|
||||
instance.media.addEventListener("durationchange", (duration) => {
|
||||
if (instance.media.paused) {
|
||||
return false
|
||||
}
|
||||
|
||||
instance.duration = duration
|
||||
})
|
||||
|
||||
instance.media.addEventListener("waiting", () => {
|
||||
if (instance.media.paused) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.waitUpdateTimeout) {
|
||||
clearTimeout(this.waitUpdateTimeout)
|
||||
this.waitUpdateTimeout = null
|
||||
}
|
||||
|
||||
// if takes more than 150ms to load, update loading state
|
||||
this.waitUpdateTimeout = setTimeout(() => {
|
||||
this.state.loading = true
|
||||
}, 150)
|
||||
})
|
||||
|
||||
instance.media.addEventListener("seeked", () => {
|
||||
instance.seek = instance.media.currentTime
|
||||
|
||||
if (this.state.sync_mode) {
|
||||
// useMusicSync("music:player:seek", {
|
||||
// position: instance.seek,
|
||||
// state: this.state,
|
||||
// })
|
||||
}
|
||||
|
||||
this.eventBus.emit(`player.seeked`, instance.seek)
|
||||
})
|
||||
|
||||
instance.media.addEventListener("loadedmetadata", () => {
|
||||
if (instance.media.duration === Infinity) {
|
||||
instance.manifest.stream = true
|
||||
|
||||
this.state.livestream_mode = true
|
||||
}
|
||||
}, { once: true })
|
||||
|
||||
instance.track = this.audioContext.createMediaElementSource(instance.media)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
async attachProcessorsToInstance(instance) {
|
||||
for await (const [index, processor] of this.audioProcessors.entries()) {
|
||||
if (processor.constructor.node_bypass === true) {
|
||||
instance.track.connect(processor.processor)
|
||||
|
||||
processor.processor.connect(this.audioContext.destination)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof processor._attach !== "function") {
|
||||
this.console.error(`Processor ${processor.constructor.refName} not support attach`)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
instance = await processor._attach(instance, index)
|
||||
}
|
||||
|
||||
const lastProcessor = instance.attachedProcessors[instance.attachedProcessors.length - 1].processor
|
||||
|
||||
// now attach to destination
|
||||
lastProcessor.connect(this.audioContext.destination)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
//
|
||||
// Playback methods
|
||||
//
|
||||
async play(instance, params = {}) {
|
||||
if (typeof instance === "number") {
|
||||
if (instance < 0) {
|
||||
instance = this.track_prev_instances[instance]
|
||||
}
|
||||
|
||||
if (instance > 0) {
|
||||
instance = this.track_instances[instance]
|
||||
}
|
||||
|
||||
if (instance === 0) {
|
||||
instance = this.track_instance
|
||||
}
|
||||
}
|
||||
|
||||
if (!instance) {
|
||||
throw new Error("Audio instance is required")
|
||||
}
|
||||
|
||||
if (this.audioContext.state === "suspended") {
|
||||
this.audioContext.resume()
|
||||
}
|
||||
|
||||
if (this.track_instance) {
|
||||
this.track_instance = this.track_instance.attachedProcessors[this.track_instance.attachedProcessors.length - 1]._destroy(this.track_instance)
|
||||
|
||||
this.destroyCurrentInstance()
|
||||
}
|
||||
|
||||
// attach processors
|
||||
instance = await this.attachProcessorsToInstance(instance)
|
||||
|
||||
// now set the current instance
|
||||
this.track_instance = await this.preloadAudioInstance(instance)
|
||||
|
||||
// reconstruct audio src if is not set
|
||||
if (this.track_instance.media.src !== instance.source) {
|
||||
this.track_instance.media.src = instance.source
|
||||
}
|
||||
|
||||
// set time to 0
|
||||
this.track_instance.media.currentTime = 0
|
||||
|
||||
if (params.time >= 0) {
|
||||
this.track_instance.media.currentTime = params.time
|
||||
}
|
||||
|
||||
if (params.volume >= 0) {
|
||||
this.track_instance.gainNode.gain.value = params.volume
|
||||
} else {
|
||||
this.track_instance.gainNode.gain.value = this.state.volume
|
||||
}
|
||||
|
||||
this.track_instance.media.muted = this.state.muted
|
||||
|
||||
// try to preload next audio
|
||||
if (this.track_next_instances.length > 0) {
|
||||
this.preloadAudioInstance(1)
|
||||
}
|
||||
|
||||
// play
|
||||
await this.track_instance.media.play()
|
||||
|
||||
// update manifest
|
||||
this.state.track_manifest = instance.manifest
|
||||
|
||||
this.native_controls.update(instance.manifest)
|
||||
|
||||
return this.track_instance
|
||||
}
|
||||
|
||||
async start(manifest, { sync = false, time, startIndex = 0 } = {}) {
|
||||
if (this.state.control_locked && !sync) {
|
||||
this.console.warn("Controls are locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
this.attachPlayerComponent()
|
||||
|
||||
// !IMPORTANT: abort preloads before destroying current instance
|
||||
await this.abortPreloads()
|
||||
await this.destroyCurrentInstance({
|
||||
sync
|
||||
})
|
||||
|
||||
this.state.loading = true
|
||||
|
||||
this.track_prev_instances = []
|
||||
this.track_next_instances = []
|
||||
|
||||
const isPlaylist = Array.isArray(manifest)
|
||||
|
||||
if (isPlaylist) {
|
||||
let playlist = manifest
|
||||
|
||||
if (playlist.length === 0) {
|
||||
this.console.warn(`[PLAYER] Playlist is empty, aborting...`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (playlist.some((item) => typeof item === "string")) {
|
||||
this.console.log("Resolving missing manifests by ids...")
|
||||
playlist = await this.getTracksByIds(playlist)
|
||||
}
|
||||
|
||||
playlist = playlist.slice(startIndex)
|
||||
|
||||
for await (const [index, _manifest] of playlist.entries()) {
|
||||
const instance = await this.createInstance(_manifest)
|
||||
|
||||
this.track_next_instances.push(instance)
|
||||
|
||||
if (index === 0) {
|
||||
this.play(this.track_next_instances[0], {
|
||||
time: time ?? 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
const instance = await this.createInstance(manifest)
|
||||
|
||||
this.track_next_instances.push(instance)
|
||||
|
||||
this.play(this.track_next_instances[0], {
|
||||
time: time ?? 0
|
||||
})
|
||||
|
||||
return manifest
|
||||
}
|
||||
|
||||
next({ sync = false } = {}) {
|
||||
if (this.state.control_locked && !sync) {
|
||||
//this.console.warn("Sync mode is locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.track_next_instances.length > 0) {
|
||||
// move current audio instance to history
|
||||
this.track_prev_instances.push(this.track_next_instances.shift())
|
||||
}
|
||||
|
||||
if (this.track_next_instances.length === 0) {
|
||||
this.console.log(`[PLAYER] No more tracks to play, stopping...`)
|
||||
|
||||
return this.stop()
|
||||
}
|
||||
|
||||
let nextIndex = 0
|
||||
|
||||
if (this.state.playback_mode === "shuffle") {
|
||||
nextIndex = Math.floor(Math.random() * this.track_next_instances.length)
|
||||
}
|
||||
|
||||
this.play(this.track_next_instances[nextIndex])
|
||||
}
|
||||
|
||||
previous({ sync = false } = {}) {
|
||||
if (this.state.control_locked && !sync) {
|
||||
//this.console.warn("Sync mode is locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.track_prev_instances.length > 0) {
|
||||
// move current audio instance to history
|
||||
this.track_next_instances.unshift(this.track_prev_instances.pop())
|
||||
|
||||
return this.play(this.track_next_instances[0])
|
||||
}
|
||||
|
||||
if (this.track_prev_instances.length === 0) {
|
||||
this.console.log(`[PLAYER] No previous tracks, replying...`)
|
||||
// replay the current track
|
||||
return this.play(this.track_instance)
|
||||
}
|
||||
}
|
||||
|
||||
async togglePlayback() {
|
||||
if (this.state.playback_status === "paused") {
|
||||
await this.resumePlayback()
|
||||
} else {
|
||||
await this.pausePlayback()
|
||||
}
|
||||
}
|
||||
|
||||
async pausePlayback() {
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (!this.track_instance) {
|
||||
this.console.error("No audio instance")
|
||||
return null
|
||||
}
|
||||
|
||||
// set gain exponentially
|
||||
this.track_instance.gainNode.gain.linearRampToValueAtTime(
|
||||
0.0001,
|
||||
this.audioContext.currentTime + (Player.gradualFadeMs / 1000)
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
this.track_instance.media.pause()
|
||||
resolve()
|
||||
}, Player.gradualFadeMs)
|
||||
|
||||
this.native_controls.updateIsPlaying(false)
|
||||
})
|
||||
}
|
||||
|
||||
async resumePlayback() {
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (!this.track_instance) {
|
||||
this.console.error("No audio instance")
|
||||
return null
|
||||
}
|
||||
|
||||
// ensure audio elemeto starts from 0 volume
|
||||
this.track_instance.gainNode.gain.value = 0.0001
|
||||
|
||||
this.track_instance.media.play().then(() => {
|
||||
resolve()
|
||||
})
|
||||
|
||||
// set gain exponentially
|
||||
this.track_instance.gainNode.gain.linearRampToValueAtTime(
|
||||
this.state.volume,
|
||||
this.audioContext.currentTime + (Player.gradualFadeMs / 1000)
|
||||
)
|
||||
|
||||
this.native_controls.updateIsPlaying(true)
|
||||
})
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.destroyCurrentInstance()
|
||||
this.abortPreloads()
|
||||
|
||||
this.state.playback_status = "stopped"
|
||||
this.state.track_manifest = null
|
||||
|
||||
this.state.livestream_mode = false
|
||||
|
||||
this.track_instance = null
|
||||
this.track_next_instances = []
|
||||
this.track_prev_instances = []
|
||||
|
||||
this.native_controls.destroy()
|
||||
}
|
||||
|
||||
mute(to) {
|
||||
if (app.isMobile && typeof to !== "boolean") {
|
||||
this.console.warn("Cannot mute on mobile")
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof to === "boolean") {
|
||||
this.state.muted = to
|
||||
this.track_instance.media.muted = to
|
||||
}
|
||||
|
||||
return this.state.muted
|
||||
}
|
||||
|
||||
volume(volume) {
|
||||
if (typeof volume !== "number") {
|
||||
return this.state.volume
|
||||
}
|
||||
|
||||
if (app.isMobile) {
|
||||
this.console.warn("Cannot change volume on mobile")
|
||||
return false
|
||||
}
|
||||
|
||||
if (volume > 1) {
|
||||
if (!app.cores.settings.get("player.allowVolumeOver100")) {
|
||||
volume = 1
|
||||
}
|
||||
}
|
||||
|
||||
if (volume < 0) {
|
||||
volume = 0
|
||||
}
|
||||
|
||||
this.state.volume = volume
|
||||
|
||||
if (this.track_instance) {
|
||||
if (this.track_instance.gainNode) {
|
||||
this.track_instance.gainNode.gain.value = this.state.volume
|
||||
}
|
||||
}
|
||||
|
||||
return this.state.volume
|
||||
}
|
||||
|
||||
seek(time, { sync = false } = {}) {
|
||||
if (!this.track_instance || !this.track_instance.media) {
|
||||
return false
|
||||
}
|
||||
|
||||
// if time not provided, return current time
|
||||
if (typeof time === "undefined") {
|
||||
return this.track_instance.media.currentTime
|
||||
}
|
||||
|
||||
if (this.state.control_locked && !sync) {
|
||||
this.console.warn("Sync mode is locked, cannot do this action")
|
||||
return false
|
||||
}
|
||||
|
||||
// if time is provided, seek to that time
|
||||
if (typeof time === "number") {
|
||||
this.track_instance.media.currentTime = time
|
||||
|
||||
return time
|
||||
}
|
||||
}
|
||||
|
||||
playbackMode(mode) {
|
||||
if (typeof mode !== "string") {
|
||||
return this.state.playback_mode
|
||||
}
|
||||
|
||||
this.state.playback_mode = mode
|
||||
}
|
||||
|
||||
duration() {
|
||||
if (!this.track_instance) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.track_instance.media.duration
|
||||
}
|
||||
|
||||
loop(to) {
|
||||
if (typeof to !== "boolean") {
|
||||
this.console.warn("Loop must be a boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
this.state.loop = to ?? !this.state.loop
|
||||
|
||||
if (this.track_instance.media) {
|
||||
this.track_instance.media.loop = this.state.loop
|
||||
}
|
||||
|
||||
return this.state.loop
|
||||
}
|
||||
|
||||
close() {
|
||||
this.stop()
|
||||
this.detachPlayerComponent()
|
||||
}
|
||||
|
||||
toggleMinimize(to) {
|
||||
this.state.minimized = to ?? !this.state.minimized
|
||||
|
||||
if (this.state.minimized) {
|
||||
app.layout.sidebar.attachBottomItem("player", BackgroundMediaPlayer, {
|
||||
noContainer: true
|
||||
})
|
||||
} else {
|
||||
app.layout.sidebar.removeBottomItem("player")
|
||||
}
|
||||
|
||||
return this.state.minimized
|
||||
}
|
||||
|
||||
toggleCollapse(to) {
|
||||
if (typeof to !== "boolean") {
|
||||
this.console.warn("Collapse must be a boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
this.state.collapsed = to ?? !this.state.collapsed
|
||||
|
||||
return this.state.collapsed
|
||||
}
|
||||
|
||||
toggleSyncMode(to, lock) {
|
||||
if (typeof to !== "boolean") {
|
||||
this.console.warn("Sync mode must be a boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
this.state.syncMode = to ?? !this.state.syncMode
|
||||
|
||||
this.state.syncModeLocked = lock ?? false
|
||||
|
||||
this.console.log(`Sync mode is now ${this.state.syncMode ? "enabled" : "disabled"} | Locked: ${this.state.syncModeLocked ? "yes" : "no"}`)
|
||||
|
||||
return this.state.syncMode
|
||||
}
|
||||
|
||||
toggleMute(to) {
|
||||
if (typeof to !== "boolean") {
|
||||
to = !this.state.muted
|
||||
}
|
||||
|
||||
return this.mute(to)
|
||||
}
|
||||
|
||||
async getTracksByIds(list) {
|
||||
if (!Array.isArray(list)) {
|
||||
this.console.warn("List must be an array")
|
||||
return false
|
||||
}
|
||||
|
||||
let ids = []
|
||||
|
||||
list.forEach((item) => {
|
||||
if (typeof item === "string") {
|
||||
ids.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
if (ids.length === 0) {
|
||||
return list
|
||||
}
|
||||
|
||||
const fetchedTracks = await PlaylistModel.getTracks(ids).catch((err) => {
|
||||
this.console.error(err)
|
||||
return false
|
||||
})
|
||||
|
||||
if (!fetchedTracks) {
|
||||
return list
|
||||
}
|
||||
|
||||
// replace fetched tracks with the ones in the list
|
||||
fetchedTracks.forEach((fetchedTrack) => {
|
||||
const index = list.findIndex((item) => item === fetchedTrack._id)
|
||||
|
||||
if (index !== -1) {
|
||||
list[index] = fetchedTrack
|
||||
}
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
async setSampleRate(to) {
|
||||
// must be a integer
|
||||
if (typeof to !== "number") {
|
||||
this.console.error("Sample rate must be a number")
|
||||
return this.audioContext.sampleRate
|
||||
}
|
||||
|
||||
// must be a integer
|
||||
if (!Number.isInteger(to)) {
|
||||
this.console.error("Sample rate must be a integer")
|
||||
return this.audioContext.sampleRate
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
app.confirm({
|
||||
title: "Change sample rate",
|
||||
content: `To change the sample rate, the app needs to be reloaded. Do you want to continue?`,
|
||||
onOk: () => {
|
||||
try {
|
||||
this.audioContext = new AudioContext({ sampleRate: to })
|
||||
|
||||
AudioPlayerStorage.set("sample_rate", to)
|
||||
|
||||
app.navigation.reload()
|
||||
|
||||
return resolve(this.audioContext.sampleRate)
|
||||
} catch (error) {
|
||||
app.message.error(`Failed to change sample rate, ${error.message}`)
|
||||
return resolve(this.audioContext.sampleRate)
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
return resolve(this.audioContext.sampleRate)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
25
packages/app/src/cores/playerv2/player.storage.js
Normal file
25
packages/app/src/cores/playerv2/player.storage.js
Normal file
@ -0,0 +1,25 @@
|
||||
import store from "store"
|
||||
|
||||
export default class AudioPlayerStorage {
|
||||
static storeKey = "audioPlayer"
|
||||
|
||||
static get(key) {
|
||||
const data = store.get(AudioPlayerStorage.storeKey)
|
||||
|
||||
if (data) {
|
||||
return data[key]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
static set(key, value) {
|
||||
const data = store.get(AudioPlayerStorage.storeKey) ?? {}
|
||||
|
||||
data[key] = value
|
||||
|
||||
store.set(AudioPlayerStorage.storeKey, data)
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
79
packages/app/src/cores/playerv2/processors/bpmNode/index.js
Normal file
79
packages/app/src/cores/playerv2/processors/bpmNode/index.js
Normal file
@ -0,0 +1,79 @@
|
||||
import ProcessorNode from "../node"
|
||||
import { createRealTimeBpmProcessor } from "realtime-bpm-analyzer"
|
||||
import { Observable } from "object-observer"
|
||||
|
||||
export default class BPMProcessorNode extends ProcessorNode {
|
||||
static refName = "bpm"
|
||||
|
||||
static node_bypass = true
|
||||
|
||||
static lock = true
|
||||
|
||||
state = Observable.from({
|
||||
bpm: 0,
|
||||
average_bpm: 0,
|
||||
current_stable_bpm: 0,
|
||||
})
|
||||
|
||||
exposeToPublic = {
|
||||
state: this.state,
|
||||
}
|
||||
|
||||
async init() {
|
||||
Observable.observe(this.state, async (changes) => {
|
||||
try {
|
||||
changes.forEach((change) => {
|
||||
if (change.type === "update") {
|
||||
const stateKey = change.path[0]
|
||||
|
||||
if (stateKey === "bpm") {
|
||||
console.log("bpm update", this.state.bpm)
|
||||
this.PlayerCore.eventBus.emit(`bpm.change`, this.state.bpm)
|
||||
}
|
||||
|
||||
this.PlayerCore.eventBus.emit(`bpm.state.update:${stateKey}`, change.object[stateKey])
|
||||
this.PlayerCore.eventBus.emit("bpm.state.update", change.object)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Failed to dispatch state updater >`, error)
|
||||
}
|
||||
})
|
||||
|
||||
this.processor = await createRealTimeBpmProcessor(this.audioContext)
|
||||
|
||||
this.processor.port.postMessage({
|
||||
message: "ASYNC_CONFIGURATION",
|
||||
parameters: {
|
||||
continuousAnalysis: true,
|
||||
stabilizationTime: 5_000,
|
||||
}
|
||||
})
|
||||
|
||||
this.processor.port.onmessage = (event) => {
|
||||
if (event.data.message === "BPM") {
|
||||
const average_bpm = event.data.result.bpm[0]?.tempo ?? 0
|
||||
|
||||
if (average_bpm !== this.state.average_bpm) {
|
||||
this.state.average_bpm = average_bpm
|
||||
|
||||
if (average_bpm > 0) {
|
||||
if (this.state.stable_bpm < average_bpm) {
|
||||
this.state.bpm = this.state.average_bpm
|
||||
} else {
|
||||
this.state.bpm = this.state.stable_bpm
|
||||
}
|
||||
} else if (this.state.stable_bpm > 0) {
|
||||
this.state.bpm = this.state.stable_bpm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.message === "BPM_STABLE") {
|
||||
const stable_bpm = event.data.result.bpm[0]?.tempo ?? 0
|
||||
|
||||
this.state.stable_bpm = stable_bpm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import AudioPlayerStorage from "../../player.storage"
|
||||
import ProcessorNode from "../node"
|
||||
|
||||
export default class CompressorProcessorNode extends ProcessorNode {
|
||||
static refName = "compressor"
|
||||
static dependsOnSettings = ["player.compressor"]
|
||||
static defaultCompressorValues = {
|
||||
threshold: -50,
|
||||
knee: 40,
|
||||
ratio: 12,
|
||||
attack: 0.003,
|
||||
release: 0.25,
|
||||
}
|
||||
|
||||
state = {
|
||||
compressorValues: AudioPlayerStorage.get("compressor") ?? CompressorProcessorNode.defaultCompressorValues,
|
||||
}
|
||||
|
||||
exposeToPublic = {
|
||||
modifyValues: function (values) {
|
||||
this.state.compressorValues = {
|
||||
...this.state.compressorValues,
|
||||
...values,
|
||||
}
|
||||
|
||||
AudioPlayerStorage.set("compressor", this.state.compressorValues)
|
||||
|
||||
this.applyValues()
|
||||
}.bind(this),
|
||||
resetDefaultValues: function () {
|
||||
this.exposeToPublic.modifyValues(CompressorProcessorNode.defaultCompressorValues)
|
||||
|
||||
return this.state.compressorValues
|
||||
}.bind(this),
|
||||
detach: this._detach.bind(this),
|
||||
attach: this._attach.bind(this),
|
||||
values: this.state.compressorValues,
|
||||
}
|
||||
|
||||
async init(AudioContext) {
|
||||
if (!AudioContext) {
|
||||
throw new Error("AudioContext is required")
|
||||
}
|
||||
|
||||
this.processor = AudioContext.createDynamicsCompressor()
|
||||
|
||||
this.applyValues()
|
||||
}
|
||||
|
||||
applyValues() {
|
||||
Object.keys(this.state.compressorValues).forEach((key) => {
|
||||
this.processor[key].value = this.state.compressorValues[key]
|
||||
})
|
||||
}
|
||||
}
|
131
packages/app/src/cores/playerv2/processors/eqNode/index.js
Normal file
131
packages/app/src/cores/playerv2/processors/eqNode/index.js
Normal file
@ -0,0 +1,131 @@
|
||||
import ProcessorNode from "../node"
|
||||
import AudioPlayerStorage from "../../player.storage"
|
||||
|
||||
export default class EqProcessorNode extends ProcessorNode {
|
||||
static refName = "eq"
|
||||
static lock = true
|
||||
|
||||
static defaultEqValue = {
|
||||
32: {
|
||||
gain: 0,
|
||||
},
|
||||
64: {
|
||||
gain: 0,
|
||||
},
|
||||
125: {
|
||||
gain: 0,
|
||||
},
|
||||
250: {
|
||||
gain: 0,
|
||||
},
|
||||
500: {
|
||||
gain: 0,
|
||||
},
|
||||
1000: {
|
||||
gain: 0,
|
||||
},
|
||||
2000: {
|
||||
gain: 0,
|
||||
},
|
||||
4000: {
|
||||
gain: 0,
|
||||
},
|
||||
8000: {
|
||||
gain: 0,
|
||||
},
|
||||
16000: {
|
||||
gain: 0,
|
||||
}
|
||||
}
|
||||
|
||||
state = {
|
||||
eqValues: AudioPlayerStorage.get("eq_values") ?? EqProcessorNode.defaultEqValue,
|
||||
}
|
||||
|
||||
exposeToPublic = {
|
||||
modifyValues: function (values) {
|
||||
Object.keys(values).forEach((key) => {
|
||||
if (isNaN(key)) {
|
||||
delete values[key]
|
||||
}
|
||||
})
|
||||
|
||||
this.state.eqValues = {
|
||||
...this.state.eqValues,
|
||||
...values,
|
||||
}
|
||||
|
||||
AudioPlayerStorage.set("eq_values", this.state.eqValues)
|
||||
|
||||
this.applyValues()
|
||||
}.bind(this),
|
||||
resetDefaultValues: function () {
|
||||
this.exposeToPublic.modifyValues(EqProcessorNode.defaultEqValue)
|
||||
|
||||
return this.state
|
||||
}.bind(this),
|
||||
values: () => this.state,
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.audioContext) {
|
||||
throw new Error("audioContext is required")
|
||||
}
|
||||
|
||||
this.processor = this.audioContext.createGain()
|
||||
|
||||
this.processor.gain.value = 1
|
||||
|
||||
this.processor.eqNodes = []
|
||||
|
||||
const values = Object.entries(this.state.eqValues).map((entry) => {
|
||||
return {
|
||||
freq: parseFloat(entry[0]),
|
||||
gain: parseFloat(entry[1].gain),
|
||||
}
|
||||
})
|
||||
|
||||
values.forEach((eqValue, index) => {
|
||||
// chekc if freq and gain is valid
|
||||
if (isNaN(eqValue.freq)) {
|
||||
eqValue.freq = 0
|
||||
}
|
||||
if (isNaN(eqValue.gain)) {
|
||||
eqValue.gain = 0
|
||||
}
|
||||
|
||||
this.processor.eqNodes[index] = this.audioContext.createBiquadFilter()
|
||||
this.processor.eqNodes[index].type = "peaking"
|
||||
this.processor.eqNodes[index].frequency.value = eqValue.freq
|
||||
this.processor.eqNodes[index].gain.value = eqValue.gain
|
||||
})
|
||||
|
||||
// connect nodes
|
||||
for await (let [index, eqNode] of this.processor.eqNodes.entries()) {
|
||||
const nextNode = this.processor.eqNodes[index + 1]
|
||||
|
||||
if (index === 0) {
|
||||
this.processor.connect(eqNode)
|
||||
}
|
||||
|
||||
if (nextNode) {
|
||||
eqNode.connect(nextNode)
|
||||
}
|
||||
}
|
||||
|
||||
// set last processor for processor node can properly connect to the next node
|
||||
this.processor._last = this.processor.eqNodes.at(-1)
|
||||
}
|
||||
|
||||
applyValues() {
|
||||
// apply to current instance
|
||||
this.processor.eqNodes.forEach((processor) => {
|
||||
const gainValue = this.state.eqValues[processor.frequency.value].gain
|
||||
|
||||
if (processor.gain.value !== gainValue) {
|
||||
this.console.debug(`[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${gainValue}`)
|
||||
processor.gain.value = this.state.eqValues[processor.frequency.value].gain
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
60
packages/app/src/cores/playerv2/processors/gainNode/index.js
Normal file
60
packages/app/src/cores/playerv2/processors/gainNode/index.js
Normal file
@ -0,0 +1,60 @@
|
||||
import AudioPlayerStorage from "../../player.storage"
|
||||
import ProcessorNode from "../node"
|
||||
|
||||
export default class GainProcessorNode extends ProcessorNode {
|
||||
static refName = "gain"
|
||||
|
||||
static lock = true
|
||||
|
||||
static defaultValues = {
|
||||
gain: 1,
|
||||
}
|
||||
|
||||
state = {
|
||||
gain: AudioPlayerStorage.get("gain") ?? GainProcessorNode.defaultValues.gain,
|
||||
}
|
||||
|
||||
exposeToPublic = {
|
||||
modifyValues: function (values) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...values,
|
||||
}
|
||||
|
||||
AudioPlayerStorage.set("gain", this.state.gain)
|
||||
|
||||
this.applyValues()
|
||||
}.bind(this),
|
||||
resetDefaultValues: function () {
|
||||
this.exposeToPublic.modifyValues(GainProcessorNode.defaultValues)
|
||||
|
||||
return this.state
|
||||
}.bind(this),
|
||||
values: () => this.state,
|
||||
}
|
||||
|
||||
applyValues() {
|
||||
// apply to current instance
|
||||
this.processor.gain.value = app.cores.player.volume() * this.state.gain
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.audioContext) {
|
||||
throw new Error("audioContext is required")
|
||||
}
|
||||
|
||||
this.processor = this.audioContext.createGain()
|
||||
|
||||
this.applyValues()
|
||||
}
|
||||
|
||||
mutateInstance(instance) {
|
||||
if (!instance) {
|
||||
throw new Error("instance is required")
|
||||
}
|
||||
|
||||
instance.gainNode = this.processor
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
11
packages/app/src/cores/playerv2/processors/index.js
Normal file
11
packages/app/src/cores/playerv2/processors/index.js
Normal file
@ -0,0 +1,11 @@
|
||||
import EqProcessorNode from "./eqNode"
|
||||
import GainProcessorNode from "./gainNode"
|
||||
import CompressorProcessorNode from "./compressorNode"
|
||||
import BPMProcessorNode from "./bpmNode"
|
||||
|
||||
export default [
|
||||
BPMProcessorNode,
|
||||
EqProcessorNode,
|
||||
GainProcessorNode,
|
||||
CompressorProcessorNode,
|
||||
]
|
172
packages/app/src/cores/playerv2/processors/node.js
Normal file
172
packages/app/src/cores/playerv2/processors/node.js
Normal file
@ -0,0 +1,172 @@
|
||||
export default class ProcessorNode {
|
||||
constructor(PlayerCore) {
|
||||
if (!PlayerCore) {
|
||||
throw new Error("PlayerCore is required")
|
||||
}
|
||||
|
||||
this.PlayerCore = PlayerCore
|
||||
this.audioContext = PlayerCore.audioContext
|
||||
}
|
||||
|
||||
async _init() {
|
||||
// check if has init method
|
||||
if (typeof this.init === "function") {
|
||||
await this.init(this.audioContext)
|
||||
}
|
||||
|
||||
// check if has declared bus events
|
||||
if (typeof this.busEvents === "object") {
|
||||
Object.entries(this.busEvents).forEach((event, fn) => {
|
||||
app.eventBus.on(event, fn)
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof this.processor._last === "undefined") {
|
||||
this.processor._last = this.processor
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
_attach(instance, index) {
|
||||
if (typeof instance !== "object") {
|
||||
instance = this.PlayerCore.currentAudioInstance
|
||||
}
|
||||
|
||||
// check if has dependsOnSettings
|
||||
if (Array.isArray(this.constructor.dependsOnSettings)) {
|
||||
// check if the instance has the settings
|
||||
if (!this.constructor.dependsOnSettings.every((setting) => app.cores.settings.get(setting))) {
|
||||
console.warn(`Skipping attachment for [${this.constructor.refName ?? this.constructor.name}] node, cause is not passing the settings dependecy > ${this.constructor.dependsOnSettings.join(", ")}`)
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
// if index is not defined, attach to the last node
|
||||
if (!index) {
|
||||
index = instance.attachedProcessors.length
|
||||
}
|
||||
|
||||
const prevNode = instance.attachedProcessors[index - 1]
|
||||
const nextNode = instance.attachedProcessors[index + 1]
|
||||
|
||||
const currentIndex = this._findIndex(instance)
|
||||
|
||||
// check if is already attached
|
||||
if (currentIndex !== false) {
|
||||
console.warn(`[${this.constructor.refName ?? this.constructor.name}] node is already attached`)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
// first check if has prevNode and if is connected to something
|
||||
// if has, disconnect it
|
||||
// if it not has, its means that is the first node, so connect to the media source
|
||||
if (prevNode && prevNode.processor._last.numberOfOutputs > 0) {
|
||||
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is already attached to the previous node, disconnecting...`)
|
||||
// if has outputs, disconnect from the next node
|
||||
prevNode.processor._last.disconnect()
|
||||
|
||||
// now, connect to the processor
|
||||
prevNode.processor._last.connect(this.processor)
|
||||
} else {
|
||||
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is the first node, connecting to the media source...`)
|
||||
instance.track.connect(this.processor)
|
||||
}
|
||||
|
||||
// now, check if it has a next node
|
||||
// if has, connect to it
|
||||
// if not, connect to the destination
|
||||
if (nextNode) {
|
||||
this.processor.connect(nextNode.processor)
|
||||
}
|
||||
|
||||
// add to the attachedProcessors
|
||||
instance.attachedProcessors.splice(index, 0, this)
|
||||
|
||||
// handle instance mutation
|
||||
if (typeof this.mutateInstance === "function") {
|
||||
instance = this.mutateInstance(instance)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
_detach(instance) {
|
||||
if (typeof instance !== "object") {
|
||||
instance = this.PlayerCore.currentAudioInstance
|
||||
}
|
||||
|
||||
// find index of the node within the attachedProcessors serching for matching refName
|
||||
const index = this._findIndex(instance)
|
||||
|
||||
if (!index) {
|
||||
return instance
|
||||
}
|
||||
|
||||
// retrieve the previous and next nodes
|
||||
const prevNode = instance.attachedProcessors[index - 1]
|
||||
const nextNode = instance.attachedProcessors[index + 1]
|
||||
|
||||
// check if has previous node and if has outputs
|
||||
if (prevNode && prevNode.processor._last.numberOfOutputs > 0) {
|
||||
// if has outputs, disconnect from the previous node
|
||||
prevNode.processor._last.disconnect()
|
||||
}
|
||||
|
||||
// disconnect
|
||||
instance = this._destroy(instance)
|
||||
|
||||
// now, connect the previous node to the next node
|
||||
if (prevNode && nextNode) {
|
||||
prevNode.processor._last.connect(nextNode.processor)
|
||||
} else {
|
||||
// it means that this is the last node, so connect to the destination
|
||||
prevNode.processor._last.connect(this.audioContext.destination)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
_destroy(instance) {
|
||||
if (typeof instance !== "object") {
|
||||
instance = this.PlayerCore.currentAudioInstance
|
||||
}
|
||||
|
||||
const index = this._findIndex(instance)
|
||||
|
||||
if (!index) {
|
||||
return instance
|
||||
}
|
||||
|
||||
this.processor.disconnect()
|
||||
|
||||
instance.attachedProcessors.splice(index, 1)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
_findIndex(instance) {
|
||||
if (!instance) {
|
||||
instance = this.PlayerCore.currentAudioInstance
|
||||
}
|
||||
|
||||
if (!instance) {
|
||||
console.warn(`Instance is not defined`)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// find index of the node within the attachedProcessors serching for matching refName
|
||||
const index = instance.attachedProcessors.findIndex((node) => {
|
||||
return node.constructor.refName === this.constructor.refName
|
||||
})
|
||||
|
||||
if (index === -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import SyncModel from "comty.js/models/sync"
|
||||
|
||||
export default {
|
||||
"tidal": async (manifest) => {
|
||||
const resolvedManifest = await SyncModel.tidalCore.getTrackManifest(manifest.id)
|
||||
|
||||
manifest.source = resolvedManifest.playback.url
|
||||
|
||||
if (!manifest.metadata) {
|
||||
manifest.metadata = {}
|
||||
}
|
||||
|
||||
manifest.metadata.title = resolvedManifest.metadata.title
|
||||
manifest.metadata.artist = resolvedManifest.metadata.artists.map(artist => artist.name).join(", ")
|
||||
manifest.metadata.album = resolvedManifest.metadata.album.title
|
||||
|
||||
const coverUID = resolvedManifest.metadata.album.cover.replace(/-/g, "/")
|
||||
|
||||
manifest.metadata.cover = `https://resources.tidal.com/images/${coverUID}/1280x1280.jpg`
|
||||
|
||||
return manifest
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user