diff --git a/getting_started.md b/getting_started.md index 36a00f61a..f4006ec23 100644 --- a/getting_started.md +++ b/getting_started.md @@ -3,7 +3,9 @@ This tutorial should help you get started with the audioplayers library, covering the basics but guiding you all the way through advanced features. You can also play around with our [official example app](https://bluefireteam.github.io/audioplayers/) and [explore the code](https://github.com/bluefireteam/audioplayers/tree/main/packages/audioplayers/example), that showcases every feature the library has to offer. -In order to install this package, add the [latest version](pub.dev/packages/audioplayers) of `audioplayers` to your `pubspec.yaml` file. This packages uses [the Federated Plugin](https://docs.flutter.dev/development/packages-and-plugins/developing-packages) guidelines to support multiple platforms, so it should just work on all supported platforms your app is built for without any extra configuration. You should not need to add the `audioplayers_*` packages directly. +In order to install this package, add the [latest version](pub.dev/packages/audioplayers) of `audioplayers` to your `pubspec.yaml` file. +This package uses [the Federated Plugin](https://docs.flutter.dev/development/packages-and-plugins/developing-packages) guidelines to support multiple platforms, so it should just work on all supported platforms your app is built for without any extra configuration. +You do not need to add the `audioplayers_*` packages directly. ## Setup Platforms @@ -29,8 +31,9 @@ Each AudioPlayer is created empty and has to be configured with an audio source The source (cf. packages/audioplayers/lib/src/source.dart) is basically what audio you are playing (a song, sound effect, radio stream, etc), and it can have one of 4 types: 1. **UrlSource**: get the audio from a remote URL from the Internet. This can be a direct link to a supported file to be downloaded, or a radio stream. -1. **DeviceFileSource**: access a file in the user's device, probably selected by a file picker -1. **AssetSource**: play an asset bundled with your app, normally within the `assets` directory +1. **DeviceFileSource**: access a file in the user's device, probably selected by a file picker. +1. **AssetSource**: play an asset bundled with your app, by default within the `assets` directory. + To customize the prefix, see [AudioCache](#audiocache). 1. **BytesSource** (only some platforms): pass in the bytes of your audio directly (read it from anywhere). In order to set the source on your player instance, call `setSource` with the appropriate source object: @@ -76,7 +79,7 @@ Changes the current position (note: this does not affect the "playing" status). Stops the playback but keeps the current position. ```dart - await player.pause(); + await player.pause(); ``` ### stop @@ -164,7 +167,11 @@ The Player Mode represents what kind of native SDK is used to playback audio, wh 1. `.mediaPlayer` (default): for long media files or streams. 1. `.lowLatency`: for short audio files, since it reduces the impacts on visuals or UI performance. -**Note**: on low latency mode, the player won't fire any duration or position updates. Also, it is not possible to use the seek method to set the audio a specific position. +**Note**: on low latency mode, these features are NOT available: +- get duration & duration event +- get position & position event +- playback completion event (this means you are responsible for stopping the player) +- seeking & seek completion event Normally you want to use `.mediaPlayer` unless you care about performance and your audios are short (i.e. for sound effects in games). @@ -182,7 +189,7 @@ You can pick one of 3 options: 1. `.error` (default): show only error messages 1. `.none`: show no messages at all (not recommended) -**Note**: before opening any issue, always try changing the log level to `.info` to gather any information that my assist you on solving the problem. +**Note**: before opening any issue, always try changing the log level to `.info` to gather any information that might assist you with solving the problem. **Note**: despite our best efforts, some native SDK implementations that we use spam a lot of log messages that we currently haven't figured out how to conform to this configuration (specially noticeable on Android). If you would like to contribute with a PR, they are more than welcome! @@ -322,6 +329,31 @@ It works as a cache because it keeps track of the copied files so that you can r If desired, you can change the `AudioCache` per player via the `AudioPlayer().audioCache` property or for all players via `AudioCache.instance`. +#### Local Assets + +When playing local assets, by default every instance of AudioPlayers uses a [shared global instance of AudioCache](https://pub.dev/documentation/audioplayers/latest/audioplayers/AudioPlayer/audioCache.html), that will have a [default prefix "/assets"](https://pub.dev/documentation/audioplayers/latest/audioplayers/AudioCache/prefix.html) configured, as per Flutter conventions. +However, you can easily change that by specifying your own instance of AudioCache with any other (or no) prefix. + +Default behavior, presuming that your audio is stored in `/assets/audio/my-audio.wav`: +```dart +final player = AudioPlayer(); +await player.play(AssetSource('audio/my-audio.wav')); +``` + +Remove the asset prefix for all players: +```dart +AudioCache.instance = AudioCache(prefix: '') +final player = AudioPlayer(); +await player.play(AssetSource('assets/audio/my-audio.wav')); +``` + +Set a different prefix for only one player (e.g. when using assets from another package): +```dart +final player = AudioPlayer(); +player.audioCache = AudioCache(prefix: 'packages/OTHER_PACKAGE/assets/') +await player.play(AssetSource('other-package-audio.wav')); +``` + ### playerId By default, each time you initialize a new instance of AudioPlayer, a unique playerId is generated and assigned to it using the [uuid package](https://pub.dev/packages/uuid). diff --git a/packages/audioplayers/example/README.md b/packages/audioplayers/example/README.md index 3e4070f9b..2fa4baf8b 100644 --- a/packages/audioplayers/example/README.md +++ b/packages/audioplayers/example/README.md @@ -1,15 +1,25 @@ # AudioPlayer Example This is an example usage of audioplayers plugin. +Check out the live [example app](https://bluefireteam.github.io/audioplayers/) as demonstration. -It's a simple app with three tabs. - -- Remote Url: Plays audio from a remote url from the Internet. -- Local File: Downloads a file to your device in order to play it from your device. -- Local Asset: Play one of the assets bundled with this app. +It's a simple app with several tabs: +- **Src**: Manage audio sources. + - Url: Plays audio from a remote Url from the Internet. + - Asset: Play one of the assets bundled with this app. + - Device File: Play a file from your device from the specified path. + - Byte Array: Play from an array of bytes. +- **Ctrl**: Control playback, such as volume, balance and rate. +- **Stream**: Display of stream updates and properties. +- **Ctx**: Customize the audio context for mobile devices. +- **Log**: Display of logs. This example bundles a `PlayerWidget` that could be used as a very simple audio player interface. +## Setup + +In order to successfully run the example locally, you have to [set up](https://github.com/bluefireteam/audioplayers/blob/main/contributing.md#environment-setup) your environment with `melos`. + ## Dart Environment Variables Set the following variables as additional args `--dart-define MY_VAR=xyz`: diff --git a/packages/audioplayers/example/example.md b/packages/audioplayers/example/example.md new file mode 100644 index 000000000..21c206fbb --- /dev/null +++ b/packages/audioplayers/example/example.md @@ -0,0 +1,243 @@ +# Simple audio player app example + +A complete example showcasing all _audioplayers_ features can be found in our [repository](https://github.com/bluefireteam/audioplayers/tree/main/packages/audioplayers/example). +Also check out our live [web app](https://bluefireteam.github.io/audioplayers/). + +```dart +import 'dart:async'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MaterialApp(home: _SimpleExampleApp())); +} + +class _SimpleExampleApp extends StatefulWidget { + const _SimpleExampleApp(); + + @override + _SimpleExampleAppState createState() => _SimpleExampleAppState(); +} + +class _SimpleExampleAppState extends State<_SimpleExampleApp> { + late AudioPlayer player = AudioPlayer(); + + @override + void initState() { + super.initState(); + + // Create the audio player. + player = AudioPlayer(); + + // Set the release mode to keep the source after playback has completed. + player.setReleaseMode(ReleaseMode.stop); + + // Start the player as soon as the app is displayed. + WidgetsBinding.instance.addPostFrameCallback((_) async { + await player.setSource(AssetSource('ambient_c_motion.mp3')); + await player.resume(); + }); + } + + @override + void dispose() { + // Release all sources and dispose the player. + player.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Simple Player'), + ), + body: PlayerWidget(player: player), + ); + } +} + +// The PlayerWidget is a copy of "/lib/components/player_widget.dart". +//#region PlayerWidget + +class PlayerWidget extends StatefulWidget { + final AudioPlayer player; + + const PlayerWidget({ + required this.player, + super.key, + }); + + @override + State createState() { + return _PlayerWidgetState(); + } +} + +class _PlayerWidgetState extends State { + PlayerState? _playerState; + Duration? _duration; + Duration? _position; + + StreamSubscription? _durationSubscription; + StreamSubscription? _positionSubscription; + StreamSubscription? _playerCompleteSubscription; + StreamSubscription? _playerStateChangeSubscription; + + bool get _isPlaying => _playerState == PlayerState.playing; + + bool get _isPaused => _playerState == PlayerState.paused; + + String get _durationText => _duration?.toString().split('.').first ?? ''; + + String get _positionText => _position?.toString().split('.').first ?? ''; + + AudioPlayer get player => widget.player; + + @override + void initState() { + super.initState(); + // Use initial values from player + _playerState = player.state; + player.getDuration().then( + (value) => setState(() { + _duration = value; + }), + ); + player.getCurrentPosition().then( + (value) => setState(() { + _position = value; + }), + ); + _initStreams(); + } + + @override + void setState(VoidCallback fn) { + // Subscriptions only can be closed asynchronously, + // therefore events can occur after widget has been disposed. + if (mounted) { + super.setState(fn); + } + } + + @override + void dispose() { + _durationSubscription?.cancel(); + _positionSubscription?.cancel(); + _playerCompleteSubscription?.cancel(); + _playerStateChangeSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).primaryColor; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + key: const Key('play_button'), + onPressed: _isPlaying ? null : _play, + iconSize: 48.0, + icon: const Icon(Icons.play_arrow), + color: color, + ), + IconButton( + key: const Key('pause_button'), + onPressed: _isPlaying ? _pause : null, + iconSize: 48.0, + icon: const Icon(Icons.pause), + color: color, + ), + IconButton( + key: const Key('stop_button'), + onPressed: _isPlaying || _isPaused ? _stop : null, + iconSize: 48.0, + icon: const Icon(Icons.stop), + color: color, + ), + ], + ), + Slider( + onChanged: (value) { + final duration = _duration; + if (duration == null) { + return; + } + final position = value * duration.inMilliseconds; + player.seek(Duration(milliseconds: position.round())); + }, + value: (_position != null && + _duration != null && + _position!.inMilliseconds > 0 && + _position!.inMilliseconds < _duration!.inMilliseconds) + ? _position!.inMilliseconds / _duration!.inMilliseconds + : 0.0, + ), + Text( + _position != null + ? '$_positionText / $_durationText' + : _duration != null + ? _durationText + : '', + style: const TextStyle(fontSize: 16.0), + ), + ], + ); + } + + void _initStreams() { + _durationSubscription = player.onDurationChanged.listen((duration) { + setState(() => _duration = duration); + }); + + _positionSubscription = player.onPositionChanged.listen( + (p) => setState(() => _position = p), + ); + + _playerCompleteSubscription = player.onPlayerComplete.listen((event) { + setState(() { + _playerState = PlayerState.stopped; + _position = Duration.zero; + }); + }); + + _playerStateChangeSubscription = + player.onPlayerStateChanged.listen((state) { + setState(() { + _playerState = state; + }); + }); + } + + Future _play() async { + final position = _position; + if (position != null && position.inMilliseconds > 0) { + await player.seek(position); + } + await player.resume(); + setState(() => _playerState = PlayerState.playing); + } + + Future _pause() async { + await player.pause(); + setState(() => _playerState = PlayerState.paused); + } + + Future _stop() async { + await player.stop(); + setState(() { + _playerState = PlayerState.stopped; + _position = Duration.zero; + }); + } +} + +//#endregion +``` diff --git a/packages/audioplayers/example/lib/components/player_widget.dart b/packages/audioplayers/example/lib/components/player_widget.dart index 70424d3ac..ddc051a1d 100644 --- a/packages/audioplayers/example/lib/components/player_widget.dart +++ b/packages/audioplayers/example/lib/components/player_widget.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; +// This code is also used in the example.md. Please keep it up to date. class PlayerWidget extends StatefulWidget { final AudioPlayer player; @@ -106,12 +107,12 @@ class _PlayerWidgetState extends State { ], ), Slider( - onChanged: (v) { + onChanged: (value) { final duration = _duration; if (duration == null) { return; } - final position = v * duration.inMilliseconds; + final position = value * duration.inMilliseconds; player.seek(Duration(milliseconds: position.round())); }, value: (_position != null && @@ -129,7 +130,6 @@ class _PlayerWidgetState extends State { : '', style: const TextStyle(fontSize: 16.0), ), - Text('State: ${_playerState ?? '-'}'), ], ); } diff --git a/packages/audioplayers/example/lib/tabs/sources.dart b/packages/audioplayers/example/lib/tabs/sources.dart index b3de1248e..62615c101 100644 --- a/packages/audioplayers/example/lib/tabs/sources.dart +++ b/packages/audioplayers/example/lib/tabs/sources.dart @@ -391,7 +391,7 @@ class _SourceDialogState extends State<_SourceDialog> { AssetSource: 'Asset', DeviceFileSource: 'Device File', UrlSource: 'Url', - BytesSource: 'Byte array', + BytesSource: 'Byte Array', }, selected: sourceType, onChange: (Type? value) { diff --git a/packages/audioplayers_platform_interface/lib/src/api/player_mode.dart b/packages/audioplayers_platform_interface/lib/src/api/player_mode.dart index 8f70499ea..36d751ab9 100644 --- a/packages/audioplayers_platform_interface/lib/src/api/player_mode.dart +++ b/packages/audioplayers_platform_interface/lib/src/api/player_mode.dart @@ -8,8 +8,9 @@ enum PlayerMode { /// Ideal for short audio files, since it reduces the impacts on visuals or /// UI performance. /// - /// In this mode the backend won't fire any duration or position updates. - /// Also, it is not possible to use the seek method to set the audio a + /// In this mode the backend won't fire any duration, position or playback + /// completion events. This means you are responsible for stopping the player. + /// Also, it is not possible to use the seek method to set the audio to a /// specific position. lowLatency, } diff --git a/troubleshooting.md b/troubleshooting.md index 33d44dace..9b6d3c685 100644 --- a/troubleshooting.md +++ b/troubleshooting.md @@ -9,7 +9,10 @@ For that check out our [Contributing Guide](https://github.com/bluefireteam/audi ### Supported Formats / Encodings -Not all formats are supported by all platforms. Essentially `audioplayers` is just centralized interface that communicate with native audio players on each platform. We are not parsing the bytes of your song. Each platform has its own native support. Please do not open issues regarding encoding/file format compatibility unless it is an AudioPlayers specific issue. +Not all formats are supported by all platforms. +Essentially `audioplayers` is just centralized interface that communicate with native audio players on each platform. +We are not parsing the bytes of your song. Each platform has its own native support. +**Please do not open issues regarding encoding / audio format compatibility unless it is an AudioPlayers specific issue.** You can check a list of supported formats below: @@ -20,11 +23,15 @@ You can check a list of supported formats below: - [Windows](https://learn.microsoft.com/en-us/windows/win32/medfound/supported-media-formats-in-media-foundation) - Linux: List of defined [audio types](https://gstreamer.freedesktop.org/documentation/plugin-development/advanced/media-types.html?gi-language=c#table-of-audio-types) and their according [Plugins](https://gstreamer.freedesktop.org/documentation/plugins_doc.html?gi-language=c) +Also, there is no guarantee that the file extension matches the audio format. +A file encoded as Opus (`.ogg`) can easily be renamed to `.mp3`, but that doesn't mean it can be played by the platform's audio player. +Please verify that the real encoding / audio format is supported by analyzing the audio file (e.g. with [Aconvert](https://www.aconvert.com/analyze.html)). + ### Issues with remote URLs #### Unsafe HTTP -It is very common for mobile platforms to forbid non-HTTPS traffic due to it's lack of encryption and severe security deficiency. However, there are ways to bypass this protection. +It is very common for mobile platforms to forbid non-HTTPS traffic due to its lack of encryption and severe security deficiency. However, there are ways to bypass this protection. On iOS and macOS, edit your `.plist` and add: @@ -52,7 +59,7 @@ On Android, add `android:usesCleartextTraffic="true"` to your `AndroidManifest.x ``` -### [Web] CORS Policy +#### [Web] CORS Policy To be able to play your own resources on Web you need to make sure your server has CORS support enabled or [temporarily disable](https://stackoverflow.com/a/74783428/5164462) the security feature in your browser. @@ -69,10 +76,9 @@ If the issue persists, then open the issue, including the file so we can test. O ### Issues with local Assets and AudioCache -Flutter requires that assets are specified on your `pubspec.yaml` file, under `flutter > assets`; check [this](https://github.com/bluefireteam/audioplayers/blob/main/packages/audioplayers/example/pubspec.yaml#L29) for an example. +[Flutter requires](https://docs.flutter.dev/ui/assets/assets-and-images) that assets are specified on your `pubspec.yaml` file, under `flutter > assets`; check [this](https://github.com/bluefireteam/audioplayers/blob/main/packages/audioplayers/example/pubspec.yaml#L29) for an example. -**Note**: when playing local assets, by default every instance of AudioPlayers uses a [shared global instance of AudioCache](https://github.com/bluefireteam/audioplayers/blob/main/packages/audioplayers/lib/src/audioplayer.dart#L24), that will have a [default prefix "/assets"](https://github.com/bluefireteam/audioplayers/blob/main/packages/audioplayers/lib/src/audio_cache.dart#L41) configured, as per Flutter conventions. -However you can easily change that by specifying your own instance of AudioCache with any other (or no) prefix. +**Note**: Make sure you have set the path to your asset correctly, see the [AudioCache](https://github.com/bluefireteam/audioplayers/blob/main/getting_started.md#audiocache) concept. ### [iOS] Background Audio