Skip to content

Commit a4d8442

Browse files
Gustl22spydon
andauthored
fix: Improve Error handling for Unsupported Sources (#1625)
# Description - feat: Improved error description for unsupported file formats - fix: Handle white space and special characters in URL and Assets (Web & Darwin) - test: Test files without file extension (not playable Darwin) ## Related Issues Closes #1494 Closes #748 Closes #972 Closes #1546 --------- Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
1 parent 8c3a213 commit a4d8442

19 files changed

Lines changed: 310 additions & 87 deletions

File tree

38.9 KB
Binary file not shown.
38.9 KB
Binary file not shown.
38.9 KB
Binary file not shown.

packages/audioplayers/example/integration_test/lib/lib_source_test_data.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ final wavUrl1TestData = LibSourceTestData(
3131
duration: const Duration(milliseconds: 451),
3232
);
3333

34+
final specialCharUrlTestData = LibSourceTestData(
35+
source: UrlSource(wavUrl3),
36+
duration: const Duration(milliseconds: 451),
37+
);
38+
3439
final mp3Url1TestData = LibSourceTestData(
3540
source: UrlSource(mp3Url1),
3641
duration: const Duration(minutes: 3, seconds: 30, milliseconds: 77),
@@ -57,6 +62,16 @@ final invalidAssetTestData = LibSourceTestData(
5762
duration: null,
5863
);
5964

65+
final specialCharAssetTestData = LibSourceTestData(
66+
source: AssetSource(specialCharAsset),
67+
duration: const Duration(milliseconds: 451),
68+
);
69+
70+
final noExtensionAssetTestData = LibSourceTestData(
71+
source: AssetSource(noExtensionAsset),
72+
duration: const Duration(milliseconds: 451),
73+
);
74+
6075
final nonExistentUrlTestData = LibSourceTestData(
6176
source: UrlSource('non_existent.txt'),
6277
duration: null,

packages/audioplayers/example/integration_test/lib_test.dart

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:audioplayers/audioplayers.dart';
2+
import 'package:audioplayers_example/tabs/sources.dart';
23
import 'package:flutter/foundation.dart';
34
import 'package:flutter_test/flutter_test.dart';
45
import 'package:integration_test/integration_test.dart';
@@ -12,8 +13,79 @@ void main() async {
1213
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
1314
final features = PlatformFeatures.instance();
1415
final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
16+
final isIOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
17+
final isMacOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS;
1518
final audioTestDataList = await getAudioTestDataList();
1619

20+
testWidgets('test asset source with special char',
21+
(WidgetTester tester) async {
22+
final player = AudioPlayer();
23+
24+
await tester.pumpLinux();
25+
await player.play(specialCharAssetTestData.source);
26+
await tester.pumpAndSettle();
27+
// Sources take some time to get initialized
28+
await tester.pump(const Duration(seconds: 8));
29+
await player.stop();
30+
31+
await tester.pumpLinux();
32+
await player.dispose();
33+
});
34+
35+
testWidgets(
36+
'test device file source with special char',
37+
(WidgetTester tester) async {
38+
final player = AudioPlayer();
39+
40+
await tester.pumpLinux();
41+
final path = await player.audioCache.loadPath(specialCharAsset);
42+
expect(path, isNot(contains('%'))); // Ensure path is not URL encoded
43+
await player.play(DeviceFileSource(path));
44+
await tester.pumpAndSettle();
45+
// Sources take some time to get initialized
46+
await tester.pump(const Duration(seconds: 8));
47+
await player.stop();
48+
49+
await tester.pumpLinux();
50+
await player.dispose();
51+
},
52+
skip: kIsWeb,
53+
);
54+
55+
testWidgets('test url source with special char', (WidgetTester tester) async {
56+
final player = AudioPlayer();
57+
58+
await tester.pumpLinux();
59+
await player.play(specialCharUrlTestData.source);
60+
await tester.pumpAndSettle();
61+
// Sources take some time to get initialized
62+
await tester.pump(const Duration(seconds: 8));
63+
await player.stop();
64+
65+
await tester.pumpLinux();
66+
await player.dispose();
67+
});
68+
69+
testWidgets(
70+
'test url source with no extension',
71+
(WidgetTester tester) async {
72+
final player = AudioPlayer();
73+
74+
await tester.pumpLinux();
75+
await player.play(noExtensionAssetTestData.source);
76+
await tester.pumpAndSettle();
77+
// Sources take some time to get initialized
78+
await tester.pump(const Duration(seconds: 8));
79+
await player.stop();
80+
81+
await tester.pumpLinux();
82+
await player.dispose();
83+
},
84+
// Darwin does not support files without extension unless its specified
85+
// #803, https://stackoverflow.com/a/54087143/5164462
86+
skip: isIOS || isMacOS,
87+
);
88+
1789
group('play multiple sources', () {
1890
testWidgets(
1991
'play multiple sources simultaneously',

packages/audioplayers/example/integration_test/platform_test.dart

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ void main() async {
4444
testData: invalidAssetTestData,
4545
);
4646
fail('PlatformException not thrown');
47-
// ignore: avoid_catches_without_on_clauses
48-
} catch (e) {
49-
expect(e, isInstanceOf<PlatformException>());
47+
} on PlatformException catch (e) {
48+
expect(e.message, startsWith('Failed to set source.'));
5049
}
5150
await tester.pumpLinux();
5251
},
@@ -64,8 +63,8 @@ void main() async {
6463
);
6564
fail('PlatformException not thrown');
6665
// ignore: avoid_catches_without_on_clauses
67-
} catch (e) {
68-
expect(e, isInstanceOf<PlatformException>());
66+
} on PlatformException catch (e) {
67+
expect(e.message, startsWith('Failed to set source.'));
6968
}
7069
await tester.pumpLinux();
7170
},
@@ -539,8 +538,8 @@ extension on WidgetTester {
539538
if (source is UrlSource) {
540539
await platform.setSourceUrl(playerId, source.url);
541540
} else if (source is AssetSource) {
542-
final url = await AudioCache.instance.load(source.path);
543-
await platform.setSourceUrl(playerId, url.path, isLocal: true);
541+
final cachePath = await AudioCache.instance.loadPath(source.path);
542+
await platform.setSourceUrl(playerId, cachePath, isLocal: true);
544543
} else if (source is BytesSource) {
545544
await platform.setSourceBytes(playerId, source.bytes);
546545
}

packages/audioplayers/example/lib/tabs/sources.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ final host = useLocalServer ? 'http://$localhost:8080' : 'https://luan.xyz';
1818

1919
final wavUrl1 = '$host/files/audio/coins.wav';
2020
final wavUrl2 = '$host/files/audio/laser.wav';
21+
final wavUrl3 = '$host/files/audio/coins_non_ascii_и.wav';
2122
final mp3Url1 = '$host/files/audio/ambient_c_motion.mp3';
2223
final mp3Url2 = '$host/files/audio/nasa_on_a_mission.mp3';
2324
final m3u8StreamUrl = useLocalServer
@@ -30,6 +31,8 @@ final mpgaStreamUrl = useLocalServer
3031
const wavAsset = 'laser.wav';
3132
const mp3Asset = 'nasa_on_a_mission.mp3';
3233
const invalidAsset = 'invalid.txt';
34+
const specialCharAsset = 'coins_non_ascii_и.wav';
35+
const noExtensionAsset = 'coins_no_extension';
3336

3437
class SourcesTab extends StatefulWidget {
3538
final AudioPlayer player;

packages/audioplayers/example/server/public/index.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,12 @@ <h1>Audioplayers Test Server</h1>
6868
href="https://pub.dev/packages/audioplayers">Audioplayers</a>
6969
package:</p>
7070
<ul>
71-
<li><a href="/files/audio/laser.wav">/files/audio/ambient_c_motion.mp3</a></li>
71+
<li><a href="/files/audio/ambient_c_motion.mp3">/files/audio/ambient_c_motion.mp3</a></li>
7272
<li><a href="/files/audio/coins.wav">/files/audio/coins.wav</a></li>
73+
<li><a href="/files/audio/coins%20whitespace.wav">/files/audio/coins whitespace.wav</a></li>
74+
<li><a href="/files/audio/coins_no_extension">/files/audio/coins_no_extension</a></li>
75+
<li><a href="/files/audio/coins_non_ascii_%D0%B8.wav">/files/audio/coins_non_ascii_и.wav</a></li>
76+
<li><a href="/files/audio/invalid.txt">/files/audio/invalid.txt</a></li>
7377
<li><a href="/files/audio/laser.wav">/files/audio/laser.wav</a></li>
7478
<li><a href="/files/audio/nasa_on_a_mission.mp3">/files/audio/nasa_on_a_mission.mp3</a></li>
7579
<li>

packages/audioplayers/lib/src/audio_cache.dart

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22

3+
import 'package:audioplayers/src/uri_ext.dart';
34
import 'package:file/file.dart';
45
import 'package:file/local.dart';
56
import 'package:flutter/foundation.dart';
@@ -21,7 +22,7 @@ import 'package:path_provider/path_provider.dart';
2122
/// For most normal uses, the static instance is used. But if you want to
2223
/// control multiple caches, you can create your own instances.
2324
class AudioCache {
24-
/// A globlally accessible instance used by default by all players.
25+
/// A globally accessible instance used by default by all players.
2526
static AudioCache instance = AudioCache();
2627

2728
@visibleForTesting
@@ -102,20 +103,35 @@ class AudioCache {
102103
return tryAbsolute!;
103104
}
104105

105-
// local asset
106-
return Uri.parse('assets/$prefix$fileName');
106+
// Relative Asset path
107+
// URL-encode twice, see:
108+
// https://github.com/flutter/engine/blob/2d39e672c95efc6c539d9b48b2cccc65df290cc4/lib/web_ui/lib/ui_web/src/ui_web/asset_manager.dart#L61
109+
// Parsing an already encoded string to an Uri does not encode it a second
110+
// time, so we have to do it manually:
111+
final encoded = UriCoder.encodeOnce(fileName);
112+
return Uri.parse(Uri.encodeFull('assets/$prefix$encoded'));
107113
}
108114

109115
/// Loads a single [fileName] to the cache.
110116
///
111-
/// Also returns a [Future] to access that file.
117+
/// Returns a [Uri] to access that file.
112118
Future<Uri> load(String fileName) async {
113119
if (!loadedFiles.containsKey(fileName)) {
114120
loadedFiles[fileName] = await fetchToMemory(fileName);
115121
}
116122
return loadedFiles[fileName]!;
117123
}
118124

125+
/// Loads a single [fileName] to the cache.
126+
///
127+
/// Returns a decoded [String] to access that file.
128+
Future<String> loadPath(String fileName) async {
129+
final encodedPath = (await load(fileName)).path;
130+
// Web needs an url double-encoded path.
131+
// Darwin needs a decoded path for local files.
132+
return kIsWeb ? encodedPath : Uri.decodeFull(encodedPath);
133+
}
134+
119135
/// Loads a single [fileName] to the cache but returns it as a File.
120136
///
121137
/// Note: this is not available for web, as File doesn't make sense on the
@@ -125,8 +141,9 @@ class AudioCache {
125141
throw 'This method cannot be used on web!';
126142
}
127143
final uri = await load(fileName);
128-
return fileSystem.file(uri.toFilePath(
129-
windows: defaultTargetPlatform == TargetPlatform.windows));
144+
return fileSystem.file(
145+
uri.toFilePath(windows: defaultTargetPlatform == TargetPlatform.windows),
146+
);
130147
}
131148

132149
/// Loads a single [fileName] to the cache but returns it as a list of bytes.

packages/audioplayers/lib/src/audioplayer.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22

33
import 'package:audioplayers/audioplayers.dart';
4+
import 'package:audioplayers/src/uri_ext.dart';
45
import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart';
56
import 'package:flutter/services.dart';
67
import 'package:meta/meta.dart';
@@ -325,8 +326,13 @@ class AudioPlayer {
325326
Future<void> setSourceUrl(String url) async {
326327
_source = UrlSource(url);
327328
await creatingCompleter.future;
329+
// Encode remote url to avoid unexpected failures.
328330
await _completePrepared(
329-
() => _platform.setSourceUrl(playerId, url, isLocal: false),
331+
() => _platform.setSourceUrl(
332+
playerId,
333+
UriCoder.encodeOnce(url),
334+
isLocal: false,
335+
),
330336
);
331337
}
332338

@@ -349,10 +355,10 @@ class AudioPlayer {
349355
/// this method.
350356
Future<void> setSourceAsset(String path) async {
351357
_source = AssetSource(path);
352-
final url = await audioCache.load(path);
358+
final cachePath = await audioCache.loadPath(path);
353359
await creatingCompleter.future;
354360
await _completePrepared(
355-
() => _platform.setSourceUrl(playerId, url.path, isLocal: true),
361+
() => _platform.setSourceUrl(playerId, cachePath, isLocal: true),
356362
);
357363
}
358364

0 commit comments

Comments
 (0)