Skip to content

Commit

Permalink
feat: Support byte array and data URIs via mimeType (#1763)
Browse files Browse the repository at this point in the history
## Related Issues

Closes #1692, closes #1670, closes #1663, closes #1269
  • Loading branch information
Gustl22 committed Mar 18, 2024
1 parent d306936 commit eaf7ce8
Show file tree
Hide file tree
Showing 16 changed files with 316 additions and 81 deletions.
63 changes: 53 additions & 10 deletions .github/workflows/test.yml
Expand Up @@ -230,8 +230,9 @@ jobs:
working-directory: ./packages/audioplayers/example/android
run: ./gradlew test

ios:
runs-on: macos-latest
ios-16:
# Run lib tests only to ensure compatibility with iOS 16.
runs-on: macos-13
timeout-minutes: 60
if: inputs.enable_ios
steps:
Expand All @@ -242,24 +243,45 @@ jobs:
channel: ${{ inputs.flutter_channel }}
- uses: bluefireteam/melos-action@main

- name: List all simulators
run: "xcrun simctl list devices"
- name: Start simulator
- name: Run Flutter integration tests
working-directory: ./packages/audioplayers/example
run: |
UDID=$(xcrun simctl list devices | grep "iPhone" | sed -n 1p | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})")
sudo xcode-select -switch /Applications/Xcode_14.3.1.app/Contents/Developer
UDID=$(xcrun simctl create test-se-16-4 com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation com.apple.CoreSimulator.SimRuntime.iOS-16-4)
xcrun simctl list devices
echo "Using simulator $UDID"
xcrun simctl boot "${UDID:?No Simulator with this name iPhone found}"
sudo xcode-select -switch /Applications/Xcode_15.2.app/Contents/Developer
( cd server; dart run bin/server.dart ) &
flutter test -d $UDID integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true
ios:
runs-on: macos-14
timeout-minutes: 60
if: inputs.enable_ios
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ inputs.flutter_version }}
channel: ${{ inputs.flutter_channel }}
- uses: bluefireteam/melos-action@main

- name: Run Flutter integration tests
working-directory: ./packages/audioplayers/example
# Need to execute lib and app tests one by one, see: https://github.com/flutter/flutter/issues/101031
run: |
UDID=$(xcrun simctl create test-se-17-2 com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation com.apple.CoreSimulator.SimRuntime.iOS-17-2)
xcrun simctl list devices
echo "Using simulator $UDID"
xcrun simctl boot "${UDID:?No Simulator with this name iPhone found}"
( cd server; dart run bin/server.dart ) &
flutter test integration_test/platform_test.dart --dart-define USE_LOCAL_SERVER=true
flutter test integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true
flutter test integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true
flutter test -d $UDID integration_test/platform_test.dart --dart-define USE_LOCAL_SERVER=true
flutter test -d $UDID integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true
flutter test -d $UDID integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true
macos:
macos-13:
# TODO: Run lib tests only to ensure compatibility with macOS 13, once tests for macOS 14 succeed.
runs-on: macos-13
timeout-minutes: 30
if: inputs.enable_macos
Expand All @@ -280,6 +302,27 @@ jobs:
flutter test -d macos integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true
flutter test -d macos integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true
macos:
runs-on: macos-14
timeout-minutes: 30
if: false # TODO: Tests on macOS 14 currently fail.
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ inputs.flutter_version }}
channel: ${{ inputs.flutter_channel }}
- uses: bluefireteam/melos-action@main

- name: Run Flutter integration tests
working-directory: ./packages/audioplayers/example
# Need to execute lib and app tests one by one, see: https://github.com/flutter/flutter/issues/101031
run: |
( cd server; dart run bin/server.dart ) &
flutter test -d macos integration_test/platform_test.dart --dart-define USE_LOCAL_SERVER=true
flutter test -d macos integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true
flutter test -d macos integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true
windows:
runs-on: windows-latest
timeout-minutes: 30
Expand Down
2 changes: 1 addition & 1 deletion feature_parity_table.md
Expand Up @@ -31,7 +31,7 @@ Note: LLM means Low Latency Mode.
<tr><td>local asset</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td></tr>
<tr><td>external URL file</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td></tr>
<tr><td>external URL stream</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td></tr>
<tr><td>byte array</td><td>SDK >=23</td><td>not yet</td><td>not yet</td><td>not yet</td><td>yes</td><td>not yet</td></tr>
<tr><td>byte array</td><td>SDK >=23</td><td>via conversion</td><td>via conversion</td><td>via conversion</td><td>yes</td><td>via conversion</td></tr>
<tr><td colspan="7"><strong>Audio Config</strong></td></tr>
<tr><td>set url</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td></tr>
<tr><td>audio cache (pre-load)</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td></tr>
Expand Down
Binary file added packages/audioplayers/example/assets/coins.mp3
Binary file not shown.
@@ -1,5 +1,6 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers_example/tabs/sources.dart';
import 'package:http/http.dart';

import '../platform_features.dart';
import '../source_test_data.dart';
Expand Down Expand Up @@ -73,7 +74,7 @@ final specialCharAssetTestData = LibSourceTestData(
);

final noExtensionAssetTestData = LibSourceTestData(
source: AssetSource(noExtensionAsset),
source: AssetSource(noExtensionAsset, mimeType: 'audio/wav'),
duration: const Duration(milliseconds: 451),
);

Expand All @@ -82,6 +83,24 @@ final nonExistentUrlTestData = LibSourceTestData(
duration: null,
);

final wavDataUriTestData = LibSourceTestData(
source: UrlSource(wavDataUri),
duration: const Duration(milliseconds: 451),
);

final mp3DataUriTestData = LibSourceTestData(
source: UrlSource(mp3DataUri),
duration: const Duration(milliseconds: 444),
);

Future<LibSourceTestData> mp3BytesTestData() async => LibSourceTestData(
source: BytesSource(
await readBytes(Uri.parse(mp3Url1)),
mimeType: 'audio/mpeg',
),
duration: const Duration(minutes: 3, seconds: 30, milliseconds: 76),
);

// Some sources are commented which are considered redundant
Future<List<LibSourceTestData>> getAudioTestDataList() async {
return [
Expand All @@ -100,21 +119,23 @@ Future<List<LibSourceTestData>> getAudioTestDataList() async {
if (_features.hasUrlSource && _features.hasPlaylistSourceType)
m3u8UrlTestData,
if (_features.hasUrlSource) mpgaUrlTestData,
if (_features.hasDataUriSource) wavDataUriTestData,
// if (_features.hasDataUriSource) mp3DataUriTestData,
if (_features.hasAssetSource) wavAsset2TestData,
/*if (_features.hasAssetSource)
LibSourceTestData(
source: AssetSource(mp3Asset),
duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119),
),*/
if (_features.hasBytesSource)
LibSourceTestData(
source: BytesSource(await AudioCache.instance.loadAsBytes(wavAsset2)),
duration: const Duration(seconds: 1, milliseconds: 068),
),
if (_features.hasBytesSource) await mp3BytesTestData(),
/*if (_features.hasBytesSource)
// Cache not working for web
LibSourceTestData(
source: BytesSource(await readBytes(Uri.parse(mp3Url1))),
duration: const Duration(minutes: 3, seconds: 30, milliseconds: 76),
source: BytesSource(
await AudioCache.instance.loadAsBytes(wavAsset2),
mimeType: 'audio/wav',
),
duration: const Duration(seconds: 1, milliseconds: 068),
),*/
];
}
27 changes: 22 additions & 5 deletions packages/audioplayers/example/integration_test/lib_test.dart
Expand Up @@ -13,8 +13,6 @@ void main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final features = PlatformFeatures.instance();
final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
final isIOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
final isMacOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS;
final audioTestDataList = await getAudioTestDataList();

testWidgets('test asset source with special char',
Expand Down Expand Up @@ -69,11 +67,30 @@ void main() async {

await player.dispose();
},
// Darwin does not support files without extension unless its specified
// #803, https://stackoverflow.com/a/54087143/5164462
skip: isIOS || isMacOS,
);

testWidgets('data URI source', (WidgetTester tester) async {
final player = AudioPlayer();

await player.play(mp3DataUriTestData.source);
// Sources take some time to get initialized
await tester.pumpPlatform(const Duration(seconds: 8));
await player.stop();

await player.dispose();
});

testWidgets('bytes array source', (WidgetTester tester) async {
final player = AudioPlayer();

await player.play((await mp3BytesTestData()).source);
// Sources take some time to get initialized
await tester.pumpPlatform(const Duration(seconds: 8));
await player.stop();

await player.dispose();
});

group('AP events', () {
late AudioPlayer player;

Expand Down
Expand Up @@ -3,7 +3,6 @@ import 'package:flutter/foundation.dart';
/// Specify supported features for a platform.
class PlatformFeatures {
static const webPlatformFeatures = PlatformFeatures(
hasBytesSource: false,
hasPlaylistSourceType: false,
hasLowLatency: false,
hasReleaseModeRelease: false,
Expand All @@ -21,6 +20,7 @@ class PlatformFeatures {
);

static const iosPlatformFeatures = PlatformFeatures(
hasDataUriSource: false,
hasBytesSource: false,
hasPlaylistSourceType: false,
hasReleaseModeRelease: false,
Expand All @@ -29,6 +29,7 @@ class PlatformFeatures {
);

static const macPlatformFeatures = PlatformFeatures(
hasDataUriSource: false,
hasBytesSource: false,
hasPlaylistSourceType: false,
hasLowLatency: false,
Expand All @@ -43,6 +44,7 @@ class PlatformFeatures {
);

static const linuxPlatformFeatures = PlatformFeatures(
hasDataUriSource: false,
hasBytesSource: false,
hasLowLatency: false,
hasReleaseModeRelease: false,
Expand All @@ -58,6 +60,7 @@ class PlatformFeatures {
);

static const windowsPlatformFeatures = PlatformFeatures(
hasDataUriSource: false,
hasPlaylistSourceType: false,
hasLowLatency: false,
hasReleaseModeRelease: false,
Expand All @@ -70,6 +73,7 @@ class PlatformFeatures {
);

final bool hasUrlSource;
final bool hasDataUriSource;
final bool hasAssetSource;
final bool hasBytesSource;

Expand Down Expand Up @@ -97,6 +101,7 @@ class PlatformFeatures {

const PlatformFeatures({
this.hasUrlSource = true,
this.hasDataUriSource = true,
this.hasAssetSource = true,
this.hasBytesSource = true,
this.hasPlaylistSourceType = true,
Expand Down
46 changes: 40 additions & 6 deletions packages/audioplayers/example/lib/tabs/sources.dart

Large diffs are not rendered by default.

66 changes: 52 additions & 14 deletions packages/audioplayers/lib/src/audioplayer.dart
@@ -1,10 +1,12 @@
import 'dart:async';
import 'dart:io';

import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers/src/uri_ext.dart';
import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';

const _uuid = Uuid();
Expand Down Expand Up @@ -370,13 +372,24 @@ class AudioPlayer {
///
/// The resources will start being fetched or buffered as soon as you call
/// this method.
Future<void> setSourceUrl(String url) async {
_source = UrlSource(url);
Future<void> setSourceUrl(String url, {String? mimeType}) async {
if (!kIsWeb &&
defaultTargetPlatform != TargetPlatform.android &&
url.startsWith('data:')) {
// Convert data URI's to bytes (native support for web and android).
final uriData = UriData.fromUri(Uri.parse(url));
mimeType ??= url.substring(url.indexOf(':') + 1, url.indexOf(';'));
await setSourceBytes(uriData.contentAsBytes(), mimeType: mimeType);
return;
}

_source = UrlSource(url, mimeType: mimeType);
// Encode remote url to avoid unexpected failures.
await _completePrepared(
() => _platform.setSourceUrl(
playerId,
UriCoder.encodeOnce(url),
mimeType: mimeType,
isLocal: false,
),
);
Expand All @@ -386,10 +399,15 @@ class AudioPlayer {
///
/// The resources will start being fetched or buffered as soon as you call
/// this method.
Future<void> setSourceDeviceFile(String path) async {
_source = DeviceFileSource(path);
Future<void> setSourceDeviceFile(String path, {String? mimeType}) async {
_source = DeviceFileSource(path, mimeType: mimeType);
await _completePrepared(
() => _platform.setSourceUrl(playerId, path, isLocal: true),
() => _platform.setSourceUrl(
playerId,
path,
isLocal: true,
mimeType: mimeType,
),
);
}

Expand All @@ -398,19 +416,39 @@ class AudioPlayer {
///
/// The resources will start being fetched or buffered as soon as you call
/// this method.
Future<void> setSourceAsset(String path) async {
_source = AssetSource(path);
Future<void> setSourceAsset(String path, {String? mimeType}) async {
_source = AssetSource(path, mimeType: mimeType);
final cachePath = await audioCache.loadPath(path);
await _completePrepared(
() => _platform.setSourceUrl(playerId, cachePath, isLocal: true),
() => _platform.setSourceUrl(
playerId,
cachePath,
mimeType: mimeType,
isLocal: true,
),
);
}

Future<void> setSourceBytes(Uint8List bytes) async {
_source = BytesSource(bytes);
await _completePrepared(
() => _platform.setSourceBytes(playerId, bytes),
);
Future<void> setSourceBytes(Uint8List bytes, {String? mimeType}) async {
if (!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.linux)) {
// Convert to file as workaround
final tempDir = (await getTemporaryDirectory()).path;
final bytesHash = Object.hashAll(bytes)
.toUnsigned(20)
.toRadixString(16)
.padLeft(5, '0');
final file = File('$tempDir/$bytesHash');
await file.writeAsBytes(bytes);
await setSourceDeviceFile(file.path, mimeType: mimeType);
} else {
_source = BytesSource(bytes, mimeType: mimeType);
await _completePrepared(
() => _platform.setSourceBytes(playerId, bytes, mimeType: mimeType),
);
}
}

/// Set the PositionUpdater to control how often the position stream will be
Expand Down

0 comments on commit eaf7ce8

Please sign in to comment.