Streaming audio in Flutter with Just Audio

Suragch
Suragch
Apr 24 · 11 min read

How to play a song or podcast from a URL

In my last article, Playing short audio clips in Flutter with Just Audio, I demonstrated how to set up Just Audio and play audio clips that only lasts a few seconds. If you’re new to the Just Audio plugin, I recommend you go through that tutorial first.

This tutorial will demonstrate how to play long-form audio like songs, podcasts, or lectures. While it’s completely possible to play these from files stored on the user’s device, a more common scenario is to stream them based on a URL link, so that’s the direction this tutorial will go. Most of what you learn will also apply to playing a local file, though.

In order to keep this tutorial focused, it will only cover basic topics like playing, pausing, buffering, and seeking in a single audio file. For a full-fledged app, you would also want to implement playlists and background audio, but those topics will have to wait for another tutorial.

If you get lost along the way you can refer to the project source code here.

Setup

Rather than describe the detailed setup process here I just briefly mention the steps. Refer to the documentation and my previous tutorial for more details.

Start a new project.

Make sure that you’re using null-safe Dart 2.12 or higher in pubspec.yaml (not required for Just Audio but the code in this tutorial will use null safety):

environment:
sdk: ">=2.12.0 <3.0.0"

Add just_audio to pubspec.yaml:

dependencies:
just_audio: ^0.7.4

If you’re creating a macOS app, then you need to allow the app to access the internet. The directions are here.

This tutorial will play audio from an HTTPS link, but if you want to substitute an HTTP link then you should enable clear text. The directions are here.

It’s possible to use a standard Flutter Slider widget to display the progress of the audio playback. If you want to go that route, there is an example in the Just Audio example project. However, I’ve found the slider hard to use, so I created a ProgressBar widget and published it as a package just for this purpose. (You can read the story in the article Creating a Flutter widget from scratch.)

Here is what a styled version looks like:

ProgressBar widget

Add audio_video_progress_bar to pubspec.yaml:

dependencies:
audio_video_progress_bar: ^0.4.0

Keeping the UI and logic separate

In this tutorial, you’re going to follow a minimalist state management style so that your UI is kept separate from the audio player logic. If you don’t do that the code tends to be hard to read. Also, by isolating the audio player from the UI, it will be much easier to switch to a different audio plugin later if you so decide. Not that I think you’ll need to, but you never know. You can learn more about this architectural style in the article Flutter state management for minimalists.

Create a state management file in the lib folder named page_manager.dart. Then add the following empty class:

class PageManager {}

Then open main.dart and replace the contentsles contenus with the following UI layout:

This initializes the state management class and also builds a layout with the progress bar and a play button.

Run the app. Here’s what it looks like on an Android emulator:

You can move the progress bar thumb and press the play button, but since they aren’t hooked up to the audio player yet, there’s no result.

Analyzing the state

Looking at the UI, there are two units of the state that can change independently of each other. One is the progress bar and the other is the type of button showing:

The progress bar will be updated every few milliseconds with the current play progress, the buffered progress, and the total audio duration.

The button widget, on the other hand, could be a play button, a pause button, or a spinning progress indicator while the audio is still loading. This state will change much less frequently than the progress bar.

Since there are two unitsunités of state, you’ll add two value notifiers to the statel'état management class.

Replace the code in page_manager.dart with the following:

This adds two value notifiers:

  • progressNotifier will notify the progress bar with new values for the current playback progress, the buffered progress, and the total audio duration. These values are grouped in a data class called ProgressBarState.
  • buttonNotifier will notify the UI about which type of button or widget to show depending on the ButtonState enum, which has three values: paused, playing, and loading.

You’ll wrap the relevant parts of the UI with ValueListenableBuilder widgets so that they’ll trigger a rebuild when there’s a state change.

In main.dart replace the ProgressBar widget with the following:

ValueListenableBuilder<ProgressBarState>(
valueListenable: _pageManager.progressNotifier,
builder: (_, value, __) {
return ProgressBar(
progress: value.current,
buffered: value.buffered,
total: value.total,
);
},
),

And replace the IconButton widget with the following:

ValueListenableBuilder<ButtonState>(
valueListenable: _pageManager.buttonNotifier,
builder: (_, value, __) {
switch (value) {
case ButtonState.loading:
return Container(
margin: EdgeInsets.all(8.0),
width: 32.0,
height: 32.0,
child: CircularProgressIndicator(),
);
case ButtonState.paused:
return IconButton(
icon: Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: () {},
);
case ButtonState.playing:
return IconButton(
icon: Icon(Icons.paused),
iconSize: 32.0,
onPressed: () {},
);
}
},
),

Now both parts of the UI that need to react to state changes are listening to the value notifiers in the state management layer.

Restart the app to make suresûr you haven’t broken anything. It should still look and behave the same as before. And by behave I mean do nothing useful.

The architecture is all set up now. You’re finally ready to add the audio player.

Adding the audio player

There will be a few steps to this. Take them one at a time.

The first step is to create the audio player and give it a song to play.

Open page_manage.dart and add the just_audio import:

import 'package:just_audio/just_audio.dart';

Then add the following code inside the PageManager class:

static const url = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3';late AudioPlayer _audioPlayer;PageManager() {
_init();
}
void _init() async {
_audioPlayer = AudioPlayer();
await _audioPlayer.setUrl(url);
}

Notes:

  • The song is a freely available mp3 from SoundHelix.
  • Since you can’t perform an async task inside of a constructor, you moved it to an _init method. There you created the AudioPlayer object and set the URL for the song it will play.
  • Since _init is called from the constructor, the AudioPlayer will start up as soon as this state management class is created. That’s done in the initState method in main.dart.

The UI can show a play or a pause button, so you need methods on the state management class to connect those buttons to.

Add the following methods to PageManager:

void play() {
_audioPlayer.play();
}
void pause() {
_audioPlayer.pause();
}

These methods simply forward the calls to the audio player.

Now back in main.dart, replace the onPressed parameter of the Pause button (that is, the button with pause icon) with the following:

onPressed: _pageManager.play,

And replace the onPressed parameter of the Play button (the one with play_arrow icon) with this:

onPressed: _pageManager.pause

If the player is playing, pressing the button should pause it, and if it’s paused, pressing the button should start it.

It’s always a good idea to dispose the audio player when you are done using it so that it can release its resources. You can accomplish that by first adding a dispose method to PageManager in page_manager.dart:

void dispose() {
_audioPlayer.dispose();
}

And then another one to _MyAppState in main.dart:

@override
void dispose() {
_pageManager.dispose();
super.dispose();
}

Now whenever the Flutter framework disposes the widget state, this will also trigger your state management class to dispose the audio player.

Run the app to see if it works.

The song starts playing so that means something is working, but the progress bar doesn’t update and the play button doesn’t become a pause button. That’s because you aren’t updating any of the notifiers in PlayManager yet. You’ll do that next.

Updating the play buttons

First let’s tackle the play/pause button. If the song is playing, you want to show the pause button, and if the song isn’t playing, you want to show the play button.

Play and pause buttons

So you need to listen for updates to the playing state.

But that’s not the only thing you need to listen for. What about when the song is loading or buffering? In that case you’re going to show a circular progress indicator in place of the play button.

Circular progress indicator

For that you need to know the processing state. Just Audio has an enum for this with the following values:

Just Audio’s ProcessingState enum

You can listen for changes in both the processing state and the playing state from the audio player’s playerStateStream. This stream provides the current PlayerState, which includes a Boolean playing property and a processingState property.

In play_page.dart add the following code to the bottom of the _init method:

_audioPlayer.playerStateStream.listen((playerState) {
final isPlaying = playerState.playing;
final processingState = playerState.processingState;
if (processingState == ProcessingState.loading ||
processingState == ProcessingState.buffering) {
buttonNotifier.value = ButtonState.loading;
} else if (!isPlaying) {
buttonNotifier.value = ButtonState.paused;
} else {
buttonNotifier.value = ButtonState.playing;
}
});

Any timetime the playing state or the processing state changes, this stream will be notified. Since you’re listening to the stream, you convert that data into something you UI understands, that is, the ButtonState. By giving buttonNotifier a new value, this will trigger the button UI to rebuild with the correct widget.

Run your app again to see the improvement.

Note: Chances are you may not see the circular progress indicator if it loads quickly. If you’d like to see it, try temporarily moving the setUrl call to the play method.

The button is working now, but the progress bar still isn’t. Let’s work on that next.

Updating the progress bar

The progress bar needs the following three data points:

  • The current play position
  • The buffered position
  • The total audio duration

AudioPlayer provides this data in three different streams:

  • positionStream: This stream of type Duration gives frequent updates, often enough to make the progress bar thumb look animated as it moves across the bar.
  • bufferedPositionStream: This stream of type Duration gives intermittent updates, enough to make the buffering show as occasional jumps in progress.
  • durationStream: This stream of type Duration? gives a single update shortly after the audio loads. After that there aren’t any more updates until a new audio source is loaded.

You could use the rxdart package to combine these three streams into one. That’s what the example project in the documentation does. However, rather than introducing a new package with its new concepts, let’s just listen to the three streams separately.

Still in play_page.dart, add the following code at the bottom of the _init method:

_audioPlayer.positionStream.listen((position) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: position,
buffered: oldState.buffered,
total: oldState.total,
);
});

This listens for changeschanger in the play position and when those happen you create a new ProgressBarState with the new current position and copies of the old state for buffered and total.

You’ll do almost exactly the same thing for the buffered position. Add the following code at the bottom of the _init method:

_audioPlayer.bufferedPositionStream.listen((bufferedPosition) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: oldState.current,
buffered: bufferedPosition,
total: oldState.total,
);
});

Here you just updated the buffered position and copied the values for the other two.

The process is the same for the duration stream. Add the following stream listener at the bottom of the _init method:

_audioPlayer.durationStream.listen((totalDuration) {
final oldState = progressNotifier.value;
progressNotifier.value = ProgressBarState(
current: oldState.current,
buffered: oldState.buffered,
total: totalDuration ?? Duration.zero,
);
});

Unlike the two previous streams, the duration stream gives nullable values. The progress bar won’t accept a null value, though, so you gave it a default of Duration.zero.

Note: If your audio source doesn’t provide the total duration, then ProgressBar won’t work since both the current progress and buffered progress are calculated as a percentage of the total duration. If this happens to you, then check the server settings and HTTP headers where you get the audio file. I had this problem early on with one of my projects, though I can’t remember my exact solution. Another option is to download the filele fichier first before playing it. That shouldn’t be necessary, though, especially not with the audio file that this tutorial is using.

Run the app again. Now the currrent progress, buffered position, and total duration all update!

(To make the video below, I temporarily moved _audioPlayer.setUrl to the play method so that there would be enough time to see the total duration update).

It’s working well. The dark bluebleus bar and the circular thumb show the current position and the medium blue shows the buffered position. However, you probably noticed in the video that trying to change the positionla positio by moving the thumb had no effect. That’s an easy fix, though.

Seeking to a new audio position

AudioPlayer has a seek method so all you need to do is hookcrochet it up. In play_page.dart, add the following method to PageManager:

void seek(Duration position) {
_audioPlayer.seek(position);
}

As before, this is just creating a public methodméthode on the state management side so that the UI layer has something to call.

In main.dart find the ProgressBar widget and add the onSeek parameter:

onSeek: _pageManager.seek,

Since onSeek provides a duration and _pageManager.seek takes a duration, there’s no need to specify it explicitly.

Run the app again:

Seeking works now!

There’s still a bug, though. When the song completes the button doesn’t change and the thumb just stays at the end. You can solve this problem also by seeking.

Find the _audioPlayer.playerStateStream listener that you added earlier in the _init method. Replace just that stream listener with the following one. The parts that changed are in bold:

_audioPlayer.playerStateStream.listen((playerState) {
final isPlaying = playerState.playing;
final processingState = playerState.processingState;
if (processingState == ProcessingState.loading ||
processingState == ProcessingState.buffering) {
buttonNotifier.value = ButtonState.loading;
} else if (!isPlaying) {
buttonNotifier.value = ButtonState.paused;
} else if (processingState != ProcessingState.completed) {
buttonNotifier.value = ButtonState.playing;
} else { // completed
_audioPlayer.seek(Duration.zero);
_audioPlayer.pause();
}
});

Now when the audio player processing state is completed, you seek to the beginning of the song and pause the playback.

Run the app one more time to make sure that everything is working.

It works! You did it!

Source code

You can find the full source code for this project here:

Going on

For long-playing audio on a mobile device, you’ll almost certainly want to enable background audio. Otherwise, if the user utilisateur exits your app before the song is finished playing, it’s difficult to figure out how to turn it off (at least on Android it is). This is a job for the audio_service plugin, made by the same author as just_audio.

Another common use case is playing multiple songs in a playlist. Just Audio can handle that with ConcatenatingAudioSource. For that, check out the next tutorial in this series:

Follow Flutter Community on Twitter 🐥: https://www.twitter.com/FlutterComm

Flutter Community

Articles and Stories from the Flutter Community

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store