diff --git a/linebridge b/linebridge index ff38d45b..57d8b4be 160000 --- a/linebridge +++ b/linebridge @@ -1 +1 @@ -Subproject commit ff38d45b9686ccbd2e902477bde4cd7eb7d251e8 +Subproject commit 57d8b4bed14b0b35d1d9753847ac39710e0d9be5 diff --git a/packages/app/package.json b/packages/app/package.json index 30edaa89..8da89a79 100755 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@comty/app", - "version": "1.37.1@alpha", + "version": "1.38.0@alpha", "license": "ComtyLicense", "main": "electron/main", "type": "module", @@ -34,7 +34,7 @@ "bear-react-carousel": "^4.0.10-alpha.0", "classnames": "2.3.1", "comty.js": "^0.63.1", - "dashjs": "^4.7.4", + "dashjs": "^5.0.0", "dompurify": "^3.0.0", "fast-average-color": "^9.2.0", "fuse.js": "6.5.3", @@ -49,6 +49,7 @@ "moment": "2.29.4", "motion": "^12.4.2", "mpegts.js": "^1.6.10", + "music-metadata": "^11.2.1", "plyr": "^3.7.8", "prop-types": "^15.8.1", "qs": "^6.14.0", diff --git a/packages/app/src/classes/ChunkedUpload/index.js b/packages/app/src/classes/ChunkedUpload/index.js index 6d6b3441..5e93c5ab 100644 --- a/packages/app/src/classes/ChunkedUpload/index.js +++ b/packages/app/src/classes/ChunkedUpload/index.js @@ -170,7 +170,7 @@ export default class ChunkedUpload { // check if is the last chunk, if so, handle sse events if (this.chunkCount === this.totalChunks) { - if (data.sseChannelId || data.eventChannelURL) { + if (data.sseChannelId || data.sseUrl) { this.waitOnSSE(data) } else { this.events.emit("finish", data) @@ -178,9 +178,8 @@ export default class ChunkedUpload { } this.events.emit("progress", { - percentProgress: Math.round( - (100 / this.totalChunks) * this.chunkCount, - ), + percent: Math.round((100 / this.totalChunks) * this.chunkCount), + state: "Uploading", }) } catch (error) { this.events.emit("error", error) @@ -196,12 +195,9 @@ export default class ChunkedUpload { } waitOnSSE(data) { - console.log( - `[UPLOADER] Connecting to SSE channel >`, - data.eventChannelURL, - ) + console.log(`[UPLOADER] Connecting to SSE channel >`, data.sseUrl) - const eventSource = new EventSource(data.eventChannelURL) + const eventSource = new EventSource(data.sseUrl) eventSource.onerror = (error) => { this.events.emit("error", error) @@ -218,19 +214,20 @@ export default class ChunkedUpload { console.log(`[UPLOADER] SSE Event >`, messageData) - if (messageData.status === "done") { + if (messageData.event === "done") { this.events.emit("finish", messageData.result) eventSource.close() } - if (messageData.status === "error") { + if (messageData.event === "error") { this.events.emit("error", messageData.result) eventSource.close() } - if (messageData.status === "progress") { + if (messageData.state) { this.events.emit("progress", { - percentProgress: messageData.progress, + percent: messageData.percent, + state: messageData.state, }) } } diff --git a/packages/app/src/components/CoverEditor/index.jsx b/packages/app/src/components/CoverEditor/index.jsx index b86b8c4e..38d22cbe 100644 --- a/packages/app/src/components/CoverEditor/index.jsx +++ b/packages/app/src/components/CoverEditor/index.jsx @@ -7,55 +7,62 @@ import UploadButton from "@components/UploadButton" import "./index.less" const CoverEditor = (props) => { - const { value, onChange, defaultUrl } = props + const { value, onChange, defaultUrl } = props - const [init, setInit] = React.useState(true) - const [url, setUrl] = React.useState(value) + const [init, setInit] = React.useState(true) + const [url, setUrl] = React.useState(value) - React.useEffect(() => { - if (!init) { - onChange(url) - } - }, [url]) + React.useEffect(() => { + if (!init) { + onChange(url) + } + }, [url]) - React.useEffect(() => { - if (!value) { - setUrl(defaultUrl) - } else { - setUrl(value) - } + React.useEffect(() => { + if (!value) { + setUrl(defaultUrl) + } else { + setUrl(value) + } - setInit(false) - }, []) + setInit(false) + }, []) - return
-
- -
+ // Handle when value prop change + React.useEffect(() => { + if (!value) { + setUrl(defaultUrl) + } else { + setUrl(value) + } + }, [value]) -
- { - setUrl(response.url) - }} - /> + return ( +
+
+ +
- { - setUrl(defaultUrl) - }} - > - Reset - +
+ { + setUrl(response.url) + }} + /> - { - props.extraActions - } -
-
+ { + setUrl(defaultUrl) + }} + > + Reset + + + {props.extraActions} +
+
+ ) } export default CoverEditor diff --git a/packages/app/src/components/Music/PlaylistView/index.jsx b/packages/app/src/components/Music/PlaylistView/index.jsx index 096c81cf..a6cd9b20 100755 --- a/packages/app/src/components/Music/PlaylistView/index.jsx +++ b/packages/app/src/components/Music/PlaylistView/index.jsx @@ -392,9 +392,7 @@ const PlaylistView = (props) => { key={item._id} order={item._id} track={item} - onClickPlayBtn={() => - handleOnClickTrack(item) - } + onPlay={() => handleOnClickTrack(item)} changeState={(update) => handleTrackChangeState( item._id, @@ -418,7 +416,7 @@ const PlaylistView = (props) => { + onPlay={() => handleOnClickTrack(item) } changeState={(update) => diff --git a/packages/app/src/components/Music/Track/index.jsx b/packages/app/src/components/Music/Track/index.jsx index 5152704d..e185eb4b 100755 --- a/packages/app/src/components/Music/Track/index.jsx +++ b/packages/app/src/components/Music/Track/index.jsx @@ -52,6 +52,10 @@ const Track = (props) => { const isPlaying = isCurrent && playback_status === "playing" const handleClickPlayBtn = React.useCallback(() => { + if (typeof props.onPlay === "function") { + return props.onPlay(props.track) + } + if (typeof props.onClickPlayBtn === "function") { props.onClickPlayBtn(props.track) } diff --git a/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.jsx b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.jsx index f8907f5f..52020552 100644 --- a/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.jsx +++ b/packages/app/src/components/MusicStudio/EnhancedLyricsEditor/components/VideoEditor/index.jsx @@ -12,95 +12,96 @@ import "./index.less" dayjs.extend(customParseFormat) const VideoEditor = (props) => { - function handleChange(key, value) { - if (typeof props.onChange !== "function") { - return false - } + function handleChange(key, value) { + if (typeof props.onChange !== "function") { + return false + } - props.onChange(key, value) - } + props.onChange(key, value) + } - return
-

- - Video -

+ return ( +
+

+ + Video +

- { - (!props.videoSourceURL) && } - description="No video" - /> - } + {!props.videoSourceURL && ( + } + description="No video" + /> + )} - { - props.videoSourceURL &&
- -
- } + {props.videoSourceURL && ( +
+ +
+ )} -
-
- - - Start video sync at - +
+
+ + + Start video sync at + - {props.startSyncAt ?? "not set"} -
+ {props.startSyncAt ?? "not set"} +
-
- Set to: +
+ Set to: - { - handleChange("startSyncAt", str) - }} - /> -
-
+ { + handleChange("startSyncAt", str) + }} + /> +
+
-
- { - handleChange("videoSourceURL", response.url) - }} - accept={[ - "video/*", - ]} - headers={{ - "transmux": "mq-hls", - }} - disabled={props.loading} - > - Upload video - - - or - - { - handleChange("videoSourceURL", e.target.value) - }} - value={props.videoSourceURL} - disabled={props.loading} - /> -
-
+
+ { + handleChange("videoSourceURL", response.url) + }} + accept={["video/*"]} + headers={{ + transformations: "mq-hls", + }} + disabled={props.loading} + > + Upload video + + or + { + handleChange("videoSourceURL", e.target.value) + }} + value={props.videoSourceURL} + disabled={props.loading} + /> +
+
+ ) } -export default VideoEditor \ No newline at end of file +export default VideoEditor diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx b/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx index 2e5d604c..1aa38e05 100644 --- a/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx +++ b/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx @@ -1,280 +1,332 @@ import React from "react" import * as antd from "antd" - import { Icons, createIconRender } from "@components/Icons" import MusicModel from "@models/music" - +import compareObjectsByProperties from "@utils/compareObjectsByProperties" import useUrlQueryActiveKey from "@hooks/useUrlQueryActiveKey" import TrackManifest from "@cores/player/classes/TrackManifest" -import { DefaultReleaseEditorState, ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor" +import { + DefaultReleaseEditorState, + ReleaseEditorStateContext, +} from "@contexts/MusicReleaseEditor" import Tabs from "./tabs" import "./index.less" const ReleaseEditor = (props) => { - const { release_id } = props + const { release_id } = props - const basicInfoRef = React.useRef() + const basicInfoRef = React.useRef() - const [submitting, setSubmitting] = React.useState(false) - const [loading, setLoading] = React.useState(true) - const [submitError, setSubmitError] = React.useState(null) + const [submitting, setSubmitting] = React.useState(false) + const [loading, setLoading] = React.useState(true) + const [submitError, setSubmitError] = React.useState(null) - const [loadError, setLoadError] = React.useState(null) - const [globalState, setGlobalState] = React.useState(DefaultReleaseEditorState) + const [loadError, setLoadError] = React.useState(null) + const [globalState, setGlobalState] = React.useState( + DefaultReleaseEditorState, + ) + const [initialValues, setInitialValues] = React.useState({}) - const [customPage, setCustomPage] = React.useState(null) - const [customPageActions, setCustomPageActions] = React.useState([]) + const [customPage, setCustomPage] = React.useState(null) + const [customPageActions, setCustomPageActions] = React.useState([]) - const [selectedTab, setSelectedTab] = useUrlQueryActiveKey({ - defaultKey: "info", - queryKey: "tab" - }) + const [selectedTab, setSelectedTab] = useUrlQueryActiveKey({ + defaultKey: "info", + queryKey: "tab", + }) - async function initialize() { - setLoading(true) - setLoadError(null) + async function initialize() { + setLoading(true) + setLoadError(null) - if (release_id !== "new") { - try { - let releaseData = await MusicModel.getReleaseData(release_id) + if (release_id !== "new") { + try { + let releaseData = await MusicModel.getReleaseData(release_id) - if (Array.isArray(releaseData.list)) { - releaseData.list = releaseData.list.map((item) => { - return new TrackManifest(item) - }) - } + if (Array.isArray(releaseData.items)) { + releaseData.items = releaseData.items.map((item) => { + return new TrackManifest(item) + }) + } - setGlobalState({ - ...globalState, - ...releaseData, - }) - } catch (error) { - setLoadError(error) - } - } + setGlobalState({ + ...globalState, + ...releaseData, + }) - setLoading(false) - } + setInitialValues(releaseData) + } catch (error) { + setLoadError(error) + } + } - async function renderCustomPage(page, actions) { - setCustomPage(page ?? null) - setCustomPageActions(actions ?? []) - } + setLoading(false) + } - async function handleSubmit() { - setSubmitting(true) - setSubmitError(null) + function hasChanges() { + const stagedChanges = { + title: globalState.title, + type: globalState.type, + public: globalState.public, + cover: globalState.cover, + items: globalState.items, + } - try { - // first sumbit tracks - const tracks = await MusicModel.putTrack({ - list: globalState.list, - }) + return !compareObjectsByProperties( + stagedChanges, + initialValues, + Object.keys(stagedChanges), + ) + } - // then submit release - const result = await MusicModel.putRelease({ - _id: globalState._id, - title: globalState.title, - description: globalState.description, - public: globalState.public, - cover: globalState.cover, - explicit: globalState.explicit, - type: globalState.type, - list: tracks.list.map((item) => item._id), - }) + async function renderCustomPage(page, actions) { + setCustomPage(page ?? null) + setCustomPageActions(actions ?? []) + } - app.location.push(`/studio/music/${result._id}`) - } catch (error) { - console.error(error) - app.message.error(error.message) + async function handleSubmit() { + setSubmitting(true) + setSubmitError(null) - setSubmitError(error) - setSubmitting(false) + try { + console.log("Submitting Tracks") - return false - } + // first sumbit tracks + const tracks = await MusicModel.putTrack({ + items: globalState.items, + }) - setSubmitting(false) - app.message.success("Release saved") - } + console.log("Submitting release") - async function handleDelete() { - app.layout.modal.confirm({ - headerText: "Are you sure you want to delete this release?", - descriptionText: "This action cannot be undone.", - onConfirm: async () => { - await MusicModel.deleteRelease(globalState._id) - app.location.push(window.location.pathname.split("/").slice(0, -1).join("/")) - }, - }) - } + // then submit release + const result = await MusicModel.putRelease({ + _id: globalState._id, + title: globalState.title, + description: globalState.description, + public: globalState.public, + cover: globalState.cover, + explicit: globalState.explicit, + type: globalState.type, + items: tracks.items.map((item) => item._id), + }) - async function canFinish() { - return true - } + app.location.push(`/studio/music/${result._id}`) + } catch (error) { + console.error(error) + app.message.error(error.message) - React.useEffect(() => { - initialize() - }, []) + setSubmitError(error) + setSubmitting(false) - if (loadError) { - return - } + return false + } - if (loading) { - return - } + setSubmitting(false) + app.message.success("Release saved") + } - const Tab = Tabs.find(({ key }) => key === selectedTab) + async function handleDelete() { + app.layout.modal.confirm({ + headerText: "Are you sure you want to delete this release?", + descriptionText: "This action cannot be undone.", + onConfirm: async () => { + await MusicModel.deleteRelease(globalState._id) + app.location.push( + window.location.pathname.split("/").slice(0, -1).join("/"), + ) + }, + }) + } - const CustomPageProps = { - close: () => { - renderCustomPage(null, null) - } - } + function canFinish() { + return hasChanges() + } - return -
- { - customPage &&
- { - customPage.header &&
-
- } - onClick={() => renderCustomPage(null, null)} - /> + React.useEffect(() => { + initialize() + }, []) -

{customPage.header}

-
+ if (loadError) { + return ( + + ) + } - { - Array.isArray(customPageActions) && customPageActions.map((action, index) => { - return { - if (typeof action.onClick === "function") { - await action.onClick() - } + if (loading) { + return + } - if (action.fireEvent) { - app.eventBus.emit(action.fireEvent) - } - }} - disabled={action.disabled} - > - {action.label} - - }) - } -
- } + const Tab = Tabs.find(({ key }) => key === selectedTab) - { - customPage.content && (React.isValidElement(customPage.content) ? - React.cloneElement(customPage.content, { - ...CustomPageProps, - ...customPage.props - }) : - React.createElement(customPage.content, { - ...CustomPageProps, - ...customPage.props - }) - ) - } -
- } - { - !customPage && <> -
- setSelectedTab(e.key)} - selectedKeys={[selectedTab]} - items={Tabs} - mode="vertical" - /> + const CustomPageProps = { + close: () => { + renderCustomPage(null, null) + }, + } -
- : } - disabled={submitting || loading || !canFinish()} - loading={submitting} - > - {release_id !== "new" ? "Save" : "Release"} - + return ( + +
+ {customPage && ( +
+ {customPage.header && ( +
+
+ } + onClick={() => + renderCustomPage(null, null) + } + /> - { - release_id !== "new" ? } - disabled={loading} - onClick={handleDelete} - > - Delete - : null - } +

{customPage.header}

+
- { - release_id !== "new" ? } - onClick={() => app.location.push(`/music/release/${globalState._id}`)} - > - Go to release - : null - } -
-
+ {Array.isArray(customPageActions) && + customPageActions.map((action, index) => { + return ( + { + if ( + typeof action.onClick === + "function" + ) { + await action.onClick() + } -
- { - submitError && - } - { - !Tab && - } - { - Tab && React.createElement(Tab.render, { - release: globalState, + if (action.fireEvent) { + app.eventBus.emit( + action.fireEvent, + ) + } + }} + disabled={action.disabled} + > + {action.label} + + ) + })} +
+ )} - state: globalState, - setState: setGlobalState, + {customPage.content && + (React.isValidElement(customPage.content) + ? React.cloneElement(customPage.content, { + ...CustomPageProps, + ...customPage.props, + }) + : React.createElement(customPage.content, { + ...CustomPageProps, + ...customPage.props, + }))} +
+ )} + {!customPage && ( + <> +
+ setSelectedTab(e.key)} + selectedKeys={[selectedTab]} + items={Tabs} + mode="vertical" + /> - references: { - basic: basicInfoRef - } - }) - } -
- - } -
- +
+ + ) : ( + + ) + } + disabled={ + submitting || loading || !canFinish() + } + loading={submitting} + > + {release_id !== "new" ? "Save" : "Release"} + + + {release_id !== "new" ? ( + } + disabled={loading} + onClick={handleDelete} + > + Delete + + ) : null} + + {release_id !== "new" ? ( + } + onClick={() => + app.location.push( + `/music/release/${globalState._id}`, + ) + } + > + Go to release + + ) : null} +
+
+ +
+ {submitError && ( + + )} + {!Tab && ( + + )} + {Tab && + React.createElement(Tab.render, { + release: globalState, + + state: globalState, + setState: setGlobalState, + + references: { + basic: basicInfoRef, + }, + })} +
+ + )} +
+
+ ) } -export default ReleaseEditor \ No newline at end of file +export default ReleaseEditor diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/components/TrackListItem/index.jsx b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/components/TrackListItem/index.jsx index 38a6c0ef..23f4fcec 100644 --- a/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/components/TrackListItem/index.jsx +++ b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/components/TrackListItem/index.jsx @@ -11,13 +11,27 @@ import { ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor" import "./index.less" +const stateToString = { + uploading: "Uploading", + transmuxing: "Processing...", + uploading_s3: "Archiving...", +} + +const getTitleString = ({ track, progress }) => { + if (progress) { + return stateToString[progress.state] || progress.state + } + + return track.title +} + const TrackListItem = (props) => { const context = React.useContext(ReleaseEditorStateContext) const [loading, setLoading] = React.useState(false) const [error, setError] = React.useState(null) - const { track } = props + const { track, progress } = props async function onClickEditTrack() { context.renderCustomPage({ @@ -33,8 +47,6 @@ const TrackListItem = (props) => { props.onDelete(track.uid) } - console.log("render") - return (
{
@@ -58,7 +70,7 @@ const TrackListItem = (props) => { {props.index + 1}
- {props.uploading.working && } + {progress !== null && } { }} /> - {track.title} + {getTitleString({ track, progress })}
{ - if (prevState.list !== this.state.list) { + if (prevState.items !== this.state.items) { if (typeof this.props.onChangeState === "function") { this.props.onChangeState(this.state) } @@ -55,7 +55,7 @@ class TracksManager extends React.Component { return false } - return this.state.list.find((item) => item.uid === uid) + return this.state.items.find((item) => item.uid === uid) } addTrackToList = (track) => { @@ -64,7 +64,7 @@ class TracksManager extends React.Component { } this.setState({ - list: [...this.state.list, track], + items: [...this.state.items, track], }) } @@ -76,18 +76,17 @@ class TracksManager extends React.Component { this.removeTrackUIDFromPendingUploads(uid) this.setState({ - list: this.state.list.filter((item) => item.uid !== uid), + items: this.state.items.filter((item) => item.uid !== uid), }) } modifyTrackByUid = (uid, track) => { - console.log("modifyTrackByUid", uid, track) if (!uid || !track) { return false } this.setState({ - list: this.state.list.map((item) => { + items: this.state.items.map((item) => { if (item.uid === uid) { return { ...item, @@ -140,7 +139,7 @@ class TracksManager extends React.Component { ) if (uploadProgressIndex === -1) { - return 0 + return null } return this.state.pendingUploads[uploadProgressIndex].progress @@ -159,7 +158,7 @@ class TracksManager extends React.Component { newData[uploadProgressIndex].progress = progress - console.log(`Updating progress for [${uid}] to [${progress}]`) + console.log(`Updating progress for [${uid}] to >`, progress) this.setState({ pendingUploads: newData, @@ -177,8 +176,7 @@ class TracksManager extends React.Component { const trackManifest = new TrackManifest({ uid: uid, - file: change.file, - onChange: this.modifyTrackByUid, + file: change.file.originFileObj, }) this.addTrackToList(trackManifest) @@ -189,7 +187,7 @@ class TracksManager extends React.Component { // remove pending file this.removeTrackUIDFromPendingUploads(uid) - let trackManifest = this.state.list.find( + let trackManifest = this.state.items.find( (item) => item.uid === uid, ) @@ -206,6 +204,23 @@ class TracksManager extends React.Component { trackManifest.source = change.file.response.url trackManifest = await trackManifest.initialize() + // if has a cover, Upload + if (trackManifest._coverBlob) { + console.log( + `[${trackManifest.uid}] Founded cover, uploading...`, + ) + const coverFile = new File( + [trackManifest._coverBlob], + "cover.jpg", + { type: trackManifest._coverBlob.type }, + ) + + const coverUpload = + await app.cores.remoteStorage.uploadFile(coverFile) + + trackManifest.cover = coverUpload.url + } + await this.modifyTrackByUid(uid, trackManifest) break @@ -231,9 +246,8 @@ class TracksManager extends React.Component { const response = await app.cores.remoteStorage .uploadFile(req.file, { onProgress: this.handleTrackFileUploadProgress, - service: "b2", headers: { - transmux: "a-dash", + transformations: "a-dash", }, }) .catch((error) => { @@ -258,17 +272,17 @@ class TracksManager extends React.Component { this.setState((prev) => { // move all list items by id const orderedIds = orderedIdsArray.map((id) => - this.state.list.find((item) => item._id === id), + this.state.items.find((item) => item._id === id), ) console.log("orderedIds", orderedIds) return { - list: orderedIds, + items: orderedIds, } }) } render() { - console.log(`Tracks List >`, this.state.list) + console.log(`Tracks List >`, this.state.items) return (
@@ -280,7 +294,7 @@ class TracksManager extends React.Component { accept="audio/*" multiple > - {this.state.list.length === 0 ? ( + {this.state.items.length === 0 ? ( ) : ( - {this.state.list.length === 0 && ( + {this.state.items.length === 0 && ( )} - {this.state.list.map((track, index) => { + {this.state.items.map((track, index) => { const progress = this.getUploadProgress(track.uid) return ( @@ -310,12 +324,7 @@ class TracksManager extends React.Component { track={track} onEdit={this.modifyTrackByUid} onDelete={this.removeTrackByUid} - uploading={{ - progress: progress, - working: this.state.pendingUploads.find( - (item) => item.uid === track.uid, - ), - }} + progress={progress} disabled={progress > 0} />
@@ -336,7 +345,7 @@ const ReleaseTracks = (props) => { { setState({ ...state, diff --git a/packages/app/src/components/MusicStudio/TrackEditor/index.jsx b/packages/app/src/components/MusicStudio/TrackEditor/index.jsx index d73f48ba..9c5fab25 100644 --- a/packages/app/src/components/MusicStudio/TrackEditor/index.jsx +++ b/packages/app/src/components/MusicStudio/TrackEditor/index.jsx @@ -10,158 +10,163 @@ import { ReleaseEditorStateContext } from "@contexts/MusicReleaseEditor" import "./index.less" const TrackEditor = (props) => { - const context = React.useContext(ReleaseEditorStateContext) - const [track, setTrack] = React.useState(props.track ?? {}) + const context = React.useContext(ReleaseEditorStateContext) + const [track, setTrack] = React.useState(props.track ?? {}) - async function handleChange(key, value) { - setTrack((prev) => { - return { - ...prev, - [key]: value - } - }) - } + async function handleChange(key, value) { + setTrack((prev) => { + return { + ...prev, + [key]: value, + } + }) + } - async function openEnhancedLyricsEditor() { - context.renderCustomPage({ - header: "Enhanced Lyrics", - content: EnhancedLyricsEditor, - props: { - track: track, - } - }) - } + async function openEnhancedLyricsEditor() { + context.renderCustomPage({ + header: "Enhanced Lyrics", + content: EnhancedLyricsEditor, + props: { + track: track, + }, + }) + } - async function handleOnSave() { - setTrack((prev) => { - const listData = [...context.list] + async function handleOnSave() { + setTrack((prev) => { + const listData = [...context.items] - const trackIndex = listData.findIndex((item) => item.uid === prev.uid) + const trackIndex = listData.findIndex( + (item) => item.uid === prev.uid, + ) - if (trackIndex === -1) { - return prev - } + if (trackIndex === -1) { + return prev + } - listData[trackIndex] = prev + listData[trackIndex] = prev - context.setGlobalState({ - ...context, - list: listData - }) + context.setGlobalState({ + ...context, + items: listData, + }) - return prev - }) - } + props.close() - React.useEffect(() => { - context.setCustomPageActions([ - { - label: "Save", - icon: "FiSave", - type: "primary", - onClick: handleOnSave, - disabled: props.track === track, - }, - ]) - }, [track]) + return prev + }) + } - return
-
-
- - Cover -
+ function setParentCover() { + handleChange("cover", context.cover) + } - handleChange("cover", url)} - extraActions={[ - - Use Parent - - ]} - /> -
+ React.useEffect(() => { + context.setCustomPageActions([ + { + label: "Save", + icon: "FiSave", + type: "primary", + onClick: handleOnSave, + disabled: props.track === track, + }, + ]) + }, [track]) -
-
- - Title -
+ return ( +
+
+
+ + Cover +
- handleChange("title", e.target.value)} - /> -
+ handleChange("cover", url)} + extraActions={[ + + Use Parent + , + ]} + /> +
-
-
- - Artist -
+
+
+ + Title +
- handleChange("artist", e.target.value)} - /> -
+ handleChange("title", e.target.value)} + /> +
-
-
- - Album -
+
+
+ + Artist +
- handleChange("album", e.target.value)} - /> -
+ handleChange("artist", e.target.value)} + /> +
-
-
- - Explicit -
+
+
+ + Album +
- handleChange("explicit", value)} - /> -
+ handleChange("album", e.target.value)} + /> +
-
-
- - Enhanced Lyrics +
+
+ + Explicit +
- handleChange("lyrics_enabled", value)} - disabled={!track.params._id} - /> -
+ handleChange("explicit", value)} + /> +
-
- - Edit - +
+
+ + Enhanced Lyrics +
- { - !track.params._id && - You cannot edit Video and Lyrics without release first - - } -
-
-
+
+ + Edit + + + {!track.params._id && ( + + You cannot edit Video and Lyrics without release + first + + )} +
+
+
+ ) } -export default TrackEditor \ No newline at end of file +export default TrackEditor diff --git a/packages/app/src/components/Player/ExtraActions/index.jsx b/packages/app/src/components/Player/Actions/index.jsx similarity index 51% rename from packages/app/src/components/Player/ExtraActions/index.jsx rename to packages/app/src/components/Player/Actions/index.jsx index b3be128b..9b873395 100755 --- a/packages/app/src/components/Player/ExtraActions/index.jsx +++ b/packages/app/src/components/Player/Actions/index.jsx @@ -6,41 +6,51 @@ import LikeButton from "@components/LikeButton" import { usePlayerStateContext } from "@contexts/WithPlayerContext" +import "./index.less" + const ExtraActions = (props) => { - const [playerState] = usePlayerStateContext() + const [trackInstance, setTrackInstance] = React.useState({}) + + const onPlayerStateChange = React.useCallback((state) => { + const instance = app.cores.player.track() + + if (instance) { + setTrackInstance(instance) + } + }, []) + + const [playerState] = usePlayerStateContext(onPlayerStateChange) const handleClickLike = async () => { - if (!playerState.track_manifest) { + if (!trackInstance) { console.error("Cannot like a track if nothing is playing") return false } - const track = app.cores.player.track() - - await track.manifest.serviceOperations.toggleItemFavourite( + await trackInstance.manifest.serviceOperations.toggleItemFavourite( "track", - playerState.track_manifest._id, + trackInstance.manifest._id, ) } return ( -
+
{app.isMobile && (
diff --git a/packages/app/src/components/Player/ToolBarPlayer/index.jsx b/packages/app/src/components/Player/ToolBarPlayer/index.jsx index fac61db7..14f671ac 100755 --- a/packages/app/src/components/Player/ToolBarPlayer/index.jsx +++ b/packages/app/src/components/Player/ToolBarPlayer/index.jsx @@ -8,11 +8,10 @@ import { usePlayerStateContext } from "@contexts/WithPlayerContext" import LiveInfo from "@components/Player/LiveInfo" import SeekBar from "@components/Player/SeekBar" import Controls from "@components/Player/Controls" +import Actions from "@components/Player/Actions" import RGBStringToValues from "@utils/rgbToValues" -import ExtraActions from "../ExtraActions" - import "./index.less" function isOverflown(parent, element) { @@ -93,7 +92,7 @@ const Player = (props) => { } } - const { title, artistStr, service, cover_analysis, cover } = + const { title, artist, service, cover_analysis, cover } = playerState.track_manifest ?? {} const playing = playerState.playback_status === "playing" @@ -201,7 +200,7 @@ const Player = (props) => { )}

- {artistStr ?? ""} + {artist ?? ""}

@@ -218,7 +217,7 @@ const Player = (props) => { streamMode={playerState.live} /> - +
{ + if (this.state.loading === true) { + console.warn(`Please wait to load the post before load more`) + return + } + this.setState({ loading: true, }) let payload = { - trim: this.state.list.length, + page: this.state.pageCount, limit: app.cores.settings.get("feed_max_fetch"), } @@ -164,10 +170,6 @@ export class PostsListsComponent extends React.Component { } } - if (params.replace) { - payload.trim = 0 - } - const result = await fn(payload).catch((err) => { console.error(err) @@ -186,10 +188,12 @@ export class PostsListsComponent extends React.Component { if (params.replace) { this.setState({ list: result, + pageCount: 0, }) } else { this.setState({ list: [...this.state.list, ...result], + pageCount: this.state.pageCount + 1, }) } } diff --git a/packages/app/src/components/UploadButton/index.jsx b/packages/app/src/components/UploadButton/index.jsx index 0a7caee1..90ca59e4 100755 --- a/packages/app/src/components/UploadButton/index.jsx +++ b/packages/app/src/components/UploadButton/index.jsx @@ -7,112 +7,102 @@ import { Icons } from "@components/Icons" import "./index.less" export default (props) => { - const [uploading, setUploading] = React.useState(false) - const [progess, setProgess] = React.useState(null) + const [uploading, setUploading] = React.useState(false) + const [progress, setProgress] = React.useState(null) - const handleOnStart = (file_uid, file) => { - if (typeof props.onStart === "function") { - props.onStart(file_uid, file) - } - } + const handleOnStart = (file_uid, file) => { + if (typeof props.onStart === "function") { + props.onStart(file_uid, file) + } + } - const handleOnProgress = (file_uid, progress) => { - if (typeof props.onProgress === "function") { - props.onProgress(file_uid, progress) - } - } + const handleOnProgress = (file_uid, progress) => { + if (typeof props.onProgress === "function") { + props.onProgress(file_uid, progress) + } + } - const handleOnError = (file_uid, error) => { - if (typeof props.onError === "function") { - props.onError(file_uid, error) - } - } + const handleOnError = (file_uid, error) => { + if (typeof props.onError === "function") { + props.onError(file_uid, error) + } + } - const handleOnSuccess = (file_uid, response) => { - if (typeof props.onSuccess === "function") { - props.onSuccess(file_uid, response) - } - } + const handleOnSuccess = (file_uid, response) => { + if (typeof props.onSuccess === "function") { + props.onSuccess(file_uid, response) + } + } - const handleUpload = async (req) => { - setUploading(true) - setProgess(1) + const handleUpload = async (req) => { + setUploading(true) + setProgress(1) - handleOnStart(req.file.uid, req.file) + handleOnStart(req.file.uid, req.file) - await app.cores.remoteStorage.uploadFile(req.file, { - headers: props.headers, - onProgress: (file, progress) => { - setProgess(progress) - handleOnProgress(file.uid, progress) - }, - onError: (file, error) => { - setProgess(null) - handleOnError(file.uid, error) - setUploading(false) - }, - onFinish: (file, response) => { - if (typeof props.ctx?.onUpdateItem === "function") { - props.ctx.onUpdateItem(response.url) - } + await app.cores.remoteStorage.uploadFile(req.file, { + headers: props.headers, + onProgress: (file, progress) => { + setProgress(progress) + handleOnProgress(file.uid, progress) + }, + onError: (file, error) => { + setProgress(null) + handleOnError(file.uid, error) + setUploading(false) + }, + onFinish: (file, response) => { + if (typeof props.ctx?.onUpdateItem === "function") { + props.ctx.onUpdateItem(response.url) + } - if (typeof props.onUploadDone === "function") { - props.onUploadDone(response) - } + if (typeof props.onUploadDone === "function") { + props.onUploadDone(response) + } - setUploading(false) - handleOnSuccess(req.file.uid, response) + setUploading(false) + handleOnSuccess(req.file.uid, response) - setTimeout(() => { - setProgess(null) - }, 1000) - }, - }) - } + setTimeout(() => { + setProgress(null) + }, 1000) + }, + }) + } - return -
- { - !progess && (props.icon ?? ) - } + return ( + +
+ {!progress && + (props.icon ?? ( + + ))} - { - progess && null} - /> - } + {progress && ( + null} + /> + )} - { - props.children ?? "Upload" - } -
-
-} \ No newline at end of file + {props.children ?? "Upload"} +
+
+ ) +} diff --git a/packages/app/src/contexts/MusicReleaseEditor/index.js b/packages/app/src/contexts/MusicReleaseEditor/index.js index 75046ce7..9b327be7 100644 --- a/packages/app/src/contexts/MusicReleaseEditor/index.js +++ b/packages/app/src/contexts/MusicReleaseEditor/index.js @@ -1,17 +1,19 @@ import React from "react" export const DefaultReleaseEditorState = { - cover: null, - title: "Untitled", - type: "single", - public: false, + cover: null, + title: "Untitled", + type: "single", + public: false, - list: [], - pendingUploads: [], + items: [], + pendingUploads: [], - setCustomPage: () => {}, + setCustomPage: () => {}, } -export const ReleaseEditorStateContext = React.createContext(DefaultReleaseEditorState) +export const ReleaseEditorStateContext = React.createContext( + DefaultReleaseEditorState, +) -export default ReleaseEditorStateContext \ No newline at end of file +export default ReleaseEditorStateContext diff --git a/packages/app/src/cores/player/classes/AudioBase.js b/packages/app/src/cores/player/classes/AudioBase.js new file mode 100644 index 00000000..1818bfcf --- /dev/null +++ b/packages/app/src/cores/player/classes/AudioBase.js @@ -0,0 +1,123 @@ +import { MediaPlayer } from "dashjs" +import PlayerProcessors from "./PlayerProcessors" +import AudioPlayerStorage from "../player.storage" + +export default class AudioBase { + constructor(player) { + this.player = player + } + + audio = new Audio() + context = null + demuxer = null + elementSource = null + + processorsManager = new PlayerProcessors(this) + processors = {} + + waitUpdateTimeout = null + + initialize = async () => { + // create a audio context + this.context = new AudioContext({ + sampleRate: + AudioPlayerStorage.get("sample_rate") ?? + this.player.constructor.defaultSampleRate, + latencyHint: "playback", + }) + + // configure some settings for audio + this.audio.crossOrigin = "anonymous" + this.audio.preload = "metadata" + + // listen all events + for (const [key, value] of Object.entries(this.audioEvents)) { + this.audio.addEventListener(key, value) + } + + // setup demuxer for mpd + this.createDemuxer() + + // create element source + this.elementSource = this.context.createMediaElementSource(this.audio) + + // initialize audio processors + await this.processorsManager.initialize() + await this.processorsManager.attachAllNodes() + } + + createDemuxer() { + this.demuxer = MediaPlayer().create() + + this.demuxer.updateSettings({ + streaming: { + buffer: { + resetSourceBuffersForTrackSwitch: true, + }, + }, + }) + + this.demuxer.initialize(this.audio, null, false) + } + + flush() { + this.audio.pause() + this.audio.src = null + this.audio.currentTime = 0 + + this.demuxer.destroy() + this.createDemuxer() + } + + audioEvents = { + ended: () => { + this.player.next() + }, + loadeddata: () => { + this.player.state.loading = false + }, + loadedmetadata: () => { + if (this.audio.duration === Infinity) { + this.player.state.live = true + } else { + this.player.state.live = false + } + }, + play: () => { + this.player.state.playback_status = "playing" + }, + playing: () => { + this.player.state.loading = false + + this.player.state.playback_status = "playing" + + if (typeof this.waitUpdateTimeout !== "undefined") { + clearTimeout(this.waitUpdateTimeout) + this.waitUpdateTimeout = null + } + }, + pause: () => { + this.player.state.playback_status = "paused" + }, + durationchange: () => { + this.player.eventBus.emit( + `player.durationchange`, + this.audio.duration, + ) + }, + waiting: () => { + if (this.waitUpdateTimeout) { + clearTimeout(this.waitUpdateTimeout) + this.waitUpdateTimeout = null + } + + // if takes more than 150ms to load, update loading state + this.waitUpdateTimeout = setTimeout(() => { + this.player.state.loading = true + }, 150) + }, + seeked: () => { + this.player.eventBus.emit(`player.seeked`, this.audio.currentTime) + }, + } +} diff --git a/packages/app/src/cores/player/classes/MediaSession.js b/packages/app/src/cores/player/classes/MediaSession.js new file mode 100644 index 00000000..cd34fad7 --- /dev/null +++ b/packages/app/src/cores/player/classes/MediaSession.js @@ -0,0 +1,56 @@ +export default class MediaSession { + constructor(player) { + this.player = player + } + + async initialize() { + for (const [action, handler] of this.handlers) { + navigator.mediaSession.setActionHandler(action, handler) + } + } + + handlers = [ + [ + "play", + () => { + console.log("media session play event", "play") + this.player.resumePlayback() + }, + ], + [ + "pause", + () => { + console.log("media session pause event", "pause") + this.player.pausePlayback() + }, + ], + [ + "seekto", + (seek) => { + console.log("media session seek event", seek) + this.player.seek(seek.seekTime) + }, + ], + ] + + update = (manifest) => { + navigator.mediaSession.metadata = new MediaMetadata({ + title: manifest.title, + artist: manifest.artist, + album: manifest.album, + artwork: [ + { + src: manifest.cover, + }, + ], + }) + } + + flush = () => { + navigator.mediaSession.metadata = null + } + + updateIsPlaying = (isPlaying) => { + navigator.mediaSession.playbackState = isPlaying ? "playing" : "paused" + } +} diff --git a/packages/app/src/cores/player/classes/PlayerProcessors.js b/packages/app/src/cores/player/classes/PlayerProcessors.js index f59db0aa..fb982159 100644 --- a/packages/app/src/cores/player/classes/PlayerProcessors.js +++ b/packages/app/src/cores/player/classes/PlayerProcessors.js @@ -1,83 +1,96 @@ import defaultAudioProccessors from "../processors" export default class PlayerProcessors { - constructor(player) { - this.player = player - } + constructor(base) { + this.base = base + } - processors = [] + nodes = [] + attached = [] - public = {} + public = {} - async initialize() { - // if already exists audio processors, destroy all before create new - if (this.processors.length > 0) { - this.player.console.log("Destroying audio processors") + async initialize() { + // if already exists audio processors, destroy all before create new + if (this.nodes.length > 0) { + this.base.player.console.log("Destroying audio processors") - this.processors.forEach((processor) => { - this.player.console.log(`Destroying audio processor ${processor.constructor.name}`, processor) - processor._destroy() - }) + this.nodes.forEach((node) => { + this.base.player.console.log( + `Destroying audio processor node ${node.constructor.name}`, + node, + ) + node._destroy() + }) - this.processors = [] - } + this.nodes = [] + } - // instanciate default audio processors - for await (const defaultProccessor of defaultAudioProccessors) { - this.processors.push(new defaultProccessor(this.player)) - } + // instanciate default audio processors + for await (const defaultProccessor of defaultAudioProccessors) { + this.nodes.push(new defaultProccessor(this)) + } - // initialize audio processors - for await (const processor of this.processors) { - if (typeof processor._init === "function") { - try { - await processor._init(this.player.audioContext) - } catch (error) { - this.player.console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error) - continue - } - } + // initialize audio processors + for await (const node of this.nodes) { + if (typeof node._init === "function") { + try { + await node._init() + } catch (error) { + this.base.player.console.error( + `Failed to initialize audio processor node ${node.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 + // check if processor has exposed public methods + if (node.exposeToPublic) { + Object.entries(node.exposeToPublic).forEach(([key, value]) => { + const refName = node.constructor.refName - if (typeof this.player.public[refName] === "undefined") { - // by default create a empty object - this.player.public[refName] = {} - } + if (typeof this.base.processors[refName] === "undefined") { + // by default create a empty object + this.base.processors[refName] = {} + } - this.player.public[refName][key] = value - }) - } - } - } + this.base.processors[refName][key] = value + }) + } + } + } - async attachProcessorsToInstance(instance) { - for await (const [index, processor] of this.processors.entries()) { - if (processor.constructor.node_bypass === true) { - instance.contextElement.connect(processor.processor) + attachAllNodes = async () => { + for await (const [index, node] of this.nodes.entries()) { + if (node.constructor.node_bypass === true) { + this.base.context.elementSource.connect(node.processor) - processor.processor.connect(this.player.audioContext.destination) + node.processor.connect(this.base.context.destination) - continue - } + continue + } - if (typeof processor._attach !== "function") { - this.player.console.error(`Processor ${processor.constructor.refName} not support attach`) + if (typeof node._attach !== "function") { + this.base.console.error( + `Processor ${node.constructor.refName} not support attach`, + ) - continue - } + continue + } - instance = await processor._attach(instance, index) - } + await node._attach(index) + } - const lastProcessor = instance.attachedProcessors[instance.attachedProcessors.length - 1].processor + const lastProcessor = this.attached[this.attached.length - 1].processor - // now attach to destination - lastProcessor.connect(this.player.audioContext.destination) + // now attach to destination + lastProcessor.connect(this.base.context.destination) + } - return instance - } -} \ No newline at end of file + detachAllNodes = async () => { + for (const [index, node] of this.attached.entries()) { + await node._detach() + } + } +} diff --git a/packages/app/src/cores/player/classes/TrackInstance.js b/packages/app/src/cores/player/classes/TrackInstance.js index f88278c8..cae48993 100644 --- a/packages/app/src/cores/player/classes/TrackInstance.js +++ b/packages/app/src/cores/player/classes/TrackInstance.js @@ -1,206 +1,131 @@ import TrackManifest from "./TrackManifest" -import { MediaPlayer } from "dashjs" export default class TrackInstance { - constructor(player, manifest) { + constructor(manifest, player) { + if (typeof manifest === "undefined") { + throw new Error("Manifest is required") + } + if (!player) { throw new Error("Player core is required") } - if (typeof manifest === "undefined") { - throw new Error("Manifest is required") + if (!(manifest instanceof TrackManifest)) { + manifest = new TrackManifest(manifest, player) + } + + if (!manifest.source) { + throw new Error("Manifest must have a source") } this.player = player this.manifest = manifest this.id = this.manifest.id ?? this.manifest._id - - return this } - _initialized = false + play = async (params = {}) => { + const startTime = performance.now() - audio = null - - contextElement = null - - abortController = new AbortController() - - attachedProcessors = [] - - waitUpdateTimeout = null - - mediaEvents = { - ended: () => { - this.player.next() - }, - loadeddata: () => { - this.player.state.loading = false - }, - loadedmetadata: () => { - if (this.audio.duration === Infinity) { - this.player.state.live = true - } else { - this.player.state.live = false - } - }, - play: () => { - this.player.state.playback_status = "playing" - }, - playing: () => { - this.player.state.loading = false - - this.player.state.playback_status = "playing" - - if (typeof this.waitUpdateTimeout !== "undefined") { - clearTimeout(this.waitUpdateTimeout) - this.waitUpdateTimeout = null - } - }, - pause: () => { - this.player.state.playback_status = "paused" - }, - durationchange: () => { - this.player.eventBus.emit( - `player.durationchange`, - this.audio.duration, - ) - }, - waiting: () => { - if (this.waitUpdateTimeout) { - clearTimeout(this.waitUpdateTimeout) - this.waitUpdateTimeout = null - } - - // if takes more than 150ms to load, update loading state - this.waitUpdateTimeout = setTimeout(() => { - this.player.state.loading = true - }, 150) - }, - seeked: () => { - this.player.eventBus.emit(`player.seeked`, this.audio.currentTime) - }, - } - - initialize = async () => { - this.manifest = await this.resolveManifest() - - this.audio = new Audio() - - this.audio.signal = this.abortController.signal - this.audio.crossOrigin = "anonymous" - this.audio.preload = "metadata" - - // support for dash audio streaming - if (this.manifest.source.endsWith(".mpd")) { - this.muxerPlayer = MediaPlayer().create() - this.muxerPlayer.updateSettings({ - streaming: { - buffer: { - resetSourceBuffersForTrackSwitch: true, - useChangeTypeForTrackSwitch: false, - }, - }, - }) - this.muxerPlayer.initialize(this.audio, null, false) - - this.muxerPlayer.attachSource(this.manifest.source) + if (!this.manifest.source.endsWith(".mpd")) { + this.player.base.demuxer.destroy() + this.player.base.audio.src = this.manifest.source } else { - this.audio.src = this.manifest.source - } - - for (const [key, value] of Object.entries(this.mediaEvents)) { - this.audio.addEventListener(key, value) - } - - this.contextElement = this.player.audioContext.createMediaElementSource( - this.audio, - ) - - this._initialized = true - - return this - } - - stop = () => { - if (this.audio) { - this.audio.pause() - } - - if (this.muxerPlayer) { - this.muxerPlayer.destroy() - } - - const lastProcessor = - this.attachedProcessors[this.attachedProcessors.length - 1] - - if (lastProcessor) { - this.attachedProcessors[ - this.attachedProcessors.length - 1 - ]._destroy(this) - } - - this.attachedProcessors = [] - } - - resolveManifest = async () => { - if (typeof this.manifest === "string") { - this.manifest = { - src: this.manifest, - } - } - - this.manifest = new TrackManifest(this.manifest, { - serviceProviders: this.player.serviceProviders, - }) - - if (this.manifest.service) { - if (!this.player.serviceProviders.has(this.manifest.service)) { - throw new Error( - `Service ${this.manifest.service} is not supported`, - ) + if (!this.player.base.demuxer) { + this.player.base.createDemuxer() } - // try to resolve source file - if (!this.manifest.source) { - console.log("Resolving manifest cause no source defined") - - this.manifest = await this.player.serviceProviders.resolve( - this.manifest.service, - this.manifest, - ) - - console.log("Manifest resolved", this.manifest) - } - } - - if (!this.manifest.source) { - throw new Error("Manifest `source` is required") - } - - // set empty metadata if not provided - if (!this.manifest.metadata) { - this.manifest.metadata = {} - } - - // auto name if a title is not provided - if (!this.manifest.metadata.title) { - this.manifest.metadata.title = this.manifest.source.split("/").pop() - } - - // process overrides - const override = await this.manifest.serviceOperations.fetchOverride() - - if (override) { - console.log( - `Override found for track ${this.manifest._id}`, - override, + await this.player.base.demuxer.attachSource( + `${this.manifest.source}?t=${Date.now()}`, ) - - this.manifest.overrides = override } - return this.manifest + this.player.base.audio.currentTime = params.time ?? 0 + + if (this.player.base.audio.paused) { + await this.player.base.audio.play() + } + + // reset audio volume and gain + this.player.base.audio.volume = 1 + this.player.base.processors.gain.set(this.player.state.volume) + + const endTime = performance.now() + + this._loadMs = endTime - startTime + + console.log(`[INSTANCE] Playing >`, this) } + + pause = async () => { + console.log("[INSTANCE] Pausing >", this) + + this.player.base.audio.pause() + } + + resume = async () => { + console.log("[INSTANCE] Resuming >", this) + + this.player.base.audio.play() + } + + // resolveManifest = async () => { + // if (typeof this.manifest === "string") { + // this.manifest = { + // src: this.manifest, + // } + // } + + // this.manifest = new TrackManifest(this.manifest, { + // serviceProviders: this.player.serviceProviders, + // }) + + // if (this.manifest.service) { + // if (!this.player.serviceProviders.has(this.manifest.service)) { + // throw new Error( + // `Service ${this.manifest.service} is not supported`, + // ) + // } + + // // try to resolve source file + // if (!this.manifest.source) { + // console.log("Resolving manifest cause no source defined") + + // this.manifest = await this.player.serviceProviders.resolve( + // this.manifest.service, + // this.manifest, + // ) + + // console.log("Manifest resolved", this.manifest) + // } + // } + + // if (!this.manifest.source) { + // throw new Error("Manifest `source` is required") + // } + + // // set empty metadata if not provided + // if (!this.manifest.metadata) { + // this.manifest.metadata = {} + // } + + // // auto name if a title is not provided + // if (!this.manifest.metadata.title) { + // this.manifest.metadata.title = this.manifest.source.split("/").pop() + // } + + // // process overrides + // const override = await this.manifest.serviceOperations.fetchOverride() + + // if (override) { + // console.log( + // `Override found for track ${this.manifest._id}`, + // override, + // ) + + // this.manifest.overrides = override + // } + + // return this.manifest + // } } diff --git a/packages/app/src/cores/player/classes/TrackManifest.js b/packages/app/src/cores/player/classes/TrackManifest.js index 1a8e404a..446f650d 100644 --- a/packages/app/src/cores/player/classes/TrackManifest.js +++ b/packages/app/src/cores/player/classes/TrackManifest.js @@ -1,4 +1,4 @@ -import jsmediatags from "jsmediatags/dist/jsmediatags.min.js" +import { parseBlob } from "music-metadata" import { FastAverageColor } from "fast-average-color" export default class TrackManifest { @@ -33,13 +33,6 @@ export default class TrackManifest { this.artist = params.artist } - if ( - typeof params.artists !== "undefined" || - Array.isArray(params.artists) - ) { - this.artistStr = params.artists.join(", ") - } - if (typeof params.source !== "undefined") { this.source = params.source } @@ -48,8 +41,8 @@ export default class TrackManifest { this.metadata = params.metadata } - if (typeof params.lyrics_enabled !== "undefined") { - this.lyrics_enabled = params.lyrics_enabled + if (typeof params.liked !== "undefined") { + this.liked = params.liked } return this @@ -58,87 +51,45 @@ export default class TrackManifest { _id = null // used for api requests uid = null // used for internal - cover = - "https://storage.ragestudio.net/comty-static-assets/default_song.png" title = "Untitled" album = "Unknown" artist = "Unknown" + cover = null // set default cover url source = null - metadata = null + metadata = {} // set default service to default service = "default" - // Extended from db - lyrics_enabled = false - liked = null - async initialize() { - if (this.params.file) { - this.metadata = await this.analyzeMetadata( - this.params.file.originFileObj, - ) + if (!this.params.file) { + return this + } - this.metadata.format = this.metadata.type.toUpperCase() + const analyzedMetadata = await parseBlob(this.params.file, { + skipPostHeaders: true, + }).catch(() => ({})) - if (this.metadata.tags) { - if (this.metadata.tags.title) { - this.title = this.metadata.tags.title - } + if (analyzedMetadata.format) { + this.metadata.format = analyzedMetadata.format.codec + } - if (this.metadata.tags.artist) { - this.artist = this.metadata.tags.artist - } + if (analyzedMetadata.common) { + this.title = analyzedMetadata.common.title ?? this.title + this.artist = analyzedMetadata.common.artist ?? this.artist + this.album = analyzedMetadata.common.album ?? this.album + } - if (this.metadata.tags.album) { - this.album = this.metadata.tags.album - } + if (analyzedMetadata.common.picture) { + const cover = analyzedMetadata.common.picture[0] - if (this.metadata.tags.picture) { - this.cover = app.cores.remoteStorage.binaryArrayToFile( - this.metadata.tags.picture, - "cover", - ) - - const coverUpload = - await app.cores.remoteStorage.uploadFile(this.cover) - - this.cover = coverUpload.url - - delete this.metadata.tags.picture - } - - this.handleChanges({ - cover: this.cover, - title: this.title, - artist: this.artist, - album: this.album, - }) - } + this._coverBlob = new Blob([cover.data], { type: cover.format }) + this.cover = URL.createObjectURL(this._coverBlob) } return this } - handleChanges = (changes) => { - if (typeof this.params.onChange === "function") { - this.params.onChange(this.uid, changes) - } - } - - analyzeMetadata = async (file) => { - return new Promise((resolve, reject) => { - jsmediatags.read(file, { - onSuccess: (data) => { - return resolve(data) - }, - onError: (error) => { - return reject(error) - }, - }) - }) - } - analyzeCoverColor = async () => { const fac = new FastAverageColor() @@ -169,8 +120,6 @@ export default class TrackManifest { this, ) - console.log(this.overrides) - if (this.overrides) { return { ...result, @@ -210,6 +159,7 @@ export default class TrackManifest { return { _id: this._id, uid: this.uid, + cover: this.cover, title: this.title, album: this.album, artist: this.artist, diff --git a/packages/app/src/cores/player/player.core.js b/packages/app/src/cores/player/player.core.js index 435a466b..2d0c260f 100755 --- a/packages/app/src/cores/player/player.core.js +++ b/packages/app/src/cores/player/player.core.js @@ -3,11 +3,11 @@ import { Core } from "@ragestudio/vessel" import ActivityEvent from "@classes/ActivityEvent" import QueueManager from "@classes/QueueManager" import TrackInstance from "./classes/TrackInstance" -//import MediaSession from "./classes/MediaSession" +import MediaSession from "./classes/MediaSession" import ServiceProviders from "./classes/Services" import PlayerState from "./classes/PlayerState" import PlayerUI from "./classes/PlayerUI" -import PlayerProcessors from "./classes/PlayerProcessors" +import AudioBase from "./classes/AudioBase" import setSampleRate from "./helpers/setSampleRate" @@ -22,27 +22,18 @@ export default class Player extends Core { // player config static defaultSampleRate = 48000 - static gradualFadeMs = 150 - static maxManifestPrecompute = 3 state = new PlayerState(this) ui = new PlayerUI(this) serviceProviders = new ServiceProviders() - //nativeControls = new MediaSession() - audioContext = new AudioContext({ - sampleRate: - AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate, - latencyHint: "playback", - }) + nativeControls = new MediaSession(this) - audioProcessors = new PlayerProcessors(this) + base = new AudioBase(this) queue = new QueueManager({ loadFunction: this.createInstance, }) - currentTrackInstance = null - public = { start: this.start, close: this.close, @@ -74,10 +65,11 @@ export default class Player extends Core { eventBus: () => { return this.eventBus }, + base: () => { + return this.base + }, state: this.state, ui: this.ui.public, - audioContext: this.audioContext, - gradualFadeMs: Player.gradualFadeMs, } async afterInitialize() { @@ -85,8 +77,8 @@ export default class Player extends Core { this.state.volume = 1 } - //await this.nativeControls.initialize() - await this.audioProcessors.initialize() + await this.nativeControls.initialize() + await this.base.initialize() } // @@ -100,10 +92,6 @@ export default class Player extends Core { } } - async createInstance(manifest) { - return new TrackInstance(this, manifest) - } - // // Playback methods // @@ -112,46 +100,21 @@ export default class Player extends Core { throw new Error("Audio instance is required") } - this.console.log("Initializing instance", instance) - // resume audio context if needed - if (this.audioContext.state === "suspended") { - this.audioContext.resume() + if (this.base.context.state === "suspended") { + this.base.context.resume() } - // initialize instance if is not - if (this.queue.currentItem._initialized === false) { - this.queue.currentItem = await instance.initialize() - } - - this.console.log("Instance", this.queue.currentItem) - // update manifest - this.state.track_manifest = this.queue.currentItem.manifest - - // attach processors - this.queue.currentItem = - await this.audioProcessors.attachProcessorsToInstance( - this.queue.currentItem, - ) - - // set audio properties - this.queue.currentItem.audio.currentTime = params.time ?? 0 - this.queue.currentItem.audio.muted = this.state.muted - this.queue.currentItem.audio.loop = - this.state.playback_mode === "repeat" - this.queue.currentItem.gainNode.gain.value = Math.pow( - this.state.volume, - 2, - ) + this.state.track_manifest = + this.queue.currentItem.manifest.toSeriableObject() // play - await this.queue.currentItem.audio.play() - - this.console.log(`Playing track >`, this.queue.currentItem) + //await this.queue.currentItem.audio.play() + await this.queue.currentItem.play(params) // update native controls - //this.nativeControls.update(this.queue.currentItem.manifest) + this.nativeControls.update(this.queue.currentItem.manifest) return this.queue.currentItem } @@ -160,10 +123,10 @@ export default class Player extends Core { this.ui.attachPlayerComponent() if (this.queue.currentItem) { - await this.queue.currentItem.stop() + await this.queue.currentItem.pause() } - await this.abortPreloads() + //await this.abortPreloads() await this.queue.flush() this.state.loading = true @@ -187,8 +150,8 @@ export default class Player extends Core { playlist = await this.serviceProviders.resolveMany(playlist) } - for await (const [index, _manifest] of playlist.entries()) { - let instance = await this.createInstance(_manifest) + for await (let [index, _manifest] of playlist.entries()) { + let instance = new TrackInstance(_manifest, this) this.queue.add(instance) } @@ -229,10 +192,6 @@ export default class Player extends Core { } next() { - if (this.queue.currentItem) { - this.queue.currentItem.stop() - } - //const isRandom = this.state.playback_mode === "shuffle" const item = this.queue.next() @@ -244,10 +203,6 @@ export default class Player extends Core { } previous() { - if (this.queue.currentItem) { - this.queue.currentItem.stop() - } - const item = this.queue.previous() return this.play(item) @@ -275,18 +230,14 @@ export default class Player extends Core { return null } - // set gain exponentially - this.queue.currentItem.gainNode.gain.linearRampToValueAtTime( - 0.0001, - this.audioContext.currentTime + Player.gradualFadeMs / 1000, - ) + this.base.processors.gain.fade(0) setTimeout(() => { - this.queue.currentItem.audio.pause() + this.queue.currentItem.pause() resolve() }, Player.gradualFadeMs) - //this.nativeControls.updateIsPlaying(false) + this.nativeControls.updateIsPlaying(false) }) } @@ -302,19 +253,12 @@ export default class Player extends Core { } // ensure audio elemeto starts from 0 volume - this.queue.currentItem.gainNode.gain.value = 0.0001 - - this.queue.currentItem.audio.play().then(() => { + this.queue.currentItem.resume().then(() => { resolve() }) + this.base.processors.gain.fade(this.state.volume) - // set gain exponentially - this.queue.currentItem.gainNode.gain.linearRampToValueAtTime( - Math.pow(this.state.volume, 2), - this.audioContext.currentTime + Player.gradualFadeMs / 1000, - ) - - //this.nativeControls.updateIsPlaying(true) + this.nativeControls.updateIsPlaying(true) }) } @@ -325,10 +269,7 @@ export default class Player extends Core { this.state.playback_mode = mode - if (this.queue.currentItem) { - this.queue.currentItem.audio.loop = - this.state.playback_mode === "repeat" - } + this.base.audio.loop = this.state.playback_mode === "repeat" AudioPlayerStorage.set("mode", mode) @@ -336,22 +277,15 @@ export default class Player extends Core { } stopPlayback() { - if (this.queue.currentItem) { - this.queue.currentItem.stop() - } - + this.base.flush() this.queue.flush() - this.abortPreloads() - this.state.playback_status = "stopped" this.state.track_manifest = null - this.queue.currentItem = null - this.track_next_instances = [] - this.track_prev_instances = [] - //this.nativeControls.destroy() + //this.abortPreloads() + this.nativeControls.flush() } // @@ -369,7 +303,7 @@ export default class Player extends Core { if (typeof to === "boolean") { this.state.muted = to - this.queue.currentItem.audio.muted = to + this.base.audio.muted = to } return this.state.muted @@ -395,65 +329,42 @@ export default class Player extends Core { volume = 0 } - this.state.volume = volume - AudioPlayerStorage.set("volume", volume) - if (this.queue.currentItem) { - if (this.queue.currentItem.gainNode) { - this.queue.currentItem.gainNode.gain.value = Math.pow( - this.state.volume, - 2, - ) - } - } + this.state.volume = volume + this.base.processors.gain.set(volume) return this.state.volume } seek(time) { - if (!this.queue.currentItem || !this.queue.currentItem.audio) { + if (!this.base.audio) { return false } // if time not provided, return current time if (typeof time === "undefined") { - return this.queue.currentItem.audio.currentTime + return this.base.audio.currentTime } // if time is provided, seek to that time if (typeof time === "number") { this.console.log( - `Seeking to ${time} | Duration: ${this.queue.currentItem.audio.duration}`, + `Seeking to ${time} | Duration: ${this.base.audio.duration}`, ) - this.queue.currentItem.audio.currentTime = time + this.base.audio.currentTime = time return time } } duration() { - if (!this.queue.currentItem || !this.queue.currentItem.audio) { + if (!this.base.audio) { return false } - return this.queue.currentItem.audio.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.queue.currentItem.audio) { - this.queue.currentItem.audio.loop = this.state.loop - } - - return this.state.loop + return this.base.audio.duration } close() { diff --git a/packages/app/src/cores/player/processors/compressorNode/index.js b/packages/app/src/cores/player/processors/compressorNode/index.js index 8c372892..446e5e15 100755 --- a/packages/app/src/cores/player/processors/compressorNode/index.js +++ b/packages/app/src/cores/player/processors/compressorNode/index.js @@ -2,44 +2,40 @@ import ProcessorNode from "../node" import Presets from "../../classes/Presets" export default class CompressorProcessorNode extends ProcessorNode { - constructor(props) { - super(props) + constructor(props) { + super(props) - this.presets = new Presets({ - storage_key: "compressor", - defaultPresetValue: { - threshold: -50, - knee: 40, - ratio: 12, - attack: 0.003, - release: 0.25, - }, - onApplyValues: this.applyValues.bind(this), - }) + this.presets = new Presets({ + storage_key: "compressor", + defaultPresetValue: { + threshold: -50, + knee: 40, + ratio: 12, + attack: 0.003, + release: 0.25, + }, + onApplyValues: this.applyValues.bind(this), + }) - this.exposeToPublic = { - presets: this.presets, - detach: this._detach, - attach: this._attach, - } - } + this.exposeToPublic = { + presets: this.presets, + detach: this._detach, + attach: this._attach, + } + } - static refName = "compressor" - static dependsOnSettings = ["player.compressor"] + static refName = "compressor" + static dependsOnSettings = ["player.compressor"] - async init(AudioContext) { - if (!AudioContext) { - throw new Error("AudioContext is required") - } + async init() { + this.processor = this.audioContext.createDynamicsCompressor() - this.processor = AudioContext.createDynamicsCompressor() + this.applyValues() + } - this.applyValues() - } - - applyValues() { - Object.keys(this.presets.currentPresetValues).forEach((key) => { - this.processor[key].value = this.presets.currentPresetValues[key] - }) - } -} \ No newline at end of file + applyValues() { + Object.keys(this.presets.currentPresetValues).forEach((key) => { + this.processor[key].value = this.presets.currentPresetValues[key] + }) + } +} diff --git a/packages/app/src/cores/player/processors/eqNode/index.js b/packages/app/src/cores/player/processors/eqNode/index.js index 09103971..5525a6d6 100755 --- a/packages/app/src/cores/player/processors/eqNode/index.js +++ b/packages/app/src/cores/player/processors/eqNode/index.js @@ -2,93 +2,98 @@ import ProcessorNode from "../node" import Presets from "../../classes/Presets" export default class EqProcessorNode extends ProcessorNode { - constructor(props) { - super(props) + constructor(props) { + super(props) - this.presets = new Presets({ - storage_key: "eq", - defaultPresetValue: { - 32: 0, - 64: 0, - 125: 0, - 250: 0, - 500: 0, - 1000: 0, - 2000: 0, - 4000: 0, - 8000: 0, - 16000: 0, - }, - onApplyValues: this.applyValues.bind(this), - }) + this.presets = new Presets({ + storage_key: "eq", + defaultPresetValue: { + 32: 0, + 64: 0, + 125: 0, + 250: 0, + 500: 0, + 1000: 0, + 2000: 0, + 4000: 0, + 8000: 0, + 16000: 0, + }, + onApplyValues: this.applyValues.bind(this), + }) - this.exposeToPublic = { - presets: this.presets, - } - } + this.exposeToPublic = { + presets: this.presets, + } + } - static refName = "eq" - static lock = true + static refName = "eq" - applyValues() { - // apply to current instance - this.processor.eqNodes.forEach((processor) => { - const gainValue = this.presets.currentPresetValues[processor.frequency.value] + applyValues() { + // apply to current instance + this.processor.eqNodes.forEach((processor) => { + const gainValue = + this.presets.currentPresetValues[processor.frequency.value] - if (processor.gain.value !== gainValue) { - console.debug(`[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${gainValue}`) - processor.gain.value = gainValue - } - }) - } + if (processor.gain.value !== gainValue) { + console.debug( + `[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${gainValue}`, + ) + processor.gain.value = gainValue + } + }) + } - async init() { - if (!this.audioContext) { - throw new Error("audioContext is required") - } + async init() { + if (!this.audioContext) { + throw new Error("audioContext is required") + } - this.processor = this.audioContext.createGain() + this.processor = this.audioContext.createGain() - this.processor.gain.value = 1 + this.processor.gain.value = 1 - this.processor.eqNodes = [] + this.processor.eqNodes = [] - const values = Object.entries(this.presets.currentPresetValues).map((entry) => { - return { - freq: parseFloat(entry[0]), - gain: parseFloat(entry[1]), - } - }) + const values = Object.entries(this.presets.currentPresetValues).map( + (entry) => { + return { + freq: parseFloat(entry[0]), + gain: parseFloat(entry[1]), + } + }, + ) - 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 - } + 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 - }) + 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] + // 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 (index === 0) { + this.processor.connect(eqNode) + } - if (nextNode) { - eqNode.connect(nextNode) - } - } + 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) - } -} \ No newline at end of file + // set last processor for processor node can properly connect to the next node + this.processor._last = this.processor.eqNodes.at(-1) + } +} diff --git a/packages/app/src/cores/player/processors/gainNode/index.js b/packages/app/src/cores/player/processors/gainNode/index.js index dc00fa59..0ed6c33f 100755 --- a/packages/app/src/cores/player/processors/gainNode/index.js +++ b/packages/app/src/cores/player/processors/gainNode/index.js @@ -1,60 +1,49 @@ -import AudioPlayerStorage from "../../player.storage" import ProcessorNode from "../node" export default class GainProcessorNode extends ProcessorNode { - static refName = "gain" + static refName = "gain" + static gradualFadeMs = 150 - static lock = true + exposeToPublic = { + set: this.setGain.bind(this), + linearRampToValueAtTime: this.linearRampToValueAtTime.bind(this), + fade: this.fade.bind(this), + } - static defaultValues = { - gain: 1, - } + setGain(gain) { + gain = this.processGainValue(gain) - state = { - gain: AudioPlayerStorage.get("gain") ?? GainProcessorNode.defaultValues.gain, - } + return (this.processor.gain.value = gain) + } - exposeToPublic = { - modifyValues: function (values) { - this.state = { - ...this.state, - ...values, - } + linearRampToValueAtTime(gain, time) { + gain = this.processGainValue(gain) + return this.processor.gain.linearRampToValueAtTime(gain, time) + } - AudioPlayerStorage.set("gain", this.state.gain) + fade(gain) { + if (gain <= 0) { + gain = 0.0001 + } else { + gain = this.processGainValue(gain) + } - this.applyValues() - }.bind(this), - resetDefaultValues: function () { - this.exposeToPublic.modifyValues(GainProcessorNode.defaultValues) + const currentTime = this.audioContext.currentTime + const fadeTime = currentTime + this.constructor.gradualFadeMs / 1000 - return this.state - }.bind(this), - values: () => this.state, - } + this.processor.gain.linearRampToValueAtTime(gain, fadeTime) + } - applyValues() { - // apply to current instance - this.processor.gain.value = app.cores.player.state.volume * this.state.gain - } + processGainValue(gain) { + return Math.pow(gain, 2) + } - async init() { - if (!this.audioContext) { - throw new Error("audioContext is required") - } + 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 - } -} \ No newline at end of file + this.processor = this.audioContext.createGain() + this.processor.gain.value = this.player.state.volume + } +} diff --git a/packages/app/src/cores/player/processors/index.js b/packages/app/src/cores/player/processors/index.js index c266c920..4a6a87c0 100755 --- a/packages/app/src/cores/player/processors/index.js +++ b/packages/app/src/cores/player/processors/index.js @@ -2,13 +2,12 @@ import EqProcessorNode from "./eqNode" import GainProcessorNode from "./gainNode" import CompressorProcessorNode from "./compressorNode" //import BPMProcessorNode from "./bpmNode" - -import SpatialNode from "./spatialNode" +//import SpatialNode from "./spatialNode" export default [ - //BPMProcessorNode, - EqProcessorNode, - GainProcessorNode, - CompressorProcessorNode, - SpatialNode, -] \ No newline at end of file + //BPMProcessorNode, + EqProcessorNode, + GainProcessorNode, + CompressorProcessorNode, + //SpatialNode, +] diff --git a/packages/app/src/cores/player/processors/node.js b/packages/app/src/cores/player/processors/node.js index 84a6a865..ddcd1826 100755 --- a/packages/app/src/cores/player/processors/node.js +++ b/packages/app/src/cores/player/processors/node.js @@ -1,172 +1,147 @@ export default class ProcessorNode { - constructor(PlayerCore) { - if (!PlayerCore) { - throw new Error("PlayerCore is required") - } + constructor(manager) { + if (!manager) { + throw new Error("processorManager is required") + } - this.PlayerCore = PlayerCore - this.audioContext = PlayerCore.audioContext - } + this.manager = manager + this.audioContext = manager.base.context + this.elementSource = manager.base.elementSource + this.player = manager.base.player + } - async _init() { - // check if has init method - if (typeof this.init === "function") { - await this.init(this.audioContext) - } + async _init() { + // check if has init method + if (typeof this.init === "function") { + await this.init() + } - // check if has declared bus events - if (typeof this.busEvents === "object") { - Object.entries(this.busEvents).forEach((event, fn) => { - app.eventBus.on(event, fn) - }) - } + // 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 - } + if (typeof this.processor._last === "undefined") { + this.processor._last = this.processor + } - return this - } + return this + } - _attach(instance, index) { - if (typeof instance !== "object") { - instance = this.PlayerCore.currentAudioInstance - } + _attach(index) { + // 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(", ")}`, + ) - // 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 null + } + } - return instance - } - } + // if index is not defined, attach to the last node + if (!index) { + index = this.manager.attached.length + } - // if index is not defined, attach to the last node - if (!index) { - index = instance.attachedProcessors.length - } + const prevNode = this.manager.attached[index - 1] + const nextNode = this.manager.attached[index + 1] - const prevNode = instance.attachedProcessors[index - 1] - const nextNode = instance.attachedProcessors[index + 1] + const currentIndex = this._findIndex() - 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`, + ) - // check if is already attached - if (currentIndex !== false) { - console.warn(`[${this.constructor.refName ?? this.constructor.name}] node is already attached`) + return null + } - 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() - // 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...`) + this.elementSource.connect(this.processor) + } - // 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.contextElement.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) + } - // 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 + this.manager.attached.splice(index, 0, this) - // add to the attachedProcessors - instance.attachedProcessors.splice(index, 0, this) + // // handle instance mutation + // if (typeof this.mutateInstance === "function") { + // instance = this.mutateInstance(instance) + // } - // handle instance mutation - if (typeof this.mutateInstance === "function") { - instance = this.mutateInstance(instance) - } + return this + } - return instance - } + _detach() { + // find index of the node within the attachedProcessors serching for matching refName + const index = this._findIndex() - _detach(instance) { - if (typeof instance !== "object") { - instance = this.PlayerCore.currentAudioInstance - } + if (!index) { + return null + } - // find index of the node within the attachedProcessors serching for matching refName - const index = this._findIndex(instance) + // retrieve the previous and next nodes + const prevNode = this.manager.attached[index - 1] + const nextNode = this.manager.attached[index + 1] - if (!index) { - return instance - } + // 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() + } - // retrieve the previous and next nodes - const prevNode = instance.attachedProcessors[index - 1] - const nextNode = instance.attachedProcessors[index + 1] + // disconnect + this.processor.disconnect() + this.manager.attached.splice(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() - } + // 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) + } - // disconnect - instance = this._destroy(instance) + return this + } - // 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) - } + _findIndex() { + // find index of the node within the attachedProcessors serching for matching refName + const index = this.manager.attached.findIndex((node) => { + return node.constructor.refName === this.constructor.refName + }) - return instance - } + if (index === -1) { + return false + } - _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 - } -} \ No newline at end of file + return index + } +} diff --git a/packages/app/src/cores/remoteStorage/remoteStorage.core.js b/packages/app/src/cores/remoteStorage/remoteStorage.core.js index 91bd8da0..3cca026c 100755 --- a/packages/app/src/cores/remoteStorage/remoteStorage.core.js +++ b/packages/app/src/cores/remoteStorage/remoteStorage.core.js @@ -84,9 +84,9 @@ export default class RemoteStorage extends Core { _reject(message) }) - uploader.events.on("progress", ({ percentProgress }) => { + uploader.events.on("progress", (data) => { if (typeof onProgress === "function") { - onProgress(file, percentProgress) + onProgress(file, data) } }) diff --git a/packages/app/src/cores/sfx/sfx.core.js b/packages/app/src/cores/sfx/sfx.core.js index 06c7ec63..aad78101 100755 --- a/packages/app/src/cores/sfx/sfx.core.js +++ b/packages/app/src/cores/sfx/sfx.core.js @@ -6,102 +6,119 @@ import store from "store" import config from "@config" export default class SFXCore extends Core { - static namespace = "sfx" + static namespace = "sfx" - soundsPool = {} + soundsPool = {} - public = { - loadSoundpack: this.loadSoundpack.bind(this), - play: this.play, - } + public = { + loadSoundpack: this.loadSoundpack.bind(this), + play: this.play, + } - onEvents = { - "sfx:test": (volume) => { - // play a sound to test volume - this.play("test", { - volume: volume / 100, - }) - } - } + onEvents = { + "sfx:test": (volume) => { + // play a sound to test volume + this.play("test", { + volume: volume / 100, + }) + }, + } - async loadSoundpack(soundpack) { - if (!soundpack) { - soundpack = store.get("soundpack") - } + async loadSoundpack(soundpack) { + if (!soundpack) { + soundpack = store.get("soundpack") + } - if (!soundpack) { - soundpack = config.defaultSoundPack ?? {} - } + if (!soundpack) { + soundpack = config.defaultSoundPack ?? {} + } - // check if is valid url with regex - const urlRegex = /^(http|https):\/\/[^ "]+$/; + // check if is valid url with regex + const urlRegex = /^(http|https):\/\/[^ "]+$/ - if (urlRegex.test(soundpack)) { - const { data } = await axios.get(soundpack) + if (urlRegex.test(soundpack)) { + const { data } = await axios.get(soundpack) - soundpack = data - } + soundpack = data + } - if (typeof soundpack.sounds !== "object") { - this.console.error(`Soundpack [${soundpack.id}] is not a valid soundpack.`) - return false - } + if (typeof soundpack.sounds !== "object") { + this.console.error( + `Soundpack [${soundpack.id}] is not a valid soundpack.`, + ) + return false + } - this.console.log(`Loading soundpack [${soundpack.id} | ${soundpack.name}] by ${soundpack.author} (${soundpack.version})`) + this.console.log( + `Loading soundpack [${soundpack.id} | ${soundpack.name}] by ${soundpack.author} (${soundpack.version})`, + ) - for (const [name, path] of Object.entries(soundpack.sounds)) { - this.soundsPool[name] = new Howl({ - volume: 0.5, - src: [path], - }) - } - } + for (const [name, path] of Object.entries(soundpack.sounds)) { + this.soundsPool[name] = new Howl({ + volume: 0.5, + src: [path], + }) + } + } - async play(name, options = {}) { - if (!window.app.cores.settings.is("ui.effects", true)) { - return false - } + async play(name, options = {}) { + if (!window.app.cores.settings.is("ui.effects", true)) { + return false + } - const audioInstance = this.soundsPool[name] + const audioInstance = this.soundsPool[name] - if (!audioInstance) { - return false - } + if (!audioInstance) { + return false + } - if (typeof options.volume !== "undefined") { - audioInstance.volume(options.volume) - } else { - audioInstance.volume((window.app.cores.settings.get("ui.general_volume") ?? 0) / 100) - } + if (typeof options.volume !== "undefined") { + audioInstance.volume(options.volume) + } else { + audioInstance.volume( + (window.app.cores.settings.get("ui.general_volume") ?? 0) / 100, + ) + } - audioInstance.play() - } + audioInstance.play() + } - async handleClick(event) { - // search for closest button - const button = event.target.closest("button") || event.target.closest(".ant-btn") + async handleClick(event) { + // search for closest button + const button = + event.target.closest("button") || event.target.closest(".ant-btn") - // search for a slider - const slider = event.target.closest("input[type=range]") + // search for a slider + const slider = event.target.closest("input[type=range]") - // if button exist and has aria-checked attribute then play switch_on or switch_off - if (button) { - if (button.hasAttribute("aria-checked")) { - return this.play(button.getAttribute("aria-checked") === "true" ? "component.switch_off" : "component.switch_on") - } + // if button exist and has aria-checked attribute then play switch_on or switch_off + if (button) { + if (button.hasAttribute("aria-checked")) { + return this.play( + button.getAttribute("aria-checked") === "true" + ? "component.switch_off" + : "component.switch_on", + ) + } - return this.play("generic_click") - } + return this.play("generic_click") + } - if (slider) { - // check if is up or down - this.console.log(slider) - } - } + if (slider) { + // check if is up or down + this.console.log(slider) + } + } - async onInitialize() { - await this.loadSoundpack() + async onInitialize() { + await this.loadSoundpack() - document.addEventListener("click", (...args) => { this.handleClick(...args) }, true) - } -} \ No newline at end of file + document.addEventListener( + "click", + (...args) => { + this.handleClick(...args) + }, + true, + ) + } +} diff --git a/packages/app/src/pages/@mobile-views/player/index.jsx b/packages/app/src/pages/@mobile-views/player/index.jsx index 305e6452..eb3c204f 100755 --- a/packages/app/src/pages/@mobile-views/player/index.jsx +++ b/packages/app/src/pages/@mobile-views/player/index.jsx @@ -4,7 +4,7 @@ import classnames from "classnames" import { Icons } from "@components/Icons" import SeekBar from "@components/Player/SeekBar" import Controls from "@components/Player/Controls" -import ExtraActions from "@components/Player/ExtraActions" +import Actions from "@components/Player/Actions" import { usePlayerStateContext } from "@contexts/WithPlayerContext" import RGBStringToValues from "@utils/rgbToValues" @@ -12,102 +12,96 @@ import RGBStringToValues from "@utils/rgbToValues" import "./index.less" const ServiceIndicator = (props) => { - if (!props.service) { - return null - } + if (!props.service) { + return null + } - switch (props.service) { - case "tidal": { - return
- Playing from Tidal -
- } - default: { - return null - } - } + switch (props.service) { + case "tidal": { + return ( +
+ Playing from Tidal +
+ ) + } + default: { + return null + } + } } const AudioPlayer = (props) => { - const [playerState] = usePlayerStateContext() + const [playerState] = usePlayerStateContext() - React.useEffect(() => { - if (app.currentDragger) { - app.currentDragger.setBackgroundColorValues(RGBStringToValues(playerState.track_manifest?.cover_analysis?.rgb)) - } + React.useEffect(() => { + if (app.currentDragger) { + app.currentDragger.setBackgroundColorValues( + RGBStringToValues( + playerState.track_manifest?.cover_analysis?.rgb, + ), + ) + } + }, [playerState.track_manifest?.cover_analysis]) - }, [playerState.track_manifest?.cover_analysis]) + const { + title, + album, + artist, + service, + lyricsEnabled, + cover_analysis, + cover, + } = playerState.track_manifest ?? {} - const { - title, - album, - artist, - service, - lyricsEnabled, - cover_analysis, - cover, - } = playerState.track_manifest ?? {} + const playing = playerState.playback_status === "playing" + const stopped = playerState.playback_status === "stopped" - const playing = playerState.playback_status === "playing" - const stopped = playerState.playback_status === "stopped" + const titleText = !playing && stopped ? "Stopped" : (title ?? "Untitled") + const subtitleText = `${artist} | ${album?.title ?? album}` - const titleText = (!playing && stopped) ? "Stopped" : (title ?? "Untitled") - const subtitleText = `${artist} | ${album?.title ?? album}` + return ( +
+
+ - return
-
- +
-
+
+
+
+

{titleText}

+
+
+ {subtitleText} +
+
+
-
-
-
-

- { - titleText - } -

-
-
-
-

- {subtitleText} -

-
-
-
-
+ - + - - - -
-
+ +
+
+ ) } -export default AudioPlayer \ No newline at end of file +export default AudioPlayer diff --git a/packages/app/src/pages/@mobile-views/player/index.less b/packages/app/src/pages/@mobile-views/player/index.less index 40cf24cc..880817ce 100755 --- a/packages/app/src/pages/@mobile-views/player/index.less +++ b/packages/app/src/pages/@mobile-views/player/index.less @@ -1,199 +1,184 @@ @top_controls_height: 55px; -.mobile_media_player_wrapper { - position: relative; +.mobile-player_wrapper { + position: relative; - z-index: 320; + z-index: 320; - display: flex; + display: flex; - flex-direction: column; + flex-direction: column; - height: 100%; - width: 100%; + height: 100%; + width: 100%; - .mobile_media_player_background { - position: absolute; + margin-bottom: 30px; - z-index: 320; + .mobile-player_background { + position: absolute; - top: 0; - left: 0; + z-index: 320; - width: 100%; - height: 100%; + top: 0; + left: 0; - background-color: rgba(var(--cover_averageValues), 0.4); - } + width: 100%; + height: 100%; + + background-color: rgba(var(--cover_averageValues), 0.4); + } } -.mobile_media_player { - position: relative; +.mobile-player { + position: relative; - display: inline-flex; - flex-direction: column; + display: inline-flex; + flex-direction: column; - align-items: flex-start; - justify-content: center; + align-items: flex-start; + justify-content: center; - width: 100%; - height: 100%; + width: 100%; + height: 100%; - gap: 10px; + gap: 10px; - transition: all 150ms ease-out; + transition: all 150ms ease-out; - z-index: 330; + z-index: 330; - .service_indicator { - color: var(--text-color); + .service_indicator { + color: var(--text-color); - background-color: var(--background-color-accent); - padding: 7px; - border-radius: 8px; + background-color: var(--background-color-accent); + padding: 7px; + border-radius: 8px; - font-size: 0.9rem; - } + font-size: 0.9rem; + } - .cover { - position: relative; + .mobile-player-cover { + position: relative; - z-index: 320; + z-index: 320; - margin: auto; + margin: auto; - width: 100%; - height: 100%; + width: 100%; + height: 100%; - min-height: 40vh; - min-width: 100%; + min-height: 40vh; + min-width: 100%; - border-radius: 24px; + border-radius: 24px; - background-position: center; - background-size: cover; - background-repeat: no-repeat; + background-position: center; + background-size: cover; + background-repeat: no-repeat; - transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; - img { - width: 100%; - height: 100%; + img { + width: 100%; + height: 100%; - object-fit: cover; - object-position: center; - } - } + object-fit: cover; + object-position: center; + } + } - .header { - position: relative; + .mobile-player-header { + position: relative; - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; - width: 100%; + width: 100%; - .info { - display: flex; - flex-direction: column; + .mobile-player-info { + display: flex; + flex-direction: column; - h1, - h2, - h3, - h4, - h5, - h6, - p, - span { - margin: 0; - color: var(--text-color); - } + h1, + h2, + h3, + h4, + h5, + h6, + p, + span { + margin: 0; + color: var(--text-color); + } - width: 100%; + width: 100%; - .title { - font-size: 1rem; - font-weight: 600; - color: var(--text-color); + .mobile-player-info-title { + display: flex; + flex-direction: row; - word-break: break-all; + align-items: center; - font-family: "Space Grotesk", sans-serif; - } + font-size: 1rem; + font-weight: 600; - .subTitle { - display: flex; - flex-direction: row; + word-break: break-all; - width: 100%; + font-family: "Space Grotesk", sans-serif; + } - justify-content: space-between; + .mobile-player-info-subTitle { + display: flex; + flex-direction: row; - .likeButton { - margin-right: 20px; - } + align-items: center; + justify-content: space-between; - .artist { - font-size: 0.6rem; - font-weight: 400; - color: var(--text-color); - } - } - } - } + font-size: 0.7rem; + font-weight: 400; + } + } + } - .player-controls { - .ant-btn { - min-width: 40px !important; - min-height: 40px !important; - } + .player-controls { + .ant-btn { + min-width: 40px !important; + min-height: 40px !important; + } - svg { - font-size: 1.2rem; - } + svg { + font-size: 1.2rem; + } - .playButton { - min-width: 50px !important; - min-height: 50px !important; + .playButton { + min-width: 50px !important; + min-height: 50px !important; - svg { - font-size: 1.6rem; - } - } - } + svg { + font-size: 1.6rem; + } + } + } - .player-seek_bar { - .progress { - .MuiSlider-root { - .MuiSlider-rail { - height: 7px; - } + .player-seek_bar { + .progress { + .MuiSlider-root { + .MuiSlider-rail { + height: 7px; + } - .MuiSlider-track { - height: 7px; - } + .MuiSlider-track { + height: 7px; + } - .MuiSlider-thumb { - width: 5px; - height: 13px; - border-radius: 2px; + .MuiSlider-thumb { + width: 5px; + height: 13px; + border-radius: 2px; - background-color: var(--background-color-contrast); - } - } - } - } - - .extra_actions { - padding: 0 30px; - - .ant-btn { - padding: 5px; - - svg { - height: 23px; - min-width: 23px; - } - } - } -} \ No newline at end of file + background-color: var(--background-color-contrast); + } + } + } + } +} diff --git a/packages/app/src/pages/_debug/audiometadata/index.jsx b/packages/app/src/pages/_debug/audiometadata/index.jsx new file mode 100644 index 00000000..c9aea4c8 --- /dev/null +++ b/packages/app/src/pages/_debug/audiometadata/index.jsx @@ -0,0 +1,47 @@ +import TrackManifest from "@cores/player/classes/TrackManifest" + +const D_Manifest = () => { + const [manifest, setManifest] = React.useState(null) + + function selectLocalFile() { + const input = document.createElement("input") + input.type = "file" + input.accept = "audio/*" + input.onchange = (e) => { + loadManifest(e.target.files[0]) + } + input.click() + } + + async function loadManifest(file) { + let track = new TrackManifest({ file: file }) + + await track.initialize() + + console.log(track) + + setManifest(track) + } + + return ( +
+

Select a local file to view & create a track manifest

+ + + + {manifest?.cover && ( + Cover + )} + + + {JSON.stringify(manifest)} + +
+ ) +} + +export default D_Manifest diff --git a/packages/app/src/pages/lyrics/components/controller/index.jsx b/packages/app/src/pages/lyrics/components/controller/index.jsx index 94265cf3..116c287d 100644 --- a/packages/app/src/pages/lyrics/components/controller/index.jsx +++ b/packages/app/src/pages/lyrics/components/controller/index.jsx @@ -177,7 +177,7 @@ const PlayerController = React.forwardRef((props, ref) => { )}
- {playerState.track_manifest?.metadata.lossless && ( + {playerState.track_manifest?.metadata?.lossless && ( { - const [playerState] = usePlayerStateContext() + const [playerState] = usePlayerStateContext() - const { lyrics } = props + const { lyrics } = props - const [initialLoading, setInitialLoading] = React.useState(true) - const [syncInterval, setSyncInterval] = React.useState(null) - const [syncingVideo, setSyncingVideo] = React.useState(false) - const [currentVideoLatency, setCurrentVideoLatency] = React.useState(0) - const hls = React.useRef(new HLS()) + const [initialLoading, setInitialLoading] = React.useState(true) + const [syncInterval, setSyncInterval] = React.useState(null) + const [syncingVideo, setSyncingVideo] = React.useState(false) + const [currentVideoLatency, setCurrentVideoLatency] = React.useState(0) + const hls = React.useRef(new HLS()) - async function seekVideoToSyncAudio() { - if (!lyrics) { - return null - } + async function seekVideoToSyncAudio() { + if (!lyrics) { + return null + } - if (!lyrics.video_source || typeof lyrics.sync_audio_at_ms === "undefined") { - return null - } + if ( + !lyrics.video_source || + typeof lyrics.sync_audio_at_ms === "undefined" + ) { + return null + } - const currentTrackTime = app.cores.player.controls.seek() + const currentTrackTime = app.cores.player.controls.seek() - setSyncingVideo(true) + setSyncingVideo(true) - let newTime = currentTrackTime + (lyrics.sync_audio_at_ms / 1000) + app.cores.player.gradualFadeMs / 1000 + let newTime = + currentTrackTime + lyrics.sync_audio_at_ms / 1000 + 150 / 1000 - // dec some ms to ensure the video seeks correctly - newTime -= 5 / 1000 + // dec some ms to ensure the video seeks correctly + newTime -= 5 / 1000 - videoRef.current.currentTime = newTime - } + videoRef.current.currentTime = newTime + } - async function syncPlayback() { - // if something is wrong, stop syncing - if (videoRef.current === null || !lyrics || !lyrics.video_source || typeof lyrics.sync_audio_at_ms === "undefined" || playerState.playback_status !== "playing") { - return stopSyncInterval() - } + async function syncPlayback() { + // if something is wrong, stop syncing + if ( + videoRef.current === null || + !lyrics || + !lyrics.video_source || + typeof lyrics.sync_audio_at_ms === "undefined" || + playerState.playback_status !== "playing" + ) { + return stopSyncInterval() + } - const currentTrackTime = app.cores.player.controls.seek() - const currentVideoTime = videoRef.current.currentTime - (lyrics.sync_audio_at_ms / 1000) + const currentTrackTime = app.cores.player.controls.seek() + const currentVideoTime = + videoRef.current.currentTime - lyrics.sync_audio_at_ms / 1000 - //console.log(`Current track time: ${currentTrackTime}, current video time: ${currentVideoTime}`) + //console.log(`Current track time: ${currentTrackTime}, current video time: ${currentVideoTime}`) - const maxOffset = maxLatencyInMs / 1000 - const currentVideoTimeDiff = Math.abs(currentVideoTime - currentTrackTime) + const maxOffset = maxLatencyInMs / 1000 + const currentVideoTimeDiff = Math.abs( + currentVideoTime - currentTrackTime, + ) - setCurrentVideoLatency(currentVideoTimeDiff) + setCurrentVideoLatency(currentVideoTimeDiff) - if (syncingVideo === true) { - return false - } + if (syncingVideo === true) { + return false + } - if (currentVideoTimeDiff > maxOffset) { - seekVideoToSyncAudio() - } - } + if (currentVideoTimeDiff > maxOffset) { + seekVideoToSyncAudio() + } + } - function startSyncInterval() { - setSyncInterval(setInterval(syncPlayback, 300)) - } + function startSyncInterval() { + setSyncInterval(setInterval(syncPlayback, 300)) + } - function stopSyncInterval() { - setSyncingVideo(false) - setSyncInterval(null) - clearInterval(syncInterval) - } + function stopSyncInterval() { + setSyncingVideo(false) + setSyncInterval(null) + clearInterval(syncInterval) + } - //* handle when player is loading - React.useEffect(() => { - if (lyrics?.video_source && playerState.loading === true && playerState.playback_status === "playing") { - videoRef.current.pause() - } + //* handle when player is loading + React.useEffect(() => { + if ( + lyrics?.video_source && + playerState.loading === true && + playerState.playback_status === "playing" + ) { + videoRef.current.pause() + } - if (lyrics?.video_source && playerState.loading === false && playerState.playback_status === "playing") { - videoRef.current.play() - } - }, [playerState.loading]) + if ( + lyrics?.video_source && + playerState.loading === false && + playerState.playback_status === "playing" + ) { + videoRef.current.play() + } + }, [playerState.loading]) - //* Handle when playback status change - React.useEffect(() => { - if (initialLoading === false) { - console.log(`VIDEO:: Playback status changed to ${playerState.playback_status}`) + //* Handle when playback status change + React.useEffect(() => { + if (initialLoading === false) { + console.log( + `VIDEO:: Playback status changed to ${playerState.playback_status}`, + ) - if (lyrics && lyrics.video_source) { - if (playerState.playback_status === "playing") { - videoRef.current.play() - startSyncInterval() - } else { - videoRef.current.pause() - stopSyncInterval() - } - } - } - }, [playerState.playback_status]) + if (lyrics && lyrics.video_source) { + if (playerState.playback_status === "playing") { + videoRef.current.play() + startSyncInterval() + } else { + videoRef.current.pause() + stopSyncInterval() + } + } + } + }, [playerState.playback_status]) - //* Handle when lyrics object change - React.useEffect(() => { - setCurrentVideoLatency(0) - stopSyncInterval() + //* Handle when lyrics object change + React.useEffect(() => { + setCurrentVideoLatency(0) + stopSyncInterval() - if (lyrics) { - if (lyrics.video_source) { - console.log("Loading video source >", lyrics.video_source) + if (lyrics) { + if (lyrics.video_source) { + console.log("Loading video source >", lyrics.video_source) - if (lyrics.video_source.endsWith(".mp4")) { - videoRef.current.src = lyrics.video_source - } else { - hls.current.loadSource(lyrics.video_source) - } + if (lyrics.video_source.endsWith(".mp4")) { + videoRef.current.src = lyrics.video_source + } else { + hls.current.loadSource(lyrics.video_source) + } - if (typeof lyrics.sync_audio_at_ms !== "undefined") { - videoRef.current.loop = false - videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000 + if (typeof lyrics.sync_audio_at_ms !== "undefined") { + videoRef.current.loop = false + videoRef.current.currentTime = + lyrics.sync_audio_at_ms / 1000 - startSyncInterval() - } else { - videoRef.current.loop = true - videoRef.current.currentTime = 0 - } + startSyncInterval() + } else { + videoRef.current.loop = true + videoRef.current.currentTime = 0 + } - if (playerState.playback_status === "playing") { - videoRef.current.play() - } - } - } + if (playerState.playback_status === "playing") { + videoRef.current.play() + } + } + } - setInitialLoading(false) - }, [lyrics]) + setInitialLoading(false) + }, [lyrics]) - React.useEffect(() => { - videoRef.current.addEventListener("seeked", (event) => { - setSyncingVideo(false) - }) + React.useEffect(() => { + videoRef.current.addEventListener("seeked", (event) => { + setSyncingVideo(false) + }) - hls.current.attachMedia(videoRef.current) + hls.current.attachMedia(videoRef.current) - return () => { - stopSyncInterval() - } - }, []) + return () => { + stopSyncInterval() + } + }, []) - return <> - { - props.lyrics?.sync_audio_at &&
-
-

Maximun latency

-

{maxLatencyInMs}ms

-
-
-

Video Latency

-

{(currentVideoLatency * 1000).toFixed(2)}ms

-
- {syncingVideo ?

Syncing video...

: null} -
- } + return ( + <> + {props.lyrics?.sync_audio_at && ( +
+
+

Maximun latency

+

{maxLatencyInMs}ms

+
+
+

Video Latency

+

{(currentVideoLatency * 1000).toFixed(2)}ms

+
+ {syncingVideo ?

Syncing video...

: null} +
+ )} -