From 0a772d19275f59312798040333912b782f07f8a8 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Wed, 12 Aug 2020 01:25:20 +1000 Subject: [PATCH] Add sequenceStream and sequenceStateStream. --- README.md | 2 ++ example/lib/main.dart | 38 ++++++++++---------- lib/just_audio.dart | 84 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 92 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 60fb32f..898977f 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,8 @@ player.playerStateStream.listen((state) { // - durationStream // - positionStream // - bufferedPositionStream +// - sequenceStateStream +// - sequenceStream // - currentIndexStream // - icyMetadataStream // - playingStream diff --git a/example/lib/main.dart b/example/lib/main.dart index 2c2a007..ff749aa 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -50,11 +50,6 @@ class _MyAppState extends State { ), ]); - List get _sequence => _playlist.sequence; - - List get _metadataSequence => - _sequence.map((s) => s.tag as AudioMetadata).toList(); - @override void initState() { super.initState(); @@ -92,11 +87,12 @@ class _MyAppState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( - child: StreamBuilder( - stream: _player.currentIndexStream, + child: StreamBuilder( + stream: _player.sequenceStateStream, builder: (context, snapshot) { - final index = snapshot.data ?? 0; - final metadata = _metadataSequence[index]; + final state = snapshot.data; + if (state?.sequence?.isEmpty ?? true) return SizedBox(); + final metadata = state.currentSource.tag as AudioMetadata; return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -191,17 +187,19 @@ class _MyAppState extends State { ), Container( height: 240.0, - child: StreamBuilder( - stream: _player.currentIndexStream, + child: StreamBuilder( + stream: _player.sequenceStateStream, builder: (context, snapshot) { - final currentIndex = snapshot.data ?? 0; + final state = snapshot.data; + final sequence = state?.sequence ?? []; return ListView.builder( - itemCount: _metadataSequence.length, + itemCount: sequence.length, itemBuilder: (context, index) => Material( - color: - index == currentIndex ? Colors.grey.shade300 : null, + color: index == state.currentIndex + ? Colors.grey.shade300 + : null, child: ListTile( - title: Text(_metadataSequence[index].title), + title: Text(sequence[index].tag.title), onTap: () { _player.seek(Duration.zero, index: index); }, @@ -243,8 +241,8 @@ class ControlButtons extends StatelessWidget { ); }, ), - StreamBuilder( - stream: player.currentIndexStream, + StreamBuilder( + stream: player.sequenceStateStream, builder: (context, snapshot) => IconButton( icon: Icon(Icons.skip_previous), onPressed: player.hasPrevious ? player.seekToPrevious : null, @@ -285,8 +283,8 @@ class ControlButtons extends StatelessWidget { } }, ), - StreamBuilder( - stream: player.currentIndexStream, + StreamBuilder( + stream: player.sequenceStateStream, builder: (context, snapshot) => IconButton( icon: Icon(Icons.skip_next), onPressed: player.hasNext ? player.seekToNext : null, diff --git a/lib/just_audio.dart b/lib/just_audio.dart index 15f1b6f..24f50f1 100644 --- a/lib/just_audio.dart +++ b/lib/just_audio.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -76,7 +77,9 @@ class AudioPlayer { final _bufferedPositionSubject = BehaviorSubject(); final _icyMetadataSubject = BehaviorSubject(); final _playerStateSubject = BehaviorSubject(); + final _sequenceSubject = BehaviorSubject>(); final _currentIndexSubject = BehaviorSubject(); + final _sequenceStateSubject = BehaviorSubject(); final _loopModeSubject = BehaviorSubject(); final _shuffleModeEnabledSubject = BehaviorSubject(); BehaviorSubject _positionSubject; @@ -127,10 +130,6 @@ class AudioPlayer { rethrow; } }); - _eventChannelStreamSubscription = _eventChannelStream.listen( - _playbackEventSubject.add, - onError: _playbackEventSubject.addError, - ); _processingStateSubject.addStream(playbackEventStream .map((event) => event.processingState) .distinct() @@ -147,6 +146,17 @@ class AudioPlayer { .map((event) => event.currentIndex) .distinct() .handleError((err, stack) {/* noop */})); + _sequenceStateSubject.addStream( + Rx.combineLatest2, int, SequenceState>( + sequenceStream, + currentIndexStream, + (sequence, currentIndex) { + if (sequence == null) return null; + if (currentIndex == null) currentIndex = 0; + currentIndex = min(sequence.length - 1, max(0, currentIndex)); + return SequenceState(sequence, currentIndex); + }, + ).distinct().handleError((err, stack) {/* noop */})); _playerStateSubject.addStream( Rx.combineLatest2( playingStream, @@ -154,6 +164,11 @@ class AudioPlayer { (playing, event) => PlayerState(playing, event.processingState)) .distinct() .handleError((err, stack) {/* noop */})); + _eventChannelStreamSubscription = _eventChannelStream.listen( + _playbackEventSubject.add, + onError: _playbackEventSubject.addError, + ); + _sequenceSubject.add(null); } /// The latest [PlaybackEvent]. @@ -216,17 +231,31 @@ class AudioPlayer { /// A stream of [PlayerState]s. Stream get playerStateStream => _playerStateSubject.stream; + /// The current sequence of indexed audio sources. + List get sequence => _sequenceSubject.value; + + /// A stream broadcasting the current sequence of indexed audio sources. + Stream> get sequenceStream => + _sequenceSubject.stream; + /// The index of the current item. int get currentIndex => _currentIndexSubject.value; /// A stream broadcasting the current item. Stream get currentIndexStream => _currentIndexSubject.stream; + /// The current [SequenceState], or `null` if either [sequence]] or + /// [currentIndex] is `null`. + SequenceState get sequenceState => _sequenceStateSubject.value; + + /// A stream broadcasting the current [SequenceState]. + Stream get sequenceStateStream => _sequenceStateSubject.stream; + /// Whether there is another item after the current index. bool get hasNext => _audioSource != null && currentIndex != null && - currentIndex + 1 < _audioSource.sequence.length; + currentIndex + 1 < sequence.length; /// Whether there is another item before the current index. bool get hasPrevious => @@ -378,6 +407,7 @@ class AudioPlayer { Future load(AudioSource source) async { try { _audioSource = source; + _broadcastSequence(); final duration = await _load(source); // Wait for loading state to pass. await processingStateStream @@ -390,6 +420,10 @@ class AudioPlayer { } } + void _broadcastSequence() { + _sequenceSubject.add(_audioSource?.sequence); + } + _registerAudioSource(AudioSource source) { _audioSources[source._id] = source; } @@ -572,6 +606,7 @@ class AudioPlayer { await _playingSubject.close(); await _volumeSubject.close(); await _speedSubject.close(); + await _sequenceSubject.close(); if (_positionSubject != null) { await _positionSubject.close(); } @@ -790,6 +825,23 @@ class IcyMetadata { other is IcyMetadata && other?.info == info && other?.headers == headers; } +/// Encapsulates the [sequence] and [currentIndex] state and ensures +/// consistency such that [currentIndex] is within the range of +/// [sequence.length]. If [sequence.length] is 0, then [currentIndex] is also +/// 0. +class SequenceState { + /// The sequence of the current [AudioSource]. + final List sequence; + + /// The index of the current source in the sequence. + final int currentIndex; + + SequenceState(this.sequence, this.currentIndex); + + /// The current source in the sequence. + IndexedAudioSource get currentSource => sequence[currentIndex]; +} + /// The audio session categories on iOS, to be used with /// [AudioPlayer.setIosCategory]. enum IosCategory { @@ -937,7 +989,7 @@ abstract class AudioSource { /// stream type on Android. If you know in advance what type of audio stream /// it is, you should instantiate [DashAudioSource] or [HlsAudioSource] /// directly. - static AudioSource uri(Uri uri, {Map headers, Object tag}) { + static AudioSource uri(Uri uri, {Map headers, dynamic tag}) { bool hasExtension(Uri uri, String extension) => uri.path.toLowerCase().endsWith('.$extension') || uri.fragment.toLowerCase().endsWith('.$extension'); @@ -1003,7 +1055,7 @@ abstract class AudioSource { /// An [AudioSource] that can appear in a sequence. abstract class IndexedAudioSource extends AudioSource { - final Object tag; + final dynamic tag; IndexedAudioSource(this.tag); @@ -1019,7 +1071,7 @@ abstract class UriAudioSource extends IndexedAudioSource { Uri _overrideUri; File _cacheFile; - UriAudioSource(this.uri, {this.headers, Object tag, @required String type}) + UriAudioSource(this.uri, {this.headers, dynamic tag, @required String type}) : _type = type, super(tag); @@ -1081,7 +1133,7 @@ abstract class UriAudioSource extends IndexedAudioSource { /// On platforms except for the web, the supplied [headers] will be passed with /// the HTTP(S) request. class ProgressiveAudioSource extends UriAudioSource { - ProgressiveAudioSource(Uri uri, {Map headers, Object tag}) + ProgressiveAudioSource(Uri uri, {Map headers, dynamic tag}) : super(uri, headers: headers, tag: tag, type: 'progressive'); } @@ -1091,7 +1143,7 @@ class ProgressiveAudioSource extends UriAudioSource { /// the HTTP(S) request. Currently headers are not recursively applied to items /// the HTTP(S) request. Currently headers are not applied recursively. class DashAudioSource extends UriAudioSource { - DashAudioSource(Uri uri, {Map headers, Object tag}) + DashAudioSource(Uri uri, {Map headers, dynamic tag}) : super(uri, headers: headers, tag: tag, type: 'dash'); } @@ -1100,7 +1152,7 @@ class DashAudioSource extends UriAudioSource { /// On platforms except for the web, the supplied [headers] will be passed with /// the HTTP(S) request. Currently headers are not applied recursively. class HlsAudioSource extends UriAudioSource { - HlsAudioSource(Uri uri, {Map headers, Object tag}) + HlsAudioSource(Uri uri, {Map headers, dynamic tag}) : super(uri, headers: headers, tag: tag, type: 'hls'); } @@ -1131,6 +1183,7 @@ class ConcatenatingAudioSource extends AudioSource { /// (Untested) Appends an [AudioSource]. Future add(AudioSource audioSource) async { children.add(audioSource); + _player._broadcastSequence(); if (_player != null) { await _player ._invokeMethod('concatenating.add', [_id, audioSource.toJson()]); @@ -1140,6 +1193,7 @@ class ConcatenatingAudioSource extends AudioSource { /// (Untested) Inserts an [AudioSource] at [index]. Future insert(int index, AudioSource audioSource) async { children.insert(index, audioSource); + _player._broadcastSequence(); if (_player != null) { await _player._invokeMethod( 'concatenating.insert', [_id, index, audioSource.toJson()]); @@ -1149,6 +1203,7 @@ class ConcatenatingAudioSource extends AudioSource { /// (Untested) Appends multiple [AudioSource]s. Future addAll(List children) async { this.children.addAll(children); + _player._broadcastSequence(); if (_player != null) { await _player._invokeMethod('concatenating.addAll', [_id, children.map((s) => s.toJson()).toList()]); @@ -1158,6 +1213,7 @@ class ConcatenatingAudioSource extends AudioSource { /// (Untested) Insert multiple [AudioSource]s at [index]. Future insertAll(int index, List children) async { this.children.insertAll(index, children); + _player._broadcastSequence(); if (_player != null) { await _player._invokeMethod('concatenating.insertAll', [_id, index, children.map((s) => s.toJson()).toList()]); @@ -1168,6 +1224,7 @@ class ConcatenatingAudioSource extends AudioSource { /// [ConcatenatingAudioSource] has already been loaded. Future removeAt(int index) async { children.removeAt(index); + _player._broadcastSequence(); if (_player != null) { await _player._invokeMethod('concatenating.removeAt', [_id, index]); } @@ -1177,6 +1234,7 @@ class ConcatenatingAudioSource extends AudioSource { /// to [end] exclusive. Future removeRange(int start, int end) async { children.removeRange(start, end); + _player._broadcastSequence(); if (_player != null) { await _player ._invokeMethod('concatenating.removeRange', [_id, start, end]); @@ -1186,6 +1244,7 @@ class ConcatenatingAudioSource extends AudioSource { /// (Untested) Moves an [AudioSource] from [currentIndex] to [newIndex]. Future move(int currentIndex, int newIndex) async { children.insert(newIndex, children.removeAt(currentIndex)); + _player._broadcastSequence(); if (_player != null) { await _player ._invokeMethod('concatenating.move', [_id, currentIndex, newIndex]); @@ -1195,6 +1254,7 @@ class ConcatenatingAudioSource extends AudioSource { /// (Untested) Removes all [AudioSources]. Future clear() async { children.clear(); + _player._broadcastSequence(); if (_player != null) { await _player._invokeMethod('concatenating.clear', [_id]); } @@ -1236,7 +1296,7 @@ class ClippingAudioSource extends IndexedAudioSource { @required this.child, this.start, this.end, - Object tag, + dynamic tag, }) : super(tag); @override