summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/music-player.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-player.js92
1 files changed, 48 insertions, 44 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
index a4a8c5c..fcf122f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
@@ -1,6 +1,7 @@
import {
- html, useState, useEffect, useRef, useCallback,
+ html, useState, useEffect, useReducer, useRef, useCallback,
} from './vendor/htm-preact.js';
+import { queueReducer, emptyQueue } from './queue-ops.js';
import { t } from './i18n.js';
import { Icon } from './icon.js';
import { useStickyBand } from './sticky.js';
@@ -52,21 +53,6 @@ function formatTime(seconds) {
return `${m}:${String(s).padStart(2, '0')}`;
}
-function shuffledOrder(n, keepFirst) {
- const order = Array.from({ length: n }, (_, i) => i);
- // Fisher-Yates, then move keepFirst to the front so shuffling on doesn't
- // interrupt whatever is already playing.
- for (let i = order.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [order[i], order[j]] = [order[j], order[i]];
- }
- if (keepFirst != null) {
- const at = order.indexOf(keepFirst);
- if (at > 0) { order.splice(at, 1); order.unshift(keepFirst); }
- }
- return order;
-}
-
// Bounded: only the currently playing track plus the read-ahead window are
// ever worth holding in memory. Older blob URLs are revoked, not merely
// dropped — otherwise every track played in a session leaks its object URL.
@@ -188,9 +174,12 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs }) {
const blobInsertRef = useRef(0);
const loadTokenRef = useRef(0);
- const [tracks, setTracks] = useState([]);
- const [order, setOrder] = useState([]);
- const [pos, setPos] = useState(0); // index into `order`
+ // The queue: every entry, the play order over it, and where in that order
+ // we are (docs/playlists.md §9.3). One reducer rather than three useStates
+ // because "add to queue" derives its new indices from the current track
+ // count, and two appends batched into one tick cannot both see it.
+ const [queueState, dispatch] = useReducer(queueReducer, null, emptyQueue);
+ const { tracks, order, pos } = queueState;
const [playing, setPlaying] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
@@ -209,14 +198,15 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs }) {
// through the whole list in an instant.
const consecutiveFailuresRef = useRef(0);
const MAX_CONSECUTIVE_FAILURES = 5;
+ const prefetchAfterInsertRef = useRef(false);
const currentTrack = tracks[order[pos]] || null;
const advancePastFailure = useCallback(() => {
consecutiveFailuresRef.current += 1;
if (consecutiveFailuresRef.current > MAX_CONSECUTIVE_FAILURES || order.length <= 1) return;
- if (pos + 1 < order.length) setPos(pos + 1);
- else if (repeat === 'all') setPos(0);
+ if (pos + 1 < order.length) dispatch({ type: 'skipTo', pos: pos + 1 });
+ else if (repeat === 'all') dispatch({ type: 'skipTo', pos: 0 });
}, [order.length, pos, repeat]);
// Stops playback the moment this bar goes away for any reason -- the
@@ -342,17 +332,38 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs }) {
// effect always re-runs rather than bailing out on reference equality.
useEffect(() => {
if (!queue) return;
- const n = queue.tracks.length;
- const initialOrder = shuffle ? shuffledOrder(n, queue.startIndex) : Array.from({ length: n }, (_, i) => i);
- const startPos = shuffle ? 0 : queue.startIndex;
- setTracks(queue.tracks);
- setOrder(initialOrder);
- setPos(startPos);
- setError('');
+ const op = queue.op || 'replace';
+ dispatch({
+ type: op === 'next' ? 'insertNext' : op === 'append' ? 'append' : 'replace',
+ tracks: queue.tracks,
+ startIndex: queue.startIndex,
+ shuffle,
+ });
+ // A deliberate queue change is new material to try, so the runaway bound
+ // starts over; the bound exists to stop an automatic advance burning
+ // through a bad queue, not to hold a user's own action against them.
consecutiveFailuresRef.current = 0;
+ if (op === 'replace') {
+ setError('');
+ } else {
+ // Nothing that is playing changed, so the play effect below will not
+ // run and will not warm anything — but "play next" just put a track at
+ // pos + 1 that nobody has fetched. Flagged here, acted on once `order`
+ // has actually been rebuilt.
+ prefetchAfterInsertRef.current = true;
+ }
// Playback itself starts from the effect below, keyed on [tracks, order, pos].
}, [queue]);
+ useEffect(() => {
+ if (!prefetchAfterInsertRef.current) return;
+ prefetchAfterInsertRef.current = false;
+ if (order.length) prefetchNext(pos);
+ // Deliberately not keyed on `pos`: this runs for a queue edit, and the
+ // ordinary advance is already covered where the current track loads.
+ // eslint-disable-next-line
+ }, [order]);
+
// Loads and plays whatever `pos` now points to. Runs after the queue
// effect above (pos/order/tracks all just changed together) and also
// after skipNext/skipPrev/onEnded update `pos` alone.
@@ -406,7 +417,7 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs }) {
saveVolume(volume);
}, [volume]);
- const skipTo = useCallback((newPos) => setPos(newPos), []);
+ const skipTo = useCallback((newPos) => dispatch({ type: 'skipTo', pos: newPos }), []);
const skipNext = useCallback(() => {
if (order.length === 0) return;
@@ -441,22 +452,15 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs }) {
else { audio.play().then(() => setPlaying(true)).catch(() => {}); }
}, [playing]);
+ // Reshuffling keeps the currently playing track in place — turning shuffle
+ // on mid-album must not interrupt what's already playing. Which track that
+ // is, is `order[pos]`; see queue-ops.js on why it is not looked up by id.
const toggleShuffle = useCallback(() => {
- setShuffle((prev) => {
- const next = !prev;
- saveShuffle(next);
- // Reshuffling keeps the currently playing track in place — turning
- // shuffle on mid-album must not interrupt what's already playing.
- const currentId = tracks[order[pos]] && tracks[order[pos]].id;
- const currentIdx = tracks.findIndex((tr) => tr.id === currentId);
- const newOrder = next
- ? shuffledOrder(tracks.length, currentIdx)
- : Array.from({ length: tracks.length }, (_, i) => i);
- setOrder(newOrder);
- setPos(next ? 0 : currentIdx);
- return next;
- });
- }, [tracks, order, pos]);
+ const next = !shuffle;
+ setShuffle(next);
+ saveShuffle(next);
+ dispatch({ type: 'reshuffle', shuffle: next });
+ }, [shuffle]);
const cycleRepeat = useCallback(() => {
setRepeat((prev) => {