mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
Merge 9c92e2529e into 69a310c78f
This commit is contained in:
commit
6ce1f2f3d7
@ -47,17 +47,31 @@ class AudioServices with WidgetsBindingObserver {
|
|||||||
|
|
||||||
Future<void> addTrack(SpotubeTrackObject track) async {
|
Future<void> addTrack(SpotubeTrackObject track) async {
|
||||||
await smtc?.addTrack(track);
|
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,
|
id: track.id,
|
||||||
album: track.album.name,
|
album: track.album.name,
|
||||||
title: track.name,
|
title: track.name,
|
||||||
artist: track.artists.asString(),
|
artist: track.artists.asString(),
|
||||||
duration: Duration(milliseconds: track.durationMs),
|
duration: track.durationMs > 0
|
||||||
|
? Duration(milliseconds: track.durationMs)
|
||||||
|
: null,
|
||||||
artUri: (track.album.images).asUri(
|
artUri: (track.album.images).asUri(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
playable: true,
|
playable: true,
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void activateSession() {
|
void activateSession() {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:audio_session/audio_session.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/audio_player.dart';
|
||||||
import 'package:spotube/provider/audio_player/state.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -72,6 +73,29 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
audioPlayer.bufferedPositionStream.listen((pos) async {
|
audioPlayer.bufferedPositionStream.listen((pos) async {
|
||||||
playbackState.add(await _transformEvent());
|
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) {
|
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