implemented current position
This commit is contained in:
parent
3b7588831c
commit
494f2aa28b
11 changed files with 210 additions and 102 deletions
|
@ -7,6 +7,7 @@ import '../entities/song.dart';
|
||||||
abstract class AudioRepository {
|
abstract class AudioRepository {
|
||||||
Stream<Song> get currentSongStream;
|
Stream<Song> get currentSongStream;
|
||||||
Stream<PlaybackState> get playbackStateStream;
|
Stream<PlaybackState> get playbackStateStream;
|
||||||
|
Stream<int> get currentPositionStream;
|
||||||
|
|
||||||
Future<Either<Failure, void>> playSong(int index, List<Song> songList);
|
Future<Either<Failure, void>> playSong(int index, List<Song> songList);
|
||||||
Future<Either<Failure, void>> play();
|
Future<Either<Failure, void>> play();
|
||||||
|
|
|
@ -90,7 +90,7 @@ class CurrentlyPlayingPage extends StatelessWidget {
|
||||||
const Spacer(
|
const Spacer(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
),
|
),
|
||||||
TimeProgressIndicator(),
|
const TimeProgressIndicator(),
|
||||||
const Spacer(
|
const Spacer(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
),
|
),
|
||||||
|
|
|
@ -32,12 +32,18 @@ abstract class _AudioStore with Store {
|
||||||
@observable
|
@observable
|
||||||
ObservableStream<PlaybackState> playbackStateStream;
|
ObservableStream<PlaybackState> playbackStateStream;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
ObservableStream<int> currentPositionStream;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
if (!_initialized) {
|
if (!_initialized) {
|
||||||
print('AudioStore.init');
|
print('AudioStore.init');
|
||||||
currentSong = _audioRepository.currentSongStream.asObservable();
|
currentSong = _audioRepository.currentSongStream.asObservable();
|
||||||
|
|
||||||
|
currentPositionStream =
|
||||||
|
_audioRepository.currentPositionStream.asObservable(initialValue: 0);
|
||||||
|
|
||||||
_disposers.add(autorun((_) {
|
_disposers.add(autorun((_) {
|
||||||
updateSong(currentSong.value);
|
updateSong(currentSong.value);
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -63,6 +63,26 @@ mixin _$AudioStore on _AudioStore, Store {
|
||||||
name: '${_$playbackStateStreamAtom.name}_set');
|
name: '${_$playbackStateStreamAtom.name}_set');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _$currentPositionStreamAtom =
|
||||||
|
Atom(name: '_AudioStore.currentPositionStream');
|
||||||
|
|
||||||
|
@override
|
||||||
|
ObservableStream<int> get currentPositionStream {
|
||||||
|
_$currentPositionStreamAtom.context
|
||||||
|
.enforceReadPolicy(_$currentPositionStreamAtom);
|
||||||
|
_$currentPositionStreamAtom.reportObserved();
|
||||||
|
return super.currentPositionStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set currentPositionStream(ObservableStream<int> value) {
|
||||||
|
_$currentPositionStreamAtom.context.conditionallyRunInAction(() {
|
||||||
|
super.currentPositionStream = value;
|
||||||
|
_$currentPositionStreamAtom.reportChanged();
|
||||||
|
}, _$currentPositionStreamAtom,
|
||||||
|
name: '${_$currentPositionStreamAtom.name}_set');
|
||||||
|
}
|
||||||
|
|
||||||
final _$initAsyncAction = AsyncAction('init');
|
final _$initAsyncAction = AsyncAction('init');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -101,7 +121,7 @@ mixin _$AudioStore on _AudioStore, Store {
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
final string =
|
final string =
|
||||||
'currentSong: ${currentSong.toString()},song: ${song.toString()},playbackStateStream: ${playbackStateStream.toString()}';
|
'currentSong: ${currentSong.toString()},song: ${song.toString()},playbackStateStream: ${playbackStateStream.toString()},currentPositionStream: ${currentPositionStream.toString()}';
|
||||||
return '{$string}';
|
return '{$string}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,3 +10,25 @@ ImageProvider getAlbumImage(String albumArtPath) {
|
||||||
}
|
}
|
||||||
return FileImage(File(albumArtPath));
|
return FileImage(File(albumArtPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String msToTimeString(int milliseconds) {
|
||||||
|
String twoDigits(num n) {
|
||||||
|
if (n >= 10) {
|
||||||
|
return '$n';
|
||||||
|
}
|
||||||
|
return '0$n';
|
||||||
|
}
|
||||||
|
|
||||||
|
final Duration duration = Duration(seconds: (milliseconds / 1000).round());
|
||||||
|
|
||||||
|
final int hours = duration.inHours;
|
||||||
|
final int minutes = duration.inMinutes.remainder(60) as int;
|
||||||
|
|
||||||
|
final String twoDigitMinutes = twoDigits(minutes);
|
||||||
|
final String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return '$hours:$twoDigitMinutes:$twoDigitSeconds';
|
||||||
|
}
|
||||||
|
return '$minutes:$twoDigitSeconds';
|
||||||
|
}
|
||||||
|
|
|
@ -23,8 +23,8 @@ class CurrentlyPlayingBar extends StatelessWidget {
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
child: const LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: 0.42,
|
value: audioStore.currentPositionStream.value / audioStore.currentSong.value.duration,
|
||||||
),
|
),
|
||||||
height: 2,
|
height: 2,
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,35 +1,42 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class TimeProgressIndicator extends StatefulWidget {
|
import '../state/audio_store.dart';
|
||||||
TimeProgressIndicator({Key key}) : super(key: key);
|
import '../utils.dart';
|
||||||
|
|
||||||
@override
|
class TimeProgressIndicator extends StatelessWidget {
|
||||||
_TimeProgressIndicatorState createState() => _TimeProgressIndicatorState();
|
const TimeProgressIndicator({Key key}) : super(key: key);
|
||||||
}
|
|
||||||
|
|
||||||
class _TimeProgressIndicatorState extends State<TimeProgressIndicator> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
final AudioStore audioStore = Provider.of<AudioStore>(context);
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Row(
|
return Observer(
|
||||||
children: [
|
builder: (BuildContext context) {
|
||||||
Text("0:42"),
|
return Padding(
|
||||||
Container(
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
width: 10,
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(msToTimeString(audioStore.currentPositionStream.value)),
|
||||||
|
Container(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
child: LinearProgressIndicator(value: audioStore.currentPositionStream.value / audioStore.currentSong.value.duration),
|
||||||
|
height: 3.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
Text(msToTimeString(audioStore.currentSong.value.duration)),
|
||||||
|
],
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
),
|
),
|
||||||
Expanded(
|
);
|
||||||
child: Container(
|
},
|
||||||
child: LinearProgressIndicator(value: 0.42),
|
|
||||||
height: 3.0,
|
|
||||||
)),
|
|
||||||
Container(
|
|
||||||
width: 10,
|
|
||||||
),
|
|
||||||
Text("3:42"),
|
|
||||||
],
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
|
||||||
|
|
||||||
import '../../domain/entities/playback_state.dart' as entity;
|
import '../../domain/entities/playback_state.dart' as entity;
|
||||||
import '../models/playback_state_model.dart';
|
import '../models/playback_state_model.dart';
|
||||||
import '../models/song_model.dart';
|
import '../models/song_model.dart';
|
||||||
import 'audio_manager_contract.dart';
|
import 'audio_manager_contract.dart';
|
||||||
|
import 'audio_player_task.dart';
|
||||||
|
|
||||||
typedef Conversion<S, T> = T Function(S);
|
typedef Conversion<S, T> = T Function(S);
|
||||||
|
|
||||||
|
@ -29,6 +29,9 @@ class AudioManagerImpl implements AudioManager {
|
||||||
(PlaybackState ps) => PlaybackStateModel.fromASPlaybackState(ps),
|
(PlaybackState ps) => PlaybackStateModel.fromASPlaybackState(ps),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<int> get currentPositionStream => _position().distinct();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> playSong(int index, List<SongModel> songList) async {
|
Future<void> playSong(int index, List<SongModel> songList) async {
|
||||||
await _startAudioService();
|
await _startAudioService();
|
||||||
|
@ -68,80 +71,35 @@ class AudioManagerImpl implements AudioManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<int> _position() async* {
|
||||||
|
BasicPlaybackState state;
|
||||||
|
int updateTime;
|
||||||
|
int statePosition;
|
||||||
|
|
||||||
|
// should this class get an init method for this?
|
||||||
|
_sourcePlaybackStateStream.listen((currentState) {
|
||||||
|
state = currentState?.basicState;
|
||||||
|
updateTime = currentState?.updateTime;
|
||||||
|
statePosition = currentState?.position;
|
||||||
|
});
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (statePosition != null && updateTime != null && state != null) {
|
||||||
|
if (state == BasicPlaybackState.playing) {
|
||||||
|
yield statePosition +
|
||||||
|
(DateTime.now().millisecondsSinceEpoch - updateTime);
|
||||||
|
} else {
|
||||||
|
yield statePosition;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
yield 0;
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(milliseconds: 200));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _backgroundTaskEntrypoint() {
|
void _backgroundTaskEntrypoint() {
|
||||||
AudioServiceBackground.run(() => AudioPlayerTask());
|
AudioServiceBackground.run(() => AudioPlayerTask());
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudioPlayerTask extends BackgroundAudioTask {
|
|
||||||
final _audioPlayer = AudioPlayer();
|
|
||||||
final _completer = Completer();
|
|
||||||
|
|
||||||
final _mediaItems = <String, MediaItem>{};
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onStart() async {
|
|
||||||
// AudioServiceBackground.setState(
|
|
||||||
// controls: [],
|
|
||||||
// basicState: BasicPlaybackState.none,
|
|
||||||
// );
|
|
||||||
await _completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onStop() {
|
|
||||||
_audioPlayer.stop();
|
|
||||||
_completer.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onAddQueueItem(MediaItem mediaItem) {
|
|
||||||
_mediaItems[mediaItem.id] = mediaItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onPlayFromMediaId(String mediaId) async {
|
|
||||||
AudioServiceBackground.setState(
|
|
||||||
controls: [pauseControl, stopControl],
|
|
||||||
basicState: BasicPlaybackState.playing,
|
|
||||||
);
|
|
||||||
|
|
||||||
await AudioServiceBackground.setMediaItem(_mediaItems[mediaId]);
|
|
||||||
await _audioPlayer.setFilePath(mediaId);
|
|
||||||
|
|
||||||
_audioPlayer.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onPlay() async {
|
|
||||||
AudioServiceBackground.setState(
|
|
||||||
controls: [pauseControl, stopControl],
|
|
||||||
basicState: BasicPlaybackState.playing);
|
|
||||||
_audioPlayer.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onPause() async {
|
|
||||||
AudioServiceBackground.setState(
|
|
||||||
controls: [playControl, stopControl],
|
|
||||||
basicState: BasicPlaybackState.paused);
|
|
||||||
await _audioPlayer.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaControl playControl = const MediaControl(
|
|
||||||
androidIcon: 'drawable/ic_action_play_arrow',
|
|
||||||
label: 'Play',
|
|
||||||
action: MediaAction.play,
|
|
||||||
);
|
|
||||||
MediaControl pauseControl = const MediaControl(
|
|
||||||
androidIcon: 'drawable/ic_action_pause',
|
|
||||||
label: 'Pause',
|
|
||||||
action: MediaAction.pause,
|
|
||||||
);
|
|
||||||
MediaControl stopControl = const MediaControl(
|
|
||||||
androidIcon: 'drawable/ic_action_stop',
|
|
||||||
label: 'Stop',
|
|
||||||
action: MediaAction.stop,
|
|
||||||
);
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import '../models/song_model.dart';
|
||||||
abstract class AudioManager {
|
abstract class AudioManager {
|
||||||
Stream<SongModel> get currentSongStream;
|
Stream<SongModel> get currentSongStream;
|
||||||
Stream<PlaybackState> get playbackStateStream;
|
Stream<PlaybackState> get playbackStateStream;
|
||||||
|
/// Current position in the song in milliseconds.
|
||||||
|
Stream<int> get currentPositionStream;
|
||||||
|
|
||||||
Future<void> playSong(int index, List<SongModel> songList);
|
Future<void> playSong(int index, List<SongModel> songList);
|
||||||
Future<void> play();
|
Future<void> play();
|
||||||
|
|
87
lib/system/datasources/audio_player_task.dart
Normal file
87
lib/system/datasources/audio_player_task.dart
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
import 'package:just_audio/just_audio.dart';
|
||||||
|
|
||||||
|
class AudioPlayerTask extends BackgroundAudioTask {
|
||||||
|
final _audioPlayer = AudioPlayer();
|
||||||
|
final _completer = Completer();
|
||||||
|
|
||||||
|
final _mediaItems = <String, MediaItem>{};
|
||||||
|
|
||||||
|
Duration _position;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onStart() async {
|
||||||
|
_audioPlayer.getPositionStream().listen((duration) => _position = duration);
|
||||||
|
|
||||||
|
// AudioServiceBackground.setState(
|
||||||
|
// controls: [],
|
||||||
|
// basicState: BasicPlaybackState.none,
|
||||||
|
// );
|
||||||
|
await _completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onStop() {
|
||||||
|
_audioPlayer.stop();
|
||||||
|
_completer.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onAddQueueItem(MediaItem mediaItem) {
|
||||||
|
_mediaItems[mediaItem.id] = mediaItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onPlayFromMediaId(String mediaId) async {
|
||||||
|
AudioServiceBackground.setState(
|
||||||
|
controls: [pauseControl, stopControl],
|
||||||
|
basicState: BasicPlaybackState.playing,
|
||||||
|
updateTime: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
|
|
||||||
|
await AudioServiceBackground.setMediaItem(_mediaItems[mediaId]);
|
||||||
|
await _audioPlayer.setFilePath(mediaId);
|
||||||
|
|
||||||
|
_audioPlayer.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onPlay() async {
|
||||||
|
AudioServiceBackground.setState(
|
||||||
|
controls: [pauseControl, stopControl],
|
||||||
|
basicState: BasicPlaybackState.playing,
|
||||||
|
updateTime: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
position: _position.inMilliseconds,
|
||||||
|
);
|
||||||
|
_audioPlayer.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onPause() async {
|
||||||
|
AudioServiceBackground.setState(
|
||||||
|
controls: [playControl, stopControl],
|
||||||
|
basicState: BasicPlaybackState.paused,
|
||||||
|
updateTime: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
position: _position.inMilliseconds,
|
||||||
|
);
|
||||||
|
await _audioPlayer.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaControl playControl = const MediaControl(
|
||||||
|
androidIcon: 'drawable/ic_action_play_arrow',
|
||||||
|
label: 'Play',
|
||||||
|
action: MediaAction.play,
|
||||||
|
);
|
||||||
|
MediaControl pauseControl = const MediaControl(
|
||||||
|
androidIcon: 'drawable/ic_action_pause',
|
||||||
|
label: 'Pause',
|
||||||
|
action: MediaAction.pause,
|
||||||
|
);
|
||||||
|
MediaControl stopControl = const MediaControl(
|
||||||
|
androidIcon: 'drawable/ic_action_stop',
|
||||||
|
label: 'Stop',
|
||||||
|
action: MediaAction.stop,
|
||||||
|
);
|
|
@ -19,6 +19,11 @@ class AudioRepositoryImpl implements AudioRepository {
|
||||||
Stream<PlaybackState> get playbackStateStream =>
|
Stream<PlaybackState> get playbackStateStream =>
|
||||||
_audioManager.playbackStateStream;
|
_audioManager.playbackStateStream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<int> get currentPositionStream {
|
||||||
|
return _audioManager.currentPositionStream;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Either<Failure, void>> playSong(int index, List<Song> songList) async {
|
Future<Either<Failure, void>> playSong(int index, List<Song> songList) async {
|
||||||
final List<SongModel> songModelList =
|
final List<SongModel> songModelList =
|
||||||
|
|
Loading…
Add table
Reference in a new issue