mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-21 04:59:53 +02:00
play on next when paused
This commit is contained in:
+2
-2
@@ -65,7 +65,7 @@
|
|||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"lint": "expo lint",
|
"lint": "expo lint",
|
||||||
"test:queue-actions": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/queue/queueActions.test.mts src/components/queue/queuePerformance.test.mts src/components/queue/virtualQueuePaging.test.mts src/components/swipeableRowState.test.mts",
|
"test:queue-actions": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/queue/queueActions.test.mts src/components/queue/queuePerformance.test.mts src/components/queue/virtualQueuePaging.test.mts src/components/swipeableRowState.test.mts",
|
||||||
"test:desktop-remote": "node --experimental-strip-types --test src/services/desktopRemotePairing.test.mts",
|
"test:desktop-remote": "node --experimental-strip-types --test src/services/desktopRemotePairing.test.mts src/services/desktopRemoteTransport.test.mts",
|
||||||
"test:dynamic-playlists": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/shared/playlists/dynamicPlaylist.test.mts",
|
"test:dynamic-playlists": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/shared/playlists/dynamicPlaylist.test.mts",
|
||||||
"test:album-grouping": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/shared/library/albumGrouping.test.mts src/shared/library/albumEligibility.test.mts src/library/albumIdentity.test.mts src/library/albumSummary.test.mts",
|
"test:album-grouping": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/shared/library/albumGrouping.test.mts src/shared/library/albumEligibility.test.mts src/library/albumIdentity.test.mts src/library/albumSummary.test.mts",
|
||||||
"test:artist-grouping": "node --experimental-strip-types --test src/library/artistGrouping.test.mts src/audio/artistCreditTransport.test.mts",
|
"test:artist-grouping": "node --experimental-strip-types --test src/library/artistGrouping.test.mts src/audio/artistCreditTransport.test.mts",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
"test:signal": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/audio/signalShare.test.mts src/audio/signalShareIntent.test.mts src/audio/signalScanGeometry.test.mts src/audio/signalLocalMatch.test.mts",
|
"test:signal": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/audio/signalShare.test.mts src/audio/signalShareIntent.test.mts src/audio/signalScanGeometry.test.mts src/audio/signalLocalMatch.test.mts",
|
||||||
"test:eq-math": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/audio/eq.test.mts",
|
"test:eq-math": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/audio/eq.test.mts",
|
||||||
"test:audio-startup": "node --experimental-strip-types --test src/audio/dspStartupCoordinator.test.mts src/audio/dspStartupGain.test.mts",
|
"test:audio-startup": "node --experimental-strip-types --test src/audio/dspStartupCoordinator.test.mts src/audio/dspStartupGain.test.mts",
|
||||||
"test:seek-bar": "node --experimental-strip-types --test src/audio/playbackClock.test.mts src/audio/playbackProgressProjection.test.mts src/components/waveformScrubDetents.test.mts",
|
"test:seek-bar": "node --experimental-strip-types --test src/audio/playbackClock.test.mts src/audio/playbackNavigation.test.mts src/audio/playbackProgressProjection.test.mts src/components/waveformScrubDetents.test.mts",
|
||||||
"test:recent-play": "node --experimental-strip-types --test src/audio/recentPlayTracking.test.mts",
|
"test:recent-play": "node --experimental-strip-types --test src/audio/recentPlayTracking.test.mts",
|
||||||
"test:lyrics": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/lyrics/parsing.test.mts src/lyrics/presentation.test.mts src/lyrics/displaySettings.test.mts src/lyrics/embedded.test.mts src/lyrics/resolver.test.mts",
|
"test:lyrics": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/lyrics/parsing.test.mts src/lyrics/presentation.test.mts src/lyrics/displaySettings.test.mts src/lyrics/embedded.test.mts src/lyrics/resolver.test.mts",
|
||||||
"test:sleep": "node --experimental-strip-types --test src/audio/sleepTimerState.test.mts",
|
"test:sleep": "node --experimental-strip-types --test src/audio/sleepTimerState.test.mts",
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ import {
|
|||||||
prepareAudioProcessingForPlayback,
|
prepareAudioProcessingForPlayback,
|
||||||
primePreparedTrackForPlayback,
|
primePreparedTrackForPlayback,
|
||||||
} from './audioProcessingStartup';
|
} from './audioProcessingStartup';
|
||||||
import { shouldRestartOnPrevious } from './playbackNavigation';
|
import {
|
||||||
|
shouldRestartOnPrevious,
|
||||||
|
shouldResumeAfterExplicitNext,
|
||||||
|
} from './playbackNavigation';
|
||||||
import {
|
import {
|
||||||
cancelManualRecentPlayTransition,
|
cancelManualRecentPlayTransition,
|
||||||
markManualRecentPlayTransition,
|
markManualRecentPlayTransition,
|
||||||
@@ -815,15 +818,19 @@ export async function togglePlay(): Promise<void> {
|
|||||||
export async function skipToNext(): Promise<void> {
|
export async function skipToNext(): Promise<void> {
|
||||||
markNowPlayingTrackTransitionDirection('next', 'phone');
|
markNowPlayingTrackTransitionDirection('next', 'phone');
|
||||||
await ensurePlayerReady();
|
await ensurePlayerReady();
|
||||||
const [nativeQueue, nativeIndex] = await Promise.all([
|
const [nativeQueue, nativeIndex, nativePlaybackState] = await Promise.all([
|
||||||
TrackPlayer.getQueue(),
|
TrackPlayer.getQueue(),
|
||||||
TrackPlayer.getActiveTrackIndex(),
|
TrackPlayer.getActiveTrackIndex(),
|
||||||
|
TrackPlayer.getPlaybackState(),
|
||||||
]);
|
]);
|
||||||
await prepareAudioProcessingForPlayback(
|
const playbackStateAtIntent = mapRntpState(nativePlaybackState.state);
|
||||||
dspTargetFromTrack(
|
const resumeAfterSkip = shouldResumeAfterExplicitNext(playbackStateAtIntent);
|
||||||
|
const playbackTarget = dspTargetFromTrack(
|
||||||
nativeIndex == null ? undefined : nativeQueue[nativeIndex + 1],
|
nativeIndex == null ? undefined : nativeQueue[nativeIndex + 1],
|
||||||
'none',
|
'none',
|
||||||
),
|
);
|
||||||
|
await prepareAudioProcessingForPlayback(
|
||||||
|
playbackTarget,
|
||||||
'skip-next',
|
'skip-next',
|
||||||
);
|
);
|
||||||
const { tracks, activeIndex } = useQueueStore.getState();
|
const { tracks, activeIndex } = useQueueStore.getState();
|
||||||
@@ -831,7 +838,10 @@ export async function skipToNext(): Promise<void> {
|
|||||||
const manualTransitionFromPath = usePlayerStore.getState().currentTrack?.path ?? null;
|
const manualTransitionFromPath = usePlayerStore.getState().currentTrack?.path ?? null;
|
||||||
if (nextIndex >= 0 && nextIndex < tracks.length) {
|
if (nextIndex >= 0 && nextIndex < tracks.length) {
|
||||||
useQueueStore.getState().setActiveIndex(nextIndex);
|
useQueueStore.getState().setActiveIndex(nextIndex);
|
||||||
setOptimisticTrack(tracks[nextIndex], usePlayerStore.getState().playbackState);
|
setOptimisticTrack(
|
||||||
|
tracks[nextIndex],
|
||||||
|
resumeAfterSkip ? 'loading' : playbackStateAtIntent,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const manualTransition = markManualRecentPlayTransition(manualTransitionFromPath);
|
const manualTransition = markManualRecentPlayTransition(manualTransitionFromPath);
|
||||||
let nativeTransitionSucceeded = false;
|
let nativeTransitionSucceeded = false;
|
||||||
@@ -839,10 +849,15 @@ export async function skipToNext(): Promise<void> {
|
|||||||
await TrackPlayer.skipToNext();
|
await TrackPlayer.skipToNext();
|
||||||
nativeTransitionSucceeded = true;
|
nativeTransitionSucceeded = true;
|
||||||
await refreshActiveIndexFromNative();
|
await refreshActiveIndexFromNative();
|
||||||
|
if (resumeAfterSkip) {
|
||||||
|
await primePreparedTrackForPlayback(playbackTarget, 'skip-next-resume');
|
||||||
|
await TrackPlayer.play();
|
||||||
|
usePlayerStore.getState().setPlaybackState('playing');
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (!nativeTransitionSucceeded) cancelManualRecentPlayTransition(manualTransition);
|
if (!nativeTransitionSucceeded) cancelManualRecentPlayTransition(manualTransition);
|
||||||
await reconcilePlayerFromNative();
|
await reconcilePlayerFromNative();
|
||||||
// no next track — ignore
|
// No next track or resume failed — keep the reconciled native state.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import {
|
import {
|
||||||
PREVIOUS_RESTART_THRESHOLD_SECONDS,
|
PREVIOUS_RESTART_THRESHOLD_SECONDS,
|
||||||
|
shouldResumeAfterExplicitNext,
|
||||||
shouldRestartOnPrevious,
|
shouldRestartOnPrevious,
|
||||||
} from './playbackNavigation.ts';
|
} from './playbackNavigation.ts';
|
||||||
|
|
||||||
@@ -16,3 +17,10 @@ test('invalid and negative positions retain previous-track behavior', () => {
|
|||||||
assert.equal(shouldRestartOnPrevious(Number.NaN), false);
|
assert.equal(shouldRestartOnPrevious(Number.NaN), false);
|
||||||
assert.equal(shouldRestartOnPrevious(Number.POSITIVE_INFINITY), false);
|
assert.equal(shouldRestartOnPrevious(Number.POSITIVE_INFINITY), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('only a paused explicit Next intent resumes playback', () => {
|
||||||
|
assert.equal(shouldResumeAfterExplicitNext('paused'), true);
|
||||||
|
assert.equal(shouldResumeAfterExplicitNext('playing'), false);
|
||||||
|
assert.equal(shouldResumeAfterExplicitNext('loading'), false);
|
||||||
|
assert.equal(shouldResumeAfterExplicitNext('stopped'), false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { PlaybackState } from '@/types/audio';
|
||||||
|
|
||||||
export const PREVIOUS_RESTART_THRESHOLD_SECONDS = 3;
|
export const PREVIOUS_RESTART_THRESHOLD_SECONDS = 3;
|
||||||
|
|
||||||
/** Match desktop Astra: restart only after crossing the previous-track cutoff. */
|
/** Match desktop Astra: restart only after crossing the previous-track cutoff. */
|
||||||
@@ -7,3 +9,13 @@ export function shouldRestartOnPrevious(positionSeconds: number): boolean {
|
|||||||
positionSeconds > PREVIOUS_RESTART_THRESHOLD_SECONDS
|
positionSeconds > PREVIOUS_RESTART_THRESHOLD_SECONDS
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An explicit Next press while paused is also an instruction to resume.
|
||||||
|
* Loading, stopped, and already-playing sessions retain their native behavior.
|
||||||
|
*/
|
||||||
|
export function shouldResumeAfterExplicitNext(
|
||||||
|
playbackState: PlaybackState,
|
||||||
|
): boolean {
|
||||||
|
return playbackState === 'paused';
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { desktopRemoteControlSequence } from './desktopRemoteTransport.ts';
|
||||||
|
|
||||||
|
test('paused desktop Next advances before resuming playback', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
desktopRemoteControlSequence('next', 'paused'),
|
||||||
|
['next', 'play'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('desktop Next preserves non-paused transport behavior', () => {
|
||||||
|
assert.deepEqual(desktopRemoteControlSequence('next', 'playing'), ['next']);
|
||||||
|
assert.deepEqual(desktopRemoteControlSequence('next', 'loading'), ['next']);
|
||||||
|
assert.deepEqual(desktopRemoteControlSequence('next', 'stopped'), ['next']);
|
||||||
|
assert.deepEqual(desktopRemoteControlSequence('next', undefined), ['next']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('other desktop commands are never expanded', () => {
|
||||||
|
assert.deepEqual(desktopRemoteControlSequence('previous', 'paused'), ['previous']);
|
||||||
|
assert.deepEqual(desktopRemoteControlSequence('play', 'paused'), ['play']);
|
||||||
|
assert.deepEqual(desktopRemoteControlSequence('seek', 'paused'), ['seek']);
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { shouldResumeAfterExplicitNext } from '../audio/playbackNavigation.ts';
|
||||||
|
import type {
|
||||||
|
DesktopRemoteControlCommand,
|
||||||
|
DesktopRemotePlaybackState,
|
||||||
|
} from '@/types/desktopRemote';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The desktop protocol exposes discrete transport commands. Expand a paused
|
||||||
|
* Next intent into ordered Next + Play requests so the transition also resumes.
|
||||||
|
*/
|
||||||
|
export function desktopRemoteControlSequence(
|
||||||
|
command: DesktopRemoteControlCommand,
|
||||||
|
playbackState: DesktopRemotePlaybackState | undefined,
|
||||||
|
): DesktopRemoteControlCommand[] {
|
||||||
|
if (
|
||||||
|
command === 'next'
|
||||||
|
&& playbackState
|
||||||
|
&& shouldResumeAfterExplicitNext(playbackState)
|
||||||
|
) {
|
||||||
|
return ['next', 'play'];
|
||||||
|
}
|
||||||
|
return [command];
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import { AstraLibraryData } from '../../modules/astra-library-scanner';
|
|||||||
import { useDesktopSyncStore } from '@/stores/desktopSyncStore';
|
import { useDesktopSyncStore } from '@/stores/desktopSyncStore';
|
||||||
import { identityMatchesPinnedConnection } from '@/services/desktopSyncPolicy';
|
import { identityMatchesPinnedConnection } from '@/services/desktopSyncPolicy';
|
||||||
import { ensureDesktopRemoteCredentialsFresh } from '@/services/desktopRemoteSession';
|
import { ensureDesktopRemoteCredentialsFresh } from '@/services/desktopRemoteSession';
|
||||||
|
import { desktopRemoteControlSequence } from '@/services/desktopRemoteTransport';
|
||||||
import { usePlaybackTargetStore } from '@/stores/playbackTargetStore';
|
import { usePlaybackTargetStore } from '@/stores/playbackTargetStore';
|
||||||
import type {
|
import type {
|
||||||
DesktopRemoteConnection,
|
DesktopRemoteConnection,
|
||||||
@@ -732,16 +733,22 @@ export const useDesktopRemoteStore = create<DesktopRemoteStore>((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
sendControl: async (command, time) => {
|
sendControl: async (command, time) => {
|
||||||
const { connection, token } = get();
|
const { connection, token, snapshot } = get();
|
||||||
if (!connection || !token) return;
|
if (!connection || !token) return;
|
||||||
try {
|
try {
|
||||||
|
const commands = desktopRemoteControlSequence(
|
||||||
|
command,
|
||||||
|
snapshot?.playbackState,
|
||||||
|
);
|
||||||
|
for (const nextCommand of commands) {
|
||||||
await sendDesktopRemoteControl(
|
await sendDesktopRemoteControl(
|
||||||
connection.baseUrl,
|
connection.baseUrl,
|
||||||
token,
|
token,
|
||||||
connection.certificateFingerprint,
|
connection.certificateFingerprint,
|
||||||
command,
|
nextCommand,
|
||||||
time
|
nextCommand === command ? time : undefined,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
set({ errorMessage: '' });
|
set({ errorMessage: '' });
|
||||||
if (command === 'seek' && typeof time === 'number') {
|
if (command === 'seek' && typeof time === 'number') {
|
||||||
set((state) => state.snapshot
|
set((state) => state.snapshot
|
||||||
|
|||||||
Reference in New Issue
Block a user