streaming edits
This commit is contained in:
parent
0a7af1d813
commit
db33f0764b
3 changed files with 62 additions and 15 deletions
|
|
@ -32,6 +32,7 @@ class PlaybackState {
|
||||||
this.queue = const [],
|
this.queue = const [],
|
||||||
this.currentIndex,
|
this.currentIndex,
|
||||||
this.playing = false,
|
this.playing = false,
|
||||||
|
this.buffering = false,
|
||||||
this.position = Duration.zero,
|
this.position = Duration.zero,
|
||||||
this.duration = Duration.zero,
|
this.duration = Duration.zero,
|
||||||
this.shuffle = false,
|
this.shuffle = false,
|
||||||
|
|
@ -43,6 +44,13 @@ class PlaybackState {
|
||||||
final List<Song> queue;
|
final List<Song> queue;
|
||||||
final int? currentIndex;
|
final int? currentIndex;
|
||||||
final bool playing;
|
final bool playing;
|
||||||
|
|
||||||
|
/// True while the player is loading/buffering a source (not yet `ready`). A
|
||||||
|
/// streamed source that is buffering legitimately reports position 0; this
|
||||||
|
/// lets the UI show a spinner instead of a frozen 0:00 bar. Transient — never
|
||||||
|
/// persisted.
|
||||||
|
final bool buffering;
|
||||||
|
|
||||||
final Duration position;
|
final Duration position;
|
||||||
final Duration duration;
|
final Duration duration;
|
||||||
final bool shuffle;
|
final bool shuffle;
|
||||||
|
|
@ -83,6 +91,7 @@ class PlaybackState {
|
||||||
List<Song>? queue,
|
List<Song>? queue,
|
||||||
int? currentIndex,
|
int? currentIndex,
|
||||||
bool? playing,
|
bool? playing,
|
||||||
|
bool? buffering,
|
||||||
Duration? position,
|
Duration? position,
|
||||||
Duration? duration,
|
Duration? duration,
|
||||||
bool? shuffle,
|
bool? shuffle,
|
||||||
|
|
@ -95,6 +104,7 @@ class PlaybackState {
|
||||||
queue: queue ?? this.queue,
|
queue: queue ?? this.queue,
|
||||||
currentIndex: currentIndex ?? this.currentIndex,
|
currentIndex: currentIndex ?? this.currentIndex,
|
||||||
playing: playing ?? this.playing,
|
playing: playing ?? this.playing,
|
||||||
|
buffering: buffering ?? this.buffering,
|
||||||
position: position ?? this.position,
|
position: position ?? this.position,
|
||||||
duration: duration ?? this.duration,
|
duration: duration ?? this.duration,
|
||||||
shuffle: shuffle ?? this.shuffle,
|
shuffle: shuffle ?? this.shuffle,
|
||||||
|
|
@ -215,7 +225,12 @@ class PlaybackController extends StateNotifier<PlaybackState>
|
||||||
_maybeSlideWindow();
|
_maybeSlideWindow();
|
||||||
});
|
});
|
||||||
player.playerStateStream.listen((s) {
|
player.playerStateStream.listen((s) {
|
||||||
state = state.copyWith(playing: s.playing);
|
final ps = s.processingState;
|
||||||
|
state = state.copyWith(
|
||||||
|
playing: s.playing,
|
||||||
|
buffering: ps == ProcessingState.loading ||
|
||||||
|
ps == ProcessingState.buffering,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
player.positionStream.listen((p) {
|
player.positionStream.listen((p) {
|
||||||
state = state.copyWith(position: p);
|
state = state.copyWith(position: p);
|
||||||
|
|
@ -306,11 +321,10 @@ class PlaybackController extends StateNotifier<PlaybackState>
|
||||||
|
|
||||||
AudioSource _sourceFor(Song song) {
|
AudioSource _sourceFor(Song song) {
|
||||||
final uri = _streamUriFor(song)!;
|
final uri = _streamUriFor(song)!;
|
||||||
if (!uri.isScheme('file')) _remoteSourceIds.add(song.id);
|
final isRemote = !uri.isScheme('file');
|
||||||
|
if (isRemote) _remoteSourceIds.add(song.id);
|
||||||
final art = _coverArtUriFor(song);
|
final art = _coverArtUriFor(song);
|
||||||
return AudioSource.uri(
|
final tag = MediaItem(
|
||||||
uri,
|
|
||||||
tag: MediaItem(
|
|
||||||
id: '${song.id}#${_tagSeq++}',
|
id: '${song.id}#${_tagSeq++}',
|
||||||
title: song.title ?? 'Unknown',
|
title: song.title ?? 'Unknown',
|
||||||
album: song.album,
|
album: song.album,
|
||||||
|
|
@ -318,8 +332,21 @@ class PlaybackController extends StateNotifier<PlaybackState>
|
||||||
duration:
|
duration:
|
||||||
song.duration != null ? Duration(seconds: song.duration!) : null,
|
song.duration != null ? Duration(seconds: song.duration!) : null,
|
||||||
artUri: art,
|
artUri: art,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
// Wrap remote streams in a caching source on mobile: it fetches bytes to an
|
||||||
|
// OS-evictable temp file, giving the native player a genuinely seekable
|
||||||
|
// source with a known length — robust against chunked transcodes that omit
|
||||||
|
// Content-Length and against brief network drops. Downloads (file://) are
|
||||||
|
// already seekable, and the media_kit desktop backend uses just_audio's
|
||||||
|
// localhost proxy path we don't rely on, so both stay on the plain source.
|
||||||
|
if (isRemote && (Platform.isAndroid || Platform.isIOS)) {
|
||||||
|
// LockCachingAudioSource is marked experimental in just_audio but is
|
||||||
|
// stable in practice; the streaming reliability it provides is the whole
|
||||||
|
// point of this path.
|
||||||
|
// ignore: experimental_member_use
|
||||||
|
return LockCachingAudioSource(uri, tag: tag);
|
||||||
|
}
|
||||||
|
return AudioSource.uri(uri, tag: tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the queue with [songs] and start at [startIndex].
|
/// Replace the queue with [songs] and start at [startIndex].
|
||||||
|
|
|
||||||
|
|
@ -549,9 +549,15 @@ final playbackProvider =
|
||||||
if (local != null) return Uri.file(local);
|
if (local != null) return Uri.file(local);
|
||||||
final client = ref.read(subsonicClientProvider);
|
final client = ref.read(subsonicClientProvider);
|
||||||
if (client == null) return null;
|
if (client == null) return null;
|
||||||
|
final rate = ref.read(settingsProvider).streamMaxBitRate;
|
||||||
|
// When transcoding, ask the server to advertise a Content-Length so the
|
||||||
|
// native player can derive a duration and hold position (otherwise the
|
||||||
|
// playhead freezes at 0:00 and the track restarts). Harmless to omit for
|
||||||
|
// original streams, which already carry a real length.
|
||||||
return client.streamUri(
|
return client.streamUri(
|
||||||
s.id,
|
s.id,
|
||||||
maxBitRate: ref.read(settingsProvider).streamMaxBitRate,
|
maxBitRate: rate,
|
||||||
|
estimateContentLength: rate > 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -302,11 +302,25 @@ class SubsonicClient {
|
||||||
/// manager, which fetches these bytes to disk). `maxBitRate == 0` means
|
/// manager, which fetches these bytes to disk). `maxBitRate == 0` means
|
||||||
/// original / no transcode; [format] requests a specific transcode container
|
/// original / no transcode; [format] requests a specific transcode container
|
||||||
/// (e.g. `mp3`, `opus`), or null for the server default / original.
|
/// (e.g. `mp3`, `opus`), or null for the server default / original.
|
||||||
Uri streamUri(String id, {int maxBitRate = 0, String? format}) =>
|
///
|
||||||
|
/// [estimateContentLength] asks the server to send an (estimated)
|
||||||
|
/// `Content-Length` header even for on-the-fly transcodes. Transcoded
|
||||||
|
/// responses are otherwise chunked with no length and no byte ranges, so the
|
||||||
|
/// native player reports `duration == null`, never reaches `ready`, and the
|
||||||
|
/// playhead freezes at 0:00 (then restarts). Only meaningful when transcoding
|
||||||
|
/// (`maxBitRate > 0` or a [format]); original streams already carry a real
|
||||||
|
/// length.
|
||||||
|
Uri streamUri(
|
||||||
|
String id, {
|
||||||
|
int maxBitRate = 0,
|
||||||
|
String? format,
|
||||||
|
bool estimateContentLength = false,
|
||||||
|
}) =>
|
||||||
_uri('stream', {
|
_uri('stream', {
|
||||||
'id': id,
|
'id': id,
|
||||||
if (maxBitRate > 0) 'maxBitRate': '$maxBitRate',
|
if (maxBitRate > 0) 'maxBitRate': '$maxBitRate',
|
||||||
if (format != null && format.isNotEmpty) 'format': format,
|
if (format != null && format.isNotEmpty) 'format': format,
|
||||||
|
if (estimateContentLength) 'estimateContentLength': 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Signed cover-art URL. [size] is clamped to Subsonic's 32–2048 range.
|
/// Signed cover-art URL. [size] is clamped to Subsonic's 32–2048 range.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue