diff --git a/src/audio/playbackController.ts b/src/audio/playbackController.ts index ab58b69..ca22703 100644 --- a/src/audio/playbackController.ts +++ b/src/audio/playbackController.ts @@ -26,6 +26,7 @@ import { prepareAudioProcessingForPlayback, primePreparedTrackForPlayback, } from './audioProcessingStartup'; +import { shouldRestartOnPrevious } from './playbackNavigation'; // If a background queue fill dies partway, the mirror no longer matches the // native queue — re-read the truth. @@ -481,6 +482,20 @@ export async function skipToNext(): Promise { export async function skipToPrevious(): Promise { await ensurePlayerReady(); + + // Use RNTP's position so headless Bluetooth/Auto commands do not depend on + // the UI-mounted progress mirror. A failed read preserves the old skip path. + let nativePosition: number | null = null; + try { + nativePosition = (await TrackPlayer.getProgress()).position; + } catch { + // Fall through to the existing previous-track behavior. + } + if (nativePosition != null && shouldRestartOnPrevious(nativePosition)) { + await seekTo(0); + return; + } + const [nativeQueue, nativeIndex] = await Promise.all([ TrackPlayer.getQueue(), TrackPlayer.getActiveTrackIndex(), diff --git a/src/audio/playbackNavigation.test.mts b/src/audio/playbackNavigation.test.mts new file mode 100644 index 0000000..1161619 --- /dev/null +++ b/src/audio/playbackNavigation.test.mts @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + PREVIOUS_RESTART_THRESHOLD_SECONDS, + shouldRestartOnPrevious, +} from './playbackNavigation.ts'; + +test('restarts only after crossing the three-second threshold', () => { + assert.equal(shouldRestartOnPrevious(3.001), true); + assert.equal(shouldRestartOnPrevious(PREVIOUS_RESTART_THRESHOLD_SECONDS), false); + assert.equal(shouldRestartOnPrevious(0), false); +}); + +test('invalid and negative positions retain previous-track behavior', () => { + assert.equal(shouldRestartOnPrevious(-1), false); + assert.equal(shouldRestartOnPrevious(Number.NaN), false); + assert.equal(shouldRestartOnPrevious(Number.POSITIVE_INFINITY), false); +}); diff --git a/src/audio/playbackNavigation.ts b/src/audio/playbackNavigation.ts new file mode 100644 index 0000000..a4c4dac --- /dev/null +++ b/src/audio/playbackNavigation.ts @@ -0,0 +1,9 @@ +export const PREVIOUS_RESTART_THRESHOLD_SECONDS = 3; + +/** Match desktop Astra: restart only after crossing the previous-track cutoff. */ +export function shouldRestartOnPrevious(positionSeconds: number): boolean { + return ( + Number.isFinite(positionSeconds) && + positionSeconds > PREVIOUS_RESTART_THRESHOLD_SECONDS + ); +}