Reputation: 31
I'm new to Flutter and followed this tutorial https://suragch.medium.com/background-audio-in-flutter-with-audio-service-and-just-audio-3cce17b4a7d to setup Just Audio and audioservice. I'm building my app for Android at the moment.
I'm having 3 issues:
Here are the 3 files I'm using:
Player screen:
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:podfic_app/screens/chapterlist_screen.dart';
import 'package:podfic_app/utils/api_service.dart';
import './just_audio/notifiers/play_button_notifier.dart';
import './just_audio/notifiers/progress_notifier.dart';
import './just_audio/page_manager.dart';
import './just_audio/services/service_locator.dart';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
class Player extends StatefulWidget {
const Player({super.key});
@override
State<Player> createState() => _PlayerState();
}
class _PlayerState extends State<Player> {
@override
void initState() {
super.initState();
getIt<PageManager>().init();
}
@override
void dispose() {
getIt<PageManager>().dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// startPlaying();
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Column(
children: [
// const SizedBox(height: 10),
// back button and menu button
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.pop()),
const Text('Listening to'),
IconButton(
icon: const Icon(Icons.playlist_play_outlined),
onPressed: () => context.push('/chapters')),
],
),
const SizedBox(height: 25),
// cover art, artist name, song name
const CurrentMetadata(),
const SizedBox(height: 30),
const SizedBox(height: 20),
// linear bar
const AudioProgressBar(),
const SizedBox(height: 30),
// previous song, pause play, skip next song
Align(
alignment: Alignment.topCenter,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(0),
child: Stack(children: [
Container(
child: const ChangeSpeedButton(),
),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
PreviousSongButton(),
PlayButton(),
NextSongButton(),
],
),
),
]),
),
),
],
),
),
),
);
}
}
// class CurrentSongTitle extends StatelessWidget {
// const CurrentSongTitle({Key? key}) : super(key: key);
// @override
// Widget build(BuildContext context) {
// final pageManager = getIt<PageManager>();
// return ValueListenableBuilder<String>(
// valueListenable: pageManager.currentSongTitleNotifier,
// builder: (_, title, __) {
// return Padding(
// padding: const EdgeInsets.only(top: 8.0),
// child: Text(title, style: const TextStyle(fontSize: 40)),
// );
// },
// );
// }
// }
class AddRemoveSongButtons extends StatelessWidget {
const AddRemoveSongButtons({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FloatingActionButton(
onPressed: pageManager.add,
child: const Icon(Icons.add),
),
FloatingActionButton(
onPressed: pageManager.remove,
child: const Icon(Icons.remove),
),
],
),
);
}
}
class AudioProgressBar extends StatelessWidget {
const AudioProgressBar({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<ProgressBarState>(
valueListenable: pageManager.progressNotifier,
builder: (_, value, __) {
return ProgressBar(
progress: value.current,
buffered: value.buffered,
total: value.total,
onSeek: pageManager.seek,
);
},
);
}
}
class AudioControlButtons extends StatelessWidget {
const AudioControlButtons({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 60,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: const [
PreviousSongButton(),
PlayButton(),
NextSongButton(),
],
),
);
}
}
class PreviousSongButton extends StatelessWidget {
const PreviousSongButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<bool>(
valueListenable: pageManager.isFirstSongNotifier,
builder: (_, isFirst, __) {
return IconButton(
icon: const Icon(Icons.skip_previous),
iconSize: 32,
onPressed: (isFirst) ? null : pageManager.previous,
);
},
);
}
}
class PlayButton extends StatelessWidget {
const PlayButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<ButtonState>(
valueListenable: pageManager.playButtonNotifier,
builder: (_, value, __) {
switch (value) {
case ButtonState.loading:
return Container(
margin: const EdgeInsets.all(8.0),
width: 48.0,
height: 48.0,
child: const CircularProgressIndicator(),
);
case ButtonState.paused:
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(200)),
child: IconButton(
icon: const Icon(Icons.play_arrow),
iconSize: 48,
color: Colors.white,
onPressed: pageManager.play,
));
case ButtonState.playing:
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(200)),
child: IconButton(
icon: const Icon(Icons.pause),
iconSize: 48,
color: Colors.white,
onPressed: pageManager.pause,
));
}
},
);
}
}
class NextSongButton extends StatelessWidget {
const NextSongButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<bool>(
valueListenable: pageManager.isLastSongNotifier,
builder: (_, isLast, __) {
return IconButton(
icon: const Icon(Icons.skip_next),
iconSize: 32,
onPressed: (isLast) ? null : pageManager.next,
);
},
);
}
}
class ChangeSpeedButton extends StatefulWidget {
const ChangeSpeedButton({Key? key}) : super(key: key);
@override
State<ChangeSpeedButton> createState() => _ChangeSpeedButtonState();
}
class _ChangeSpeedButtonState extends State<ChangeSpeedButton> {
var selectedValue = 1.0;
@override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return DropdownButtonHideUnderline(
child: DropdownButton(
icon: const Visibility(
visible: false, child: Icon(Icons.arrow_downward)),
value: selectedValue
.toString(), //we set a value here depending on the button pressed, and call the pagemanager method assing it the value.
items: dropdownItems,
onChanged: (value) {
setState(() {
selectedValue = double.parse(value.toString());
});
pageManager.changeSpeed(selectedValue);
}),
);
}
List<DropdownMenuItem<String>> get dropdownItems {
List<DropdownMenuItem<String>> menuItems = [
const DropdownMenuItem(value: "0.75", child: Text("0.75x")),
const DropdownMenuItem(value: "1.0", child: Text("1.0x")),
const DropdownMenuItem(value: "1.25", child: Text("1.25x")),
const DropdownMenuItem(value: "1.5", child: Text("1.5x")),
const DropdownMenuItem(value: "1.75", child: Text("1.75x")),
const DropdownMenuItem(value: "2.0", child: Text("2.0x")),
];
return menuItems;
}
}
class CurrentMetadata extends StatelessWidget {
const CurrentMetadata({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final pageManager = getIt<PageManager>();
return ValueListenableBuilder<MediaItem?>(
valueListenable: pageManager.currentSongMetadataNotifier,
builder: (context, mediaItem, child) {
if (mediaItem == null) {
return SizedBox.shrink();
}
return Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network((mediaItem.artUri ?? '').toString()),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(mediaItem.extras!['chapterName'],
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w700)),
const SizedBox(height: 6),
Text(
mediaItem.title,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 6),
Text(
'By ${mediaItem.artist}',
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.titleSmall,
),
],
),
IconButton(
icon: const Icon(Icons.favorite),
color: Colors.red,
onPressed: () {}),
],
),
)
],
);
// Text(mediaItem.album ?? ''),
// Text(mediaItem.displaySubtitle ?? ''),
},
);
}
}
PageManager.dart
import 'package:flutter/foundation.dart';
import 'package:podfic_app/models/getpodficlist_model.dart';
import 'package:podfic_app/models/series_model.dart';
import 'package:podfic_app/notifiers/chapterObject_notifier.dart';
import 'package:podfic_app/utils/api_service.dart';
import 'notifiers/play_button_notifier.dart';
import 'notifiers/progress_notifier.dart';
import 'notifiers/repeat_button_notifier.dart';
import 'services/playlist_repository.dart';
import 'package:audio_service/audio_service.dart';
import 'services/service_locator.dart';
class PageManager {
// Listeners: Updates going to the UI
final currentSongMetadataNotifier = ValueNotifier<MediaItem?>(null);
final playlistNotifier = ValueNotifier<List<String>>([]);
final progressNotifier = ProgressNotifier();
final repeatButtonNotifier = RepeatButtonNotifier();
final isFirstSongNotifier = ValueNotifier<bool>(true);
final playButtonNotifier = PlayButtonNotifier();
final isLastSongNotifier = ValueNotifier<bool>(true);
final isShuffleModeEnabledNotifier = ValueNotifier<bool>(false);
final _audioHandler = getIt<AudioHandler>();
List<Chapter>? chapterObject = getIt<ChapterObject>().chapterObject.value;
// Events: Calls coming from the UI
void init() async {
await loadPlaylist();
_listenToChangesInPlaylist();
_listenToPlaybackState();
_listenToCurrentPosition();
_listenToBufferedPosition();
_listenToTotalDuration();
_listenToChangesInSong();
// _startPlaying();
}
Future<void> loadPlaylist() async {
final mediaItems = seriesItems.chapterList?.map((chapter) {
final _podficInfo = chapter.podficInfo;
return MediaItem(
id: _podficInfo.id.toString(),
artUri: Uri.parse(_podficInfo.coverArt!),
artist: seriesItems.author,
title: seriesItems.title!,
extras: {
'url': _podficInfo.url,
'chapterName':
"Chapter ${_podficInfo.chapterNumber}: ${_podficInfo.title}"
},
);
}).toList() ??
[];
_audioHandler.addQueueItems(mediaItems);
}
// void _setInitialPlaylist() {
// final _seriesInfo = getIt<SeriesInfo>();
// final _playlist = SeriesInfo(chapterList: chapterItems);
// }
void play() => _audioHandler.play();
void pause() => _audioHandler.pause();
void seek(Duration position) => _audioHandler.seek(position);
void previous() => _audioHandler.skipToPrevious();
void next() => _audioHandler.skipToNext();
void repeat() {}
void shuffle() {}
void add() async {
//don't need this for now.
final songRepository = getIt<PlaylistRepository>();
final song = await songRepository.fetchAnotherSong();
final mediaItem = MediaItem(
id: song['id'] ?? '',
album: song['album'] ?? '',
title: song['title'] ?? '',
extras: {'url': song['url']},
);
_audioHandler.addQueueItem(mediaItem);
}
void remove() {
//don't need this for now.
final lastIndex = _audioHandler.queue.value.length - 1;
if (lastIndex < 0) return;
_audioHandler.removeQueueItemAt(lastIndex);
}
void dispose() {
_audioHandler.stop();
}
void _listenToChangesInPlaylist() {
_audioHandler.queue.listen((playlist) {
if (playlist.isEmpty) {
playlistNotifier.value = [];
currentSongMetadataNotifier.value = '' as MediaItem?;
} else {
final newList = playlist.map((item) => item.title).toList();
playlistNotifier.value = newList;
}
_updateSkipButtons();
});
}
void _listenToPlaybackState() {
_audioHandler.playbackState.listen((playbackState) {
final isPlaying = playbackState.playing;
final processingState = playbackState.processingState;
if (processingState == AudioProcessingState.loading ||
processingState == AudioProcessingState.buffering) {
playButtonNotifier.value = ButtonState.loading;
} else if (!isPlaying) {
playButtonNotifier.value = ButtonState.paused;
} else if (processingState != AudioProcessingState.completed) {
playButtonNotifier.value = ButtonState.playing;
logger.i('Reproduciendo');
} else {
_audioHandler.seek(Duration.zero);
_audioHandler.pause();
}
});
}
void _listenToCurrentPosition() {
AudioService.position.listen((position) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: position,
buffered: oldState.buffered,
total: oldState.total,
);
});
}
void _listenToBufferedPosition() {
_audioHandler.playbackState.listen((playbackState) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: oldState.current,
buffered: playbackState.bufferedPosition,
total: oldState.total,
);
});
}
void _listenToTotalDuration() {
_audioHandler.mediaItem.listen((mediaItem) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: oldState.current,
buffered: oldState.buffered,
total: mediaItem?.duration ?? Duration.zero,
);
});
}
void _listenToChangesInSong() {
_audioHandler.mediaItem.listen((mediaItem) {
currentSongMetadataNotifier.value = mediaItem;
_updateSkipButtons();
});
}
void _updateSkipButtons() {
final mediaItem = _audioHandler.mediaItem.value;
final playlist = _audioHandler.queue.value;
if (playlist.length < 2 || mediaItem == null) {
isFirstSongNotifier.value = true;
isLastSongNotifier.value = true;
} else {
isFirstSongNotifier.value = playlist.first == mediaItem;
isLastSongNotifier.value = playlist.last == mediaItem;
}
}
void changeSpeed(value) {
_audioHandler.setSpeed(value);
}
// void _startPlaying(){
// _audioHandler.
// }
}
// class PlaySpeedNotifier extends ValueNotifier {
// PlaySpeedNotifier() : super(initialSpeed);
// static const initialSpeed = 1.0;
// var speed = initialSpeed;
// void changeSpeed(value) {
// speed = value;
// _audio
// }
// }
Audiohandler.dart
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import 'package:podfic_app/utils/api_service.dart';
Future<AudioHandler> initAudioService() async {
return await AudioService.init(
builder: () => MyAudioHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.mycompany.myapp.audio',
androidNotificationChannelName: 'FanPods',
androidNotificationOngoing: true,
androidStopForegroundOnPause: true,
),
);
}
class MyAudioHandler extends BaseAudioHandler {
final _player = AudioPlayer();
final _playlist = ConcatenatingAudioSource(children: []);
MyAudioHandler() {
loadEmptyPlaylist();
_notifyAudioHandlerAboutPlaybackEvents();
_listenForDurationChanges();
_listenForCurrentSongIndexChanges();
}
Future<void> loadEmptyPlaylist() async {
try {
await _player.setAudioSource(_playlist);
} catch (e) {
logger.i('algo paso con loadEemptyplaylist');
}
}
// Future<void> startPlaying() => _player.setAudioSource(_playlist);
@override
Future<void> addQueueItems(List<MediaItem> mediaItems) async {
// manage Just Audio
final audioSource = mediaItems.map(_createAudioSource);
_playlist.addAll(audioSource.toList());
// notify system
final newQueue = queue.value..addAll(mediaItems);
queue.add(newQueue);
logger.i(newQueue);
}
UriAudioSource _createAudioSource(MediaItem mediaItem) {
return AudioSource.uri(
Uri.parse(mediaItem.extras!['url']),
tag: mediaItem,
);
}
@override
Future<void> play() => _player.play();
@override
Future<void> pause() => _player.pause();
void _notifyAudioHandlerAboutPlaybackEvents() {
_player.playbackEventStream.listen((PlaybackEvent event) {
final playing = _player.playing;
playbackState.add(playbackState.value.copyWith(
controls: [
MediaControl.skipToPrevious,
if (playing) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
],
systemActions: const {
MediaAction.seek,
},
androidCompactActionIndices: const [0, 1, 3],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading,
ProcessingState.buffering: AudioProcessingState.buffering,
ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
}[_player.processingState]!,
playing: playing,
updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
));
});
}
@override
Future<void> seek(Duration position) => _player.seek(position);
void _listenForDurationChanges() {
_player.durationStream.listen((duration) {
final index = _player.currentIndex;
final newQueue = queue.value;
if (index == null || newQueue.isEmpty) return;
final oldMediaItem = newQueue[index];
final newMediaItem = oldMediaItem.copyWith(duration: duration);
newQueue[index] = newMediaItem;
queue.add(newQueue);
mediaItem.add(newMediaItem);
});
}
@override
Future<void> skipToNext() => _player.seekToNext();
@override
Future<void> skipToPrevious() => _player.seekToPrevious();
void _listenForCurrentSongIndexChanges() {
_player.currentIndexStream.listen((index) {
final playlist = queue.value;
if (index == null || playlist.isEmpty) return;
mediaItem.add(playlist[index]);
logger.i(playlist);
});
}
@override
Future<void> addQueueItem(MediaItem mediaItem) async {
// manage Just Audio
final audioSource = _createAudioSource(mediaItem);
_playlist.add(audioSource);
// notify system
final newQueue = queue.value..add(mediaItem);
queue.add(newQueue);
}
@override
Future<void> removeQueueItemAt(int index) async {
// manage Just Audio
_playlist.removeAt(index);
// notify system
final newQueue = queue.value..removeAt(index);
queue.add(newQueue);
}
@override
Future<void> skipToQueueItem(int index) async {
if (index < 0 || index >= queue.value.length) return;
if (_player.shuffleModeEnabled) {
index = _player.shuffleIndices![index];
}
_player.seek(Duration.zero, index: index);
}
@override
Future<void> stop() async {
await _player.dispose();
return super.stop();
}
}
I tried including pageManager.play in the init section of Player and PageManager, but nothing happens. I think I need to set the audiosource as a concatenatingAudioSource, but I created a method to execute it and passed it _playlist, running it under _listenForCurrentSongIndexChanges(), but nothing happened.
Upvotes: 1
Views: 1171
Reputation: 31
Realized the play method was executing before the init method finished, so simple delay worked.
Upvotes: 2