Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support byte array and data URIs via mimeType #1763

Merged
merged 44 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
cbe4c41
Test data uri
Gustl22 Mar 2, 2024
fc8a547
Mp3 data uri
Gustl22 Mar 2, 2024
82ffbac
feat: Support data URIs and byte arrays
Gustl22 Mar 13, 2024
e7c3f20
implement on web
Gustl22 Mar 13, 2024
a05352a
Adapt duration
Gustl22 Mar 13, 2024
e4d2515
Merge remote-tracking branch 'upstream/main' into gustl22/1728-uri-te…
Gustl22 Mar 13, 2024
f31d4af
Adapt tests, features and sources
Gustl22 Mar 13, 2024
ce189ed
darwin: Attempt error handling
Gustl22 Mar 13, 2024
7a1a5a0
darwin: Attempt error handling
Gustl22 Mar 13, 2024
335adc1
darwin: Attempt error handling 3
Gustl22 Mar 13, 2024
b7ebbaf
darwin: Attempt error handling 4
Gustl22 Mar 13, 2024
b18db39
darwin: mimeType
Gustl22 Mar 13, 2024
19ee6d2
mime types
Gustl22 Mar 13, 2024
a8956a3
darwin: Error warning
Gustl22 Mar 13, 2024
1e84d9b
Format + Fix Optional
Gustl22 Mar 13, 2024
fa90368
Set mimetype for debug
Gustl22 Mar 13, 2024
58cce78
Adapt tests
Gustl22 Mar 13, 2024
7b84bde
Test with newest darwin OS
Gustl22 Mar 13, 2024
6d9494c
swift format (?)
Gustl22 Mar 13, 2024
4cb8f8c
WIP: Coverage of all macos & ios
Gustl22 Mar 14, 2024
967ea28
swift format
Gustl22 Mar 14, 2024
2aa17df
Log error
Gustl22 Mar 14, 2024
3aea341
Hand over mimeType
Gustl22 Mar 14, 2024
ac2b98b
Revert "Log error"
Gustl22 Mar 14, 2024
0b781a7
Attempt to fix
Gustl22 Mar 14, 2024
86b147e
Attempt to fix 2
Gustl22 Mar 14, 2024
af6e9dd
Attempt to fix 3
Gustl22 Mar 14, 2024
ca5b1f5
Use AVURLAssetOutOfBandMIMETypeKey for all versions / platforms
Gustl22 Mar 14, 2024
8580d9f
Revert "Use AVURLAssetOutOfBandMIMETypeKey for all versions / platforms"
Gustl22 Mar 15, 2024
d902852
Revert "Attempt to fix 3"
Gustl22 Mar 15, 2024
6a5b7ac
try macos-13
Gustl22 Mar 15, 2024
418aafc
download all ios
Gustl22 Mar 15, 2024
804bcde
Attempt to create specific device
Gustl22 Mar 15, 2024
7f48764
Install ios 16.4 device
Gustl22 Mar 15, 2024
1471576
Install ios 16 device
Gustl22 Mar 15, 2024
9d12ac6
Use static ios versions
Gustl22 Mar 15, 2024
6bafc0f
Specify udid
Gustl22 Mar 15, 2024
57f8eae
switch xcode
Gustl22 Mar 15, 2024
0d0f6d6
make ios tests ready
Gustl22 Mar 15, 2024
5287e73
finish up tests
Gustl22 Mar 15, 2024
890b38d
feat: Support bytes source and data URIs
Gustl22 Mar 15, 2024
9543ff4
Skip macOS 14 tests
Gustl22 Mar 17, 2024
a4a4252
fix error with download date time
Gustl22 Mar 17, 2024
c17c799
Merge remote-tracking branch 'upstream/main' into 1728-uri-test-data
Gustl22 Mar 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 53 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
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-14:
# runs-on: macos-14
# timeout-minutes: 30
# if: inputs.enable_macos
# 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
Original file line number Diff line number Diff line change
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.
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be removed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No this is just added as redundancy (like the other sources, but in a different format). But in order to make the tests faster, I commented them out. They are useful for testing certain scenarios though.

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading