From 1ca402a3f42e0d2454e1713f80c20fc98e2e254d Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Sun, 15 Nov 2020 19:31:22 +1100 Subject: [PATCH] Unit tests. --- .gitignore | 1 + just_audio/pubspec.lock | 165 ++++++- just_audio/pubspec.yaml | 2 + just_audio/test/just_audio_test.dart | 647 ++++++++++++++++++++++++++- 4 files changed, 806 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 8a11d28..c97198a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ GeneratedPluginRegistrant.java GeneratedPluginRegistrant.swift build/ .flutter-plugins +coverage diff --git a/just_audio/pubspec.lock b/just_audio/pubspec.lock index 51e7551..2038a9e 100644 --- a/just_audio/pubspec.lock +++ b/just_audio/pubspec.lock @@ -1,6 +1,27 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "12.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.40.6" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" async: dependency: "direct main" description: @@ -22,6 +43,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0-nullsafety.1" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.2" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "7.1.0" characters: dependency: transitive description: @@ -36,6 +78,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0-nullsafety.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -43,6 +92,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0-nullsafety.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.0" collection: dependency: transitive description: @@ -64,6 +120,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.5" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.9" fake_async: dependency: transitive description: @@ -85,6 +148,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.2.1" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.11" flutter: dependency: "direct main" description: flutter @@ -100,6 +170,13 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" intl: dependency: transitive description: @@ -107,6 +184,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.16.1" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.2" just_audio_platform_interface: dependency: "direct main" description: @@ -121,6 +205,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.4" matcher: dependency: transitive description: @@ -135,6 +226,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0-nullsafety.3" + mockito: + dependency: "direct dev" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.3" + node_interop: + dependency: transitive + description: + name: node_interop + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + node_io: + dependency: transitive + description: + name: node_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" path: dependency: "direct main" description: @@ -177,6 +296,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.4+1" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.2" platform: dependency: transitive description: @@ -185,7 +311,7 @@ packages: source: hosted version: "2.2.1" plugin_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: plugin_platform_interface url: "https://pub.dartlang.org" @@ -198,6 +324,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.13" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" rxdart: dependency: "direct main" description: @@ -210,6 +350,13 @@ packages: description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.8" source_span: dependency: transitive description: @@ -273,6 +420,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0-nullsafety.3" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+15" win32: dependency: transitive description: @@ -287,6 +441,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.2" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" sdks: - dart: ">=2.10.0-110 <2.11.0" + dart: ">=2.10.0 <2.11.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/just_audio/pubspec.yaml b/just_audio/pubspec.yaml index add23a6..a9803b2 100644 --- a/just_audio/pubspec.yaml +++ b/just_audio/pubspec.yaml @@ -22,6 +22,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + mockito: ^4.1.3 + plugin_platform_interface: ^1.0.2 flutter: plugin: diff --git a/just_audio/test/just_audio_test.dart b/just_audio/test/just_audio_test.dart index 14c6a7a..ca22311 100644 --- a/just_audio/test/just_audio_test.dart +++ b/just_audio/test/just_audio_test.dart @@ -1,21 +1,654 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:just_audio_platform_interface/just_audio_platform_interface.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() { - const MethodChannel channel = MethodChannel('just_audio'); + TestWidgetsFlutterBinding.ensureInitialized(); + // We need an actual HttpClient to test the proxy server. + final overrides = MyHttpOverrides(); + HttpOverrides.global = overrides; + HttpOverrides.runWithHttpOverrides(runTests, overrides); +} + +void runTests() { + final mock = MockJustAudio(); + JustAudioPlatform.instance = mock; + final audioSessionChannel = MethodChannel('com.ryanheise.audio_session'); + + void expectDuration(Duration a, Duration b, {int epsilon = 200}) { + expect((a - b).inMilliseconds.abs(), lessThanOrEqualTo(epsilon)); + } + + void expectState({ + AudioPlayer player, + Duration position, + ProcessingState processingState, + bool playing, + }) { + if (position != null) { + expectDuration(player.position, position); + } + if (processingState != null) { + expect(player.processingState, equals(processingState)); + } + if (playing != null) { + expect(player.playing, equals(playing)); + } + } setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '42'; + audioSessionChannel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; }); }); tearDown(() { - channel.setMockMethodCallHandler(null); + audioSessionChannel.setMockMethodCallHandler(null); }); -// test('getPlatformVersion', () async { -// expect(await AudioPlayer.platformVersion, '42'); -// }); + test('init', () async { + final player = AudioPlayer(); + expect(player.processingState, equals(ProcessingState.none)); + expect(player.position, equals(Duration.zero)); + //expect(player.bufferedPosition, equals(Duration.zero)); + expect(player.duration, equals(null)); + expect(player.icyMetadata, equals(null)); + expect(player.currentIndex, equals(null)); + expect(player.androidAudioSessionId, equals(null)); + expect(player.playing, equals(false)); + expect(player.volume, equals(1.0)); + expect(player.speed, equals(1.0)); + expect(player.sequence, equals(null)); + expect(player.hasNext, equals(false)); + expect(player.hasPrevious, equals(false)); + //expect(player.loopMode, equals(LoopMode.off)); + //expect(player.shuffleModeEnabled, equals(false)); + expect(player.automaticallyWaitsToMinimizeStalling, equals(true)); + player.dispose(); + }); + + test('load', () async { + final player = AudioPlayer(); + final duration = await player.setUrl('https://foo.foo/foo.mp3'); + expect(duration, equals(audioSourceDuration)); + expect(player.duration, equals(duration)); + expect(player.processingState, equals(ProcessingState.ready)); + expect(player.position, equals(Duration.zero)); + expect(player.currentIndex, equals(0)); + expect(player.hasNext, equals(false)); + expect(player.hasPrevious, equals(false)); + expect(player.sequence.length, equals(1)); + expect(player.playing, equals(false)); + player.dispose(); + }); + + test('load error', () async { + final player = AudioPlayer(); + var exception; + try { + await player.setUrl('https://foo.foo/404.mp3'); + exception = null; + } catch (e) { + exception = e; + } + expect(exception != null, equals(true)); + try { + await player.setUrl('https://foo.foo/abort.mp3'); + exception = null; + } catch (e) { + exception = e; + } + expect(exception != null, equals(true)); + try { + await player.setUrl('https://foo.foo/error.mp3'); + exception = null; + } catch (e) { + exception = e; + } + expect(exception != null, equals(true)); + player.dispose(); + }); + + test('control', () async { + final player = AudioPlayer(); + final duration = await player.setUrl('https://foo.foo/foo.mp3'); + final point1 = duration * 0.3; + final stopwatch = Stopwatch(); + expectState( + player: player, + position: Duration.zero, + processingState: ProcessingState.ready, + playing: false, + ); + await player.seek(point1); + expectState( + player: player, + position: point1, + processingState: ProcessingState.ready, + playing: false, + ); + player.play(); + expectState( + player: player, + position: point1, + processingState: ProcessingState.ready, + ); + await Future.delayed(Duration(milliseconds: 100)); + expectState(player: player, playing: true); + await Future.delayed(Duration(seconds: 1)); + expectState( + player: player, + position: point1 + Duration(seconds: 1), + processingState: ProcessingState.ready, + playing: true, + ); + await player.seek(duration - Duration(seconds: 3)); + expectState( + player: player, + position: duration - Duration(seconds: 3), + processingState: ProcessingState.ready, + playing: true, + ); + await player.pause(); + expectState( + player: player, + position: duration - Duration(seconds: 3), + processingState: ProcessingState.ready, + playing: false, + ); + stopwatch.reset(); + stopwatch.start(); + final playFuture = player.play(); + expectState( + player: player, + position: duration - Duration(seconds: 3), + processingState: ProcessingState.ready, + ); + await Future.delayed(Duration(milliseconds: 100)); + expectState(player: player, playing: true); + await playFuture; + expectDuration(stopwatch.elapsed, Duration(seconds: 3)); + expectState( + player: player, + position: duration, + processingState: ProcessingState.completed, + playing: true, + ); + player.dispose(); + }); + + test('speed', () async { + final player = AudioPlayer(); + final duration = await player.setUrl('https://foo.foo/foo.mp3'); + final period1 = Duration(seconds: 2); + final period2 = Duration(seconds: 2); + final speed1 = 0.75; + final speed2 = 1.5; + final position1 = period1 * speed1; + final position2 = position1 + period2 * speed2; + expectState(player: player, position: Duration.zero); + await player.setSpeed(speed1); + player.play(); + await Future.delayed(period1); + expectState(player: player, position: position1); + await player.setSpeed(speed2); + await Future.delayed(period2); + expectState(player: player, position: position2); + player.dispose(); + }); + + test('positionStream', () async { + final player = AudioPlayer(); + final duration = await player.setUrl('https://foo.foo/foo.mp3'); + final period = Duration(seconds: 3); + final position1 = period; + final position2 = position1 + period; + double speed1 = 0.75; + final speed2 = 1.5; + final stepDuration = period ~/ 5; + var target = stepDuration; + player.setSpeed(speed1); + player.play(); + final stopwatch = Stopwatch(); + stopwatch.start(); + await for (var position in player.positionStream) { + if (position >= position1) { + break; + } else if (position >= target) { + expectDuration(position, stopwatch.elapsed * speed1); + target += stepDuration; + } + } + player.setSpeed(speed2); + stopwatch.reset(); + target = position1 + target; + await for (var position in player.positionStream) { + if (position >= position2) { + break; + } else if (position >= target) { + expectDuration(position, position1 + stopwatch.elapsed * speed2); + target += stepDuration; + } + } + player.dispose(); + }); + + test('icyMetadata', () async { + final player = AudioPlayer(); + expect(player.icyMetadata, equals(null)); + final duration = await player.setUrl('https://foo.foo/foo.mp3'); + player.play(); + expect(player.icyMetadata.headers.genre, equals(icyMetadata.headers.genre)); + expect((await player.icyMetadataStream.first).headers.genre, + equals(icyMetadata.headers.genre)); + player.dispose(); + }); + + test('proxy', () async { + final server = MockWebServer(); + await server.start(); + final player = AudioPlayer(); + // This simulates an actual URL + final uri = Uri.parse( + 'http://${InternetAddress.loopbackIPv4.address}:${server.port}/proxy/foo.mp3'); + await player.setUrl('$uri', headers: {'custom-header': 'Hello'}); + // Obtain the proxy URL that the platform side should use to load the data. + final proxyUri = Uri.parse(player.icyMetadata.info.url); + // Simulate the platform side requesting the data. + final request = await HttpClient().getUrl(proxyUri); + final response = await request.close(); + final responseText = await response.transform(utf8.decoder).join(); + expect(response.statusCode, equals(HttpStatus.ok)); + expect(responseText, equals('Hello')); + expect(response.headers.value(HttpHeaders.contentTypeHeader), + equals('audio/mock')); + await server.stop(); + }); + + test('proxy0.9', () async { + final server = MockWebServer(); + await server.start(); + final player = AudioPlayer(); + // This simulates an actual URL + final uri = Uri.parse( + 'http://${InternetAddress.loopbackIPv4.address}:${server.port}/proxy0.9/foo.mp3'); + await player.setUrl('$uri', headers: {'custom-header': 'Hello'}); + // Obtain the proxy URL that the platform side should use to load the data. + final proxyUri = Uri.parse(player.icyMetadata.info.url); + // Simulate the platform side requesting the data. + final socket = await Socket.connect(proxyUri.host, proxyUri.port); + //final socket = await Socket.connect(uri.host, uri.port); + socket.write('GET ${uri.path} HTTP/1.1\n' 'test-header: value\n' '\n'); + await socket.flush(); + final responseText = await socket + .transform(Converter.castFrom, String, Uint8List, dynamic>( + utf8.decoder)) + .join(); + await socket.close(); + expect(responseText, equals('Hello')); + await server.stop(); + }); + + test('sequence', () async { + final source1 = ConcatenatingAudioSource(children: [ + LoopingAudioSource( + count: 2, + child: ClippingAudioSource( + start: Duration(seconds: 60), + end: Duration(seconds: 65), + child: AudioSource.uri(Uri.parse("https://foo.foo/foo.mp3")), + tag: 'a', + ), + ), + AudioSource.uri( + Uri.parse("https://bar.bar/bar.mp3"), + tag: 'b', + ), + AudioSource.uri( + Uri.parse("https://baz.baz/baz.mp3"), + tag: 'c', + ), + ]); + expect(source1.sequence.map((s) => s.tag as String).toList(), + equals(['a', 'a', 'b', 'c'])); + final source2 = ConcatenatingAudioSource(children: []); + final player = AudioPlayer(); + await player.load(source2); + expect(source2.sequence.length, equals(0)); + await source2 + .add(AudioSource.uri(Uri.parse('https://b.b/b.mp3'), tag: 'b')); + await source2.insert( + 0, AudioSource.uri(Uri.parse('https://a.a/a.mp3'), tag: 'a')); + await source2.insert( + 2, AudioSource.uri(Uri.parse('https://c.c/c.mp3'), tag: 'c')); + await source2.addAll([ + AudioSource.uri(Uri.parse('https://d.d/d.mp3'), tag: 'd'), + AudioSource.uri(Uri.parse('https://e.e/e.mp3'), tag: 'e'), + ]); + await source2.insertAll(3, [ + AudioSource.uri(Uri.parse('https://e.e/e.mp3'), tag: 'e'), + AudioSource.uri(Uri.parse('https://f.f/f.mp3'), tag: 'f'), + ]); + expect(source2.sequence.map((s) => s.tag as String), + equals(['a', 'b', 'c', 'e', 'f', 'd', 'e'])); + await source2.removeAt(0); + expect(source2.sequence.map((s) => s.tag as String), + equals(['b', 'c', 'e', 'f', 'd', 'e'])); + await source2.move(3, 2); + expect(source2.sequence.map((s) => s.tag as String), + equals(['b', 'c', 'f', 'e', 'd', 'e'])); + await source2.move(2, 3); + expect(source2.sequence.map((s) => s.tag as String), + equals(['b', 'c', 'e', 'f', 'd', 'e'])); + await source2.removeRange(0, 2); + expect(source2.sequence.map((s) => s.tag as String), + equals(['e', 'f', 'd', 'e'])); + await source2.removeAt(3); + expect( + source2.sequence.map((s) => s.tag as String), equals(['e', 'f', 'd'])); + await source2.removeRange(1, 3); + expect(source2.sequence.map((s) => s.tag as String), equals(['e'])); + await source2.clear(); + expect(source2.sequence.map((s) => s.tag as String), equals([])); + }); + + test('detect', () async { + expect(AudioSource.uri(Uri.parse('https://a.a/a.mpd')) is DashAudioSource, + equals(true)); + expect(AudioSource.uri(Uri.parse('https://a.a/a.m3u8')) is HlsAudioSource, + equals(true)); + expect( + AudioSource.uri(Uri.parse('https://a.a/a.mp3')) + is ProgressiveAudioSource, + equals(true)); + expect(AudioSource.uri(Uri.parse('https://a.a/a#.mpd')) is DashAudioSource, + equals(true)); + }); } + +class MockJustAudio extends Mock + with MockPlatformInterfaceMixin + implements JustAudioPlatform { + final _players = {}; + + @override + Future init(InitRequest request) async { + final player = MockAudioPlayer(request.id); + _players[request.id] = player; + return player; + } + + @override + Future disposePlayer( + DisposePlayerRequest request) async { + _players[request.id].dispose(DisposeRequest()); + return DisposePlayerResponse(); + } +} + +const audioSourceDuration = Duration(minutes: 2); + +final icyMetadata = IcyMetadata( + headers: IcyHeaders( + url: 'url', + genre: 'Genre', + metadataInterval: 3, + bitrate: 100, + isPublic: true, + name: 'name', + ), + info: IcyInfo( + title: 'title', + url: 'url', + ), +); + +final icyMetadataMessage = IcyMetadataMessage( + headers: IcyHeadersMessage( + url: 'url', + genre: 'Genre', + metadataInterval: 3, + bitrate: 100, + isPublic: true, + name: 'name', + ), + info: IcyInfoMessage( + title: 'title', + url: 'url', + ), +); + +class MockAudioPlayer implements AudioPlayerPlatform { + final String _id; + final eventController = StreamController(); + AudioSourceMessage _audioSource; + ProcessingStateMessage _processingState; + Duration _updatePosition; + DateTime _updateTime; + Duration _duration = audioSourceDuration; + int _currentIndex; + int _index; + var _playing = false; + var _speed = 1.0; + var _volume = 1.0; + var _loopMode = LoopModeMessage.off; + var _shuffleModeEnabled = false; + Completer _playCompleter; + Timer _playTimer; + + MockAudioPlayer(String id) : this._id = id; + + @override + String get id => _id; + + @override + Stream get playbackEventMessageStream => + eventController.stream; + + @override + Future load(LoadRequest request) async { + final audioSource = request.audioSourceMessage; + if (audioSource is UriAudioSourceMessage) { + if (audioSource.uri.contains('abort')) { + throw PlatformException(code: 'abort', message: 'Failed to load URL'); + } else if (audioSource.uri.contains('404')) { + throw PlatformException(code: '404', message: 'Not found'); + } else if (audioSource.uri.contains('error')) { + throw PlatformException(code: 'error', message: 'Unknown error'); + } + } + _audioSource = audioSource; + _index = request.initialIndex ?? 0; + _setPosition(request.initialPosition ?? Duration.zero); + _processingState = ProcessingStateMessage.ready; + _broadcastPlaybackEvent(); + return LoadResponse(duration: _duration); + } + + @override + Future play(PlayRequest request) async { + if (_playing) return PlayResponse(); + _playing = true; + _playTimer = Timer(_remaining, () { + _setPosition(_position); + _processingState = ProcessingStateMessage.completed; + _broadcastPlaybackEvent(); + _playCompleter?.complete(); + }); + _playCompleter = Completer(); + _broadcastPlaybackEvent(); + await _playCompleter.future; + return PlayResponse(); + } + + @override + Future pause(PauseRequest request) async { + if (!_playing) return PauseResponse(); + _playing = false; + _playTimer?.cancel(); + _playCompleter?.complete(); + _setPosition(_position); + _broadcastPlaybackEvent(); + return PauseResponse(); + } + + @override + Future seek(SeekRequest request) async { + _setPosition(request.position); + _index = request.index; + _broadcastPlaybackEvent(); + return SeekResponse(); + } + + @override + Future setAndroidAudioAttributes( + SetAndroidAudioAttributesRequest request) async { + return SetAndroidAudioAttributesResponse(); + } + + @override + Future + setAutomaticallyWaitsToMinimizeStalling( + SetAutomaticallyWaitsToMinimizeStallingRequest request) async { + return SetAutomaticallyWaitsToMinimizeStallingResponse(); + } + + @override + Future setLoopMode(SetLoopModeRequest request) async { + _loopMode = request.loopMode; + return SetLoopModeResponse(); + } + + @override + Future setShuffleMode( + SetShuffleModeRequest request) async { + _shuffleModeEnabled = request.shuffleMode == ShuffleModeMessage.all; + return SetShuffleModeResponse(); + } + + @override + Future setSpeed(SetSpeedRequest request) async { + _speed = request.speed; + _setPosition(_position); + return SetSpeedResponse(); + } + + @override + Future setVolume(SetVolumeRequest request) async { + _volume = request.volume; + return SetVolumeResponse(); + } + + @override + Future dispose(DisposeRequest request) async { + return DisposeResponse(); + } + + @override + Future concatenatingInsertAll( + ConcatenatingInsertAllRequest request) async { + // TODO + return ConcatenatingInsertAllResponse(); + } + + @override + Future concatenatingMove( + ConcatenatingMoveRequest request) async { + // TODO + return ConcatenatingMoveResponse(); + } + + @override + Future concatenatingRemoveRange( + ConcatenatingRemoveRangeRequest request) async { + // TODO + return ConcatenatingRemoveRangeResponse(); + } + + _broadcastPlaybackEvent() { + String url; + if (_audioSource is UriAudioSourceMessage) { + // Not sure why this cast is necessary... + url = (_audioSource as UriAudioSourceMessage).uri.toString(); + } + eventController.add(PlaybackEventMessage( + processingState: _processingState, + updatePosition: _updatePosition, + updateTime: _updateTime, + bufferedPosition: _position ?? Duration.zero, + icyMetadata: IcyMetadataMessage( + headers: IcyHeadersMessage( + url: url, + genre: 'Genre', + metadataInterval: 3, + bitrate: 100, + isPublic: true, + name: 'name', + ), + info: IcyInfoMessage( + title: 'title', + url: url, + ), + ), + duration: _duration, + currentIndex: _index, + androidAudioSessionId: null, + )); + } + + Duration get _position { + if (_playing && _processingState == ProcessingStateMessage.ready) { + final result = + _updatePosition + (DateTime.now().difference(_updateTime)) * _speed; + return _duration == null || result <= _duration ? result : _duration; + } else { + return _updatePosition; + } + } + + Duration get _remaining => (_duration - _position) * (1 / _speed); + + void _setPosition(Duration position) { + _updatePosition = position; + _updateTime = DateTime.now(); + } +} + +class MockWebServer { + HttpServer _server; + int get port => _server.port; + + Future start() async { + _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + _server.listen((request) async { + final response = request.response; + final body = utf8.encode('${request.headers.value("custom-header")}'); + if (request.uri.path == '/proxy0.9/foo.mp3') { + final clientSocket = + await request.response.detachSocket(writeHeaders: false); + clientSocket.add(body); + await clientSocket.flush(); + await clientSocket.close(); + } else { + response.contentLength = body.length; + response.statusCode = HttpStatus.ok; + response.headers.set(HttpHeaders.contentTypeHeader, 'audio/mock'); + response.add(body); + await response.flush(); + await response.close(); + } + }); + } + + Future stop() => _server.close(); +} + +class MyHttpOverrides extends HttpOverrides {}