mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
fix: propagate player-discovered duration to OS media session
Tracks opened from Home/Album/Liked Album pages can carry an unknown (zero) durationMs from the metadata source, which was forwarded verbatim to audio_service as Duration.zero. Android's media notification then rendered 0:00 / 0:00 with no working seek bar, and macOS Now Playing showed --:-- (issue #3077), even though the in-app player UI showed the correct duration via the player's durationStream. WindowsAudioService already forwards audioPlayer.durationStream to SMTC (setEndTime), but MobileAudioService (Android/macOS/Linux) never updated the active MediaItem after the player discovered the real duration. - Report an unknown (zero) model duration as null instead of Duration.zero so the OS treats it as unknown rather than 0:00. - Update the active MediaItem from audioPlayer.durationStream once the real duration is known, mirroring the existing Windows behaviour. Fixes #3077 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
69a310c78f
commit
9c92e2529e
@ -47,17 +47,31 @@ class AudioServices with WidgetsBindingObserver {
|
||||
|
||||
Future<void> addTrack(SpotubeTrackObject track) async {
|
||||
await smtc?.addTrack(track);
|
||||
mobile?.addItem(MediaItem(
|
||||
mobile?.addItem(mediaItemFromTrack(track));
|
||||
}
|
||||
|
||||
/// Maps a track to the [MediaItem] published to the OS media session.
|
||||
///
|
||||
/// Album/playlist tracks can carry an unknown (zero) duration depending on
|
||||
/// the metadata source. Report it as `null` (unknown) instead of
|
||||
/// [Duration.zero] so the OS doesn't render a bogus `0:00 / 0:00` progress
|
||||
/// bar. The real duration is propagated by [MobileAudioService] once the
|
||||
/// player discovers it.
|
||||
@visibleForTesting
|
||||
static MediaItem mediaItemFromTrack(SpotubeTrackObject track) {
|
||||
return MediaItem(
|
||||
id: track.id,
|
||||
album: track.album.name,
|
||||
title: track.name,
|
||||
artist: track.artists.asString(),
|
||||
duration: Duration(milliseconds: track.durationMs),
|
||||
duration: track.durationMs > 0
|
||||
? Duration(milliseconds: track.durationMs)
|
||||
: null,
|
||||
artUri: (track.album.images).asUri(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
playable: true,
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
void activateSession() {
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/audio_player/state.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
@ -72,6 +73,29 @@ class MobileAudioService extends BaseAudioHandler {
|
||||
audioPlayer.bufferedPositionStream.listen((pos) async {
|
||||
playbackState.add(await _transformEvent());
|
||||
});
|
||||
|
||||
// Tracks coming from album/playlist metadata may carry an unknown
|
||||
// (zero) duration, so the [MediaItem] sent to the OS initially has no
|
||||
// duration. Once the player has loaded the source and discovered the
|
||||
// real duration, propagate it to the active [MediaItem] so the
|
||||
// MediaSession/Now Playing UI shows duration, progress and seeking.
|
||||
audioPlayer.durationStream.listen((duration) {
|
||||
final updatedItem =
|
||||
mediaItemWithDuration(mediaItem.valueOrNull, duration);
|
||||
if (updatedItem != null) {
|
||||
mediaItem.add(updatedItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns [item] updated with the player-discovered [duration], or `null`
|
||||
/// when no update should be published (no active item, unknown duration or
|
||||
/// the item already has that exact duration).
|
||||
@visibleForTesting
|
||||
static MediaItem? mediaItemWithDuration(MediaItem? item, Duration duration) {
|
||||
if (item == null || duration <= Duration.zero) return null;
|
||||
if (item.duration == duration) return null;
|
||||
return item.copyWith(duration: duration);
|
||||
}
|
||||
|
||||
void addItem(MediaItem item) {
|
||||
|
||||
117
test/services/audio_services/media_item_duration_test.dart
Normal file
117
test/services/audio_services/media_item_duration_test.dart
Normal file
@ -0,0 +1,117 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/services/audio_services/audio_services.dart';
|
||||
import 'package:spotube/services/audio_services/mobile_audio_service.dart';
|
||||
|
||||
void main() {
|
||||
group('AudioServices.mediaItemFromTrack', () {
|
||||
test('maps a track with a known duration', () {
|
||||
final item = AudioServices.mediaItemFromTrack(FakeData.track);
|
||||
|
||||
expect(item.id, FakeData.track.id);
|
||||
expect(item.title, FakeData.track.name);
|
||||
expect(item.album, FakeData.track.album.name);
|
||||
expect(item.playable, true);
|
||||
expect(item.duration, const Duration(minutes: 3));
|
||||
});
|
||||
|
||||
test(
|
||||
'reports an unknown (zero) duration as null instead of Duration.zero',
|
||||
() {
|
||||
// Album/playlist tracks can come with durationMs == 0 depending on
|
||||
// the metadata source (issue #3077). The OS must be told the duration
|
||||
// is unknown, not that it is 0:00.
|
||||
final track = SpotubeTrackObject.full(
|
||||
id: "1",
|
||||
name: "A good track",
|
||||
externalUri: "https://example.com",
|
||||
album: FakeData.albumSimple,
|
||||
durationMs: 0,
|
||||
isrc: "USUM72112345",
|
||||
explicit: false,
|
||||
);
|
||||
|
||||
final item = AudioServices.mediaItemFromTrack(track);
|
||||
|
||||
expect(item.duration, isNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('MobileAudioService.mediaItemWithDuration', () {
|
||||
const item = MediaItem(
|
||||
id: "1",
|
||||
title: "A good track",
|
||||
album: "A good album",
|
||||
artist: "What an artist",
|
||||
playable: true,
|
||||
);
|
||||
|
||||
test('returns null when there is no active media item', () {
|
||||
expect(
|
||||
MobileAudioService.mediaItemWithDuration(
|
||||
null,
|
||||
const Duration(minutes: 3),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns null when the discovered duration is unknown', () {
|
||||
expect(
|
||||
MobileAudioService.mediaItemWithDuration(item, Duration.zero),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
MobileAudioService.mediaItemWithDuration(
|
||||
item,
|
||||
const Duration(seconds: -1),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns null when the item already has that duration', () {
|
||||
final withDuration = item.copyWith(duration: const Duration(minutes: 3));
|
||||
|
||||
expect(
|
||||
MobileAudioService.mediaItemWithDuration(
|
||||
withDuration,
|
||||
const Duration(minutes: 3),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('fills in an initially unknown duration once discovered', () {
|
||||
final updated = MobileAudioService.mediaItemWithDuration(
|
||||
item,
|
||||
const Duration(minutes: 3),
|
||||
);
|
||||
|
||||
expect(updated, isNotNull);
|
||||
expect(updated!.duration, const Duration(minutes: 3));
|
||||
// Everything else must be preserved.
|
||||
expect(updated.id, item.id);
|
||||
expect(updated.title, item.title);
|
||||
expect(updated.album, item.album);
|
||||
expect(updated.artist, item.artist);
|
||||
expect(updated.playable, item.playable);
|
||||
});
|
||||
|
||||
test('replaces a stale metadata duration with the player duration', () {
|
||||
final withMetadataDuration =
|
||||
item.copyWith(duration: const Duration(minutes: 3));
|
||||
|
||||
final updated = MobileAudioService.mediaItemWithDuration(
|
||||
withMetadataDuration,
|
||||
const Duration(minutes: 3, seconds: 7),
|
||||
);
|
||||
|
||||
expect(updated, isNotNull);
|
||||
expect(updated!.duration, const Duration(minutes: 3, seconds: 7));
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user