Now Playing: complete Media Session lock-screen controls
The Media Session API was partial — play/pause/stop/seek±10/prev/next handlers + metadata/artwork existed, but the OS lock-screen/Bluetooth/notification control had a DEAD scrubber (no position, no drag-to-seek). Completed it: - setPositionState (duration/position/rate) so the lock screen shows a live progress bar, pushed throttled (~1/s) from timeupdate, reset on loadedmetadata of a new track, and on manual seek. - 'seekto' action handler so dragging the lock-screen/notification scrubber actually seeks (with fastSeek when available). Now hardware/Bluetooth keys + the lock-screen scrubber fully drive playback with art, metadata, and live position. Feature-detected throughout.
This commit is contained in:
parent
843f4081cf
commit
866f2e4a23
1 changed files with 39 additions and 0 deletions
|
|
@ -17,6 +17,7 @@ function initializeMediaPlayer() {
|
||||||
// Set up audio event listeners
|
// Set up audio event listeners
|
||||||
audioPlayer.addEventListener('timeupdate', updateAudioProgress);
|
audioPlayer.addEventListener('timeupdate', updateAudioProgress);
|
||||||
audioPlayer.addEventListener('timeupdate', npCrossfadeTick);
|
audioPlayer.addEventListener('timeupdate', npCrossfadeTick);
|
||||||
|
audioPlayer.addEventListener('timeupdate', npThrottledPositionState);
|
||||||
audioPlayer.addEventListener('ended', onAudioEnded);
|
audioPlayer.addEventListener('ended', onAudioEnded);
|
||||||
audioPlayer.addEventListener('error', onAudioError);
|
audioPlayer.addEventListener('error', onAudioError);
|
||||||
audioPlayer.addEventListener('loadstart', onAudioLoadStart);
|
audioPlayer.addEventListener('loadstart', onAudioLoadStart);
|
||||||
|
|
@ -165,6 +166,8 @@ function setTrackInfo(track) {
|
||||||
updateNpTrackInfo();
|
updateNpTrackInfo();
|
||||||
updateMediaSessionMetadata();
|
updateMediaSessionMetadata();
|
||||||
updateMediaSessionPlaybackState();
|
updateMediaSessionPlaybackState();
|
||||||
|
// Reset the lock-screen scrubber when duration becomes known for the new track.
|
||||||
|
if (audioPlayer) audioPlayer.addEventListener('loadedmetadata', updateMediaSessionPositionState, { once: true });
|
||||||
|
|
||||||
// Kick off lyrics fetch for the new track. The panel stays
|
// Kick off lyrics fetch for the new track. The panel stays
|
||||||
// collapsed by default — fetching in the background means the
|
// collapsed by default — fetching in the background means the
|
||||||
|
|
@ -1954,6 +1957,7 @@ function handleNpProgressBarChange(event) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
audioPlayer.currentTime = newTime;
|
audioPlayer.currentTime = newTime;
|
||||||
|
updateMediaSessionPositionState();
|
||||||
|
|
||||||
// Sync sidebar progress
|
// Sync sidebar progress
|
||||||
const sidebarBar = document.getElementById('progress-bar');
|
const sidebarBar = document.getElementById('progress-bar');
|
||||||
|
|
@ -2995,6 +2999,41 @@ function initMediaSession() {
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||||
if (npQueue.length > 0) playNextInQueue();
|
if (npQueue.length > 0) playNextInQueue();
|
||||||
});
|
});
|
||||||
|
// Scrub from the lock screen / notification scrubber.
|
||||||
|
try {
|
||||||
|
navigator.mediaSession.setActionHandler('seekto', (details) => {
|
||||||
|
if (!audioPlayer || !isFinite(audioPlayer.duration)) return;
|
||||||
|
if (details.fastSeek && 'fastSeek' in audioPlayer) {
|
||||||
|
audioPlayer.fastSeek(details.seekTime);
|
||||||
|
} else if (typeof details.seekTime === 'number') {
|
||||||
|
audioPlayer.currentTime = details.seekTime;
|
||||||
|
}
|
||||||
|
updateMediaSessionPositionState();
|
||||||
|
});
|
||||||
|
} catch (e) { /* some browsers don't support seekto — handlers above still work */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeupdate fires ~4x/s; only push position to the OS ~1x/s.
|
||||||
|
let _npPosStateLast = 0;
|
||||||
|
function npThrottledPositionState() {
|
||||||
|
const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
||||||
|
if (now - _npPosStateLast < 950) return;
|
||||||
|
_npPosStateLast = now;
|
||||||
|
updateMediaSessionPositionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feeds the lock-screen scrubber its progress (elapsed / duration / rate).
|
||||||
|
// Without this the OS shows a dead, position-less media control.
|
||||||
|
function updateMediaSessionPositionState() {
|
||||||
|
if (!('mediaSession' in navigator) || !('setPositionState' in navigator.mediaSession)) return;
|
||||||
|
if (!audioPlayer || !isFinite(audioPlayer.duration) || audioPlayer.duration <= 0) return;
|
||||||
|
try {
|
||||||
|
navigator.mediaSession.setPositionState({
|
||||||
|
duration: audioPlayer.duration,
|
||||||
|
playbackRate: audioPlayer.playbackRate || 1,
|
||||||
|
position: Math.min(audioPlayer.currentTime, audioPlayer.duration),
|
||||||
|
});
|
||||||
|
} catch (e) { /* invalid state (e.g. mid-load) — skip this tick */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMediaSessionMetadata() {
|
function updateMediaSessionMetadata() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue