Skip to content

Commit 46d5338

Browse files
authored
Merge pull request #1598 from root-intruder/redesign
fixes #1020 Pressing "Next track" via headphones will skip to next track but pause
2 parents 524a0fc + 68692bb commit 46d5338

1 file changed

Lines changed: 45 additions & 0 deletions

File tree

lib/services/music_player_background_task.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
141141
final _volumeNormalizationLogger = Logger("VolumeNormalization");
142142
final _outputLogger = Logger("Output");
143143

144+
/// Time window used to ignore spurious play/pause callbacks immediately
145+
/// after a skip command from certain Bluetooth headsets. Behavior before this fix was that a double-tap on such headsets would trigger a skip followed by an unintended pause, because the headset sent play/pause events immediately after the skip event. With this guard, if a play/pause event is received within this window after a skip command, it will be ignored.
146+
static const Duration _skipPlayPauseGuardWindow = Duration(milliseconds: 50);
147+
148+
/// Timestamp of the most recent explicit skip command (next/previous).
149+
DateTime? _lastSkipCommandAt;
150+
144151
// Init the new sleep timer with a length of 0
145152
// SleepTimer sleepTimer = SleepTimer(SleepTimerType.duration, 0);
146153

@@ -164,6 +171,24 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
164171

165172
final outputSwitcherChannel = MethodChannel('com.unicornsonlsd.finamp/output_switcher');
166173

174+
/// Some Bluetooth headsets send skip and pause/play media button events in
175+
/// very quick succession for a double-tap skip gesture. This guard ignores a
176+
/// trailing play/pause event if it arrives right after skip, preventing an
177+
/// unintended pause while still allowing normal controls outside the window.
178+
bool get _shouldIgnorePlayPauseAfterRecentSkip {
179+
final lastSkipCommandAt = _lastSkipCommandAt;
180+
if (lastSkipCommandAt == null) return false;
181+
182+
final elapsed = DateTime.now().difference(lastSkipCommandAt);
183+
if (elapsed <= _skipPlayPauseGuardWindow) {
184+
_audioServiceBackgroundTaskLogger.fine(
185+
"Ignoring play/pause because skip was ${elapsed.inMilliseconds}ms ago (threshold ${_skipPlayPauseGuardWindow.inMilliseconds}ms)",
186+
);
187+
return true;
188+
}
189+
return false;
190+
}
191+
167192
Future<void> showOutputSwitcherDialog() async {
168193
if (!Platform.isAndroid) {
169194
return;
@@ -583,6 +608,12 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
583608

584609
@override
585610
Future<void> play({bool disableFade = false}) async {
611+
_audioServiceBackgroundTaskLogger.fine(
612+
"play() start: disableFade=$disableFade, playing=${_player.playing}, fadeDirection=${fadeState.value.fadeDirection}, currentIndex=${_player.currentIndex}, position=${_player.position}",
613+
);
614+
if (_shouldIgnorePlayPauseAfterRecentSkip) {
615+
return;
616+
}
586617
if (!disableFade && FinampSettingsHelper.finampSettings.audioFadeInDuration > Duration.zero) {
587618
return fadeInAndPlay();
588619
} else {
@@ -608,6 +639,12 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
608639

609640
@override
610641
Future<void> pause({bool disableFade = false}) async {
642+
_audioServiceBackgroundTaskLogger.fine(
643+
"pause() start: disableFade=$disableFade, playing=${_player.playing}, fadeDirection=${fadeState.value.fadeDirection}, currentIndex=${_player.currentIndex}, position=${_player.position}",
644+
);
645+
if (_shouldIgnorePlayPauseAfterRecentSkip) {
646+
return;
647+
}
611648
if (!disableFade && FinampSettingsHelper.finampSettings.audioFadeOutDuration > Duration.zero) {
612649
return fadeOutAndPause();
613650
} else {
@@ -771,6 +808,10 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
771808

772809
@override
773810
Future<void> skipToPrevious({bool forceSkip = false}) async {
811+
_audioServiceBackgroundTaskLogger.fine(
812+
"skipToPrevious() start: forceSkip=$forceSkip, playing=${_player.playing}, fadeDirection=${fadeState.value.fadeDirection}, hasPrevious=${_player.hasPrevious}, loopMode=${_player.loopMode}, currentIndex=${_player.currentIndex}, position=${_player.position}",
813+
);
814+
_lastSkipCommandAt = DateTime.now();
774815
bool doSkip = true;
775816

776817
try {
@@ -803,6 +844,10 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
803844

804845
@override
805846
Future<void> skipToNext() async {
847+
_audioServiceBackgroundTaskLogger.fine(
848+
"skipToNext() start: playing=${_player.playing}, fadeDirection=${fadeState.value.fadeDirection}, hasNext=${_player.hasNext}, loopMode=${_player.loopMode}, currentIndex=${_player.currentIndex}, position=${_player.position}",
849+
);
850+
_lastSkipCommandAt = DateTime.now();
806851
try {
807852
if (_player.loopMode == LoopMode.one || !_player.hasNext) {
808853
// if the user manually skips to the next track, they probably want to actually skip to the next track

0 commit comments

Comments
 (0)