diff --git a/lib/api/deezer_audio_source.dart b/lib/api/deezer_audio_source.dart index fca7181..ff60e64 100644 --- a/lib/api/deezer_audio_source.dart +++ b/lib/api/deezer_audio_source.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'dart:typed_data'; import 'package:encrypt/encrypt.dart'; +import 'package:flutter/foundation.dart' as flutter; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/settings.dart'; @@ -13,6 +15,13 @@ import 'package:http/http.dart' as http; import 'package:dart_blowfish/dart_blowfish.dart'; import 'package:logging/logging.dart'; +typedef _IsolateMessage = ( + Stream> source, + int start, + String trackId, + SendPort sendPort +); + // Maybe better implementation of Blowfish CBC instead of random-ass, unpublished library from github? // This class can be considered a rewrite in Dart of the Java backend (from the StreamServer.deezer() function and also from the Deezer class) class DeezerAudioSource extends StreamAudioSource { @@ -25,6 +34,10 @@ class DeezerAudioSource extends StreamAudioSource { late String _mediaVersion; final StreamInfoCallback? onStreamObtained; + // some cache + int? _cachedSourceLength; + String? _cachedContentType; + DeezerAudioSource({ required AudioQuality quality, required String trackId, @@ -201,18 +214,69 @@ class DeezerAudioSource extends StreamAudioSource { return decrypted; } + static Stream> decryptionStream(Stream> source, + {required int start, required String trackId}) async* { + var dropBytes = start % 2048; + final deezerStart = start - dropBytes; + int counter = deezerStart ~/ chunkSize; + final buffer = List.empty(growable: true); + final key = await flutter.compute(getKey, trackId); + + await for (var bytes in source) { + if (dropBytes > 0) { + bytes = bytes.sublist(dropBytes); + } + + buffer.addAll(bytes); + + int i; + for (i = 0; i < buffer.length; i += chunkSize) { + if (buffer.length <= i + chunkSize) { + break; + } + + bytes = buffer.sublist(i, i + chunkSize); + + if ((counter % 3) == 0) { + bytes = decryptChunk(key, bytes); + } + + counter++; + yield bytes; + } + + if (i < buffer.length) { + buffer.removeRange(0, i); + } else { + buffer.clear(); + } + } + + // add remaining items in buffer + if (buffer.isNotEmpty) yield buffer; + } + @override Future request([int? start, int? end]) async { start ??= 0; + if (_cachedSourceLength != null) { + if (start == _cachedSourceLength) { + return StreamAudioResponse( + sourceLength: _cachedSourceLength, + contentLength: 0, + offset: start, + stream: const Stream>.empty(), + contentType: _cachedContentType!); + } + } + _logger.fine("authorizing..."); if (!await deezerAPI.authorize()) { _logger.severe("authorization failed! cannot continue!"); throw Exception("Authorization failed!"); } - late final StreamController> controller; - final Uri uri; try { uri = await _fallbackUrl(); @@ -243,73 +307,24 @@ class DeezerAudioSource extends StreamAudioSource { if (rc != HttpStatus.ok && rc != HttpStatus.partialContent) { throw Exception(await res.stream.bytesToString()); } - int counter = deezerStart ~/ chunkSize; + int dropBytes = start % chunkSize; - _logger.finest( - "deezerStart: $deezerStart (actual start: $start), end: $end, counter: $counter, dropBytes: $dropBytes"); - var buffer = []; - final key = getKey(trackId); - - int total = 0; - - final subscription = res.stream.listen( - (bytes) { - // _logger.finest( - // "got stream bytes (${bytes.length}) with buffer.length = ${buffer.length}"); - total += bytes.length; - buffer.addAll(bytes); - - int i; - for (i = 0; i < buffer.length; i += chunkSize) { - if (buffer.length <= i + chunkSize) { - break; - } - - bytes = buffer.sublist(i, i + chunkSize); - - if ((counter % 3) == 0) { - bytes = decryptChunk(key, bytes); - } - if (dropBytes > 0) { - controller.add(bytes.sublist(dropBytes)); - dropBytes = 0; - counter++; - continue; - } - - counter++; - controller.add(bytes); - } - - buffer.removeRange(0, i); - }, - onDone: () { - total += buffer.length; - _logger.finest( - "onDone() called, remaining buffer ${buffer.length}, total: $total"); - // add remaining items in buffer - if (buffer.isNotEmpty) controller.add(buffer); - controller.close(); - }, - ); - - subscription.pause(); - controller = StreamController>( - onListen: subscription.resume, - onPause: subscription.pause, - onResume: subscription.resume, - onCancel: subscription.cancel, - ); + "deezerStart: $deezerStart (actual start: $start), end: $end, dropBytes: $dropBytes"); + final stream = decryptionStream(res.stream, start: start, trackId: trackId); final cl = res.contentLength! - dropBytes; + if (end == null) { + _cachedSourceLength = cl + start; + } + return StreamAudioResponse( - sourceLength: cl + start, + sourceLength: _cachedSourceLength, contentLength: cl, offset: start, - stream: controller.stream, - contentType: + stream: stream, + contentType: _cachedContentType = quality == AudioQuality.FLAC ? "audio/flac" : "audio/mpeg"); } } diff --git a/lib/api/player.dart b/lib/api/player.dart index 8c65e55..85662ed 100644 --- a/lib/api/player.dart +++ b/lib/api/player.dart @@ -50,6 +50,9 @@ class PlayerHelper { final _streamInfoSubject = BehaviorSubject(); ValueStream get streamInfo => _streamInfoSubject.stream; + final _bufferPositionSubject = BehaviorSubject(); + ValueStream get bufferPosition => _bufferPositionSubject.stream; + /// Find queue index by id /// /// The function gets more expensive the longer the queue is and the further the element is from the beginning. @@ -128,6 +131,9 @@ class PlayerHelper { Logger('PlayerHelper').fine("streamInfo received"); _streamInfoSubject.add(event['data'] as StreamQualityInfo); break; + case 'bufferPosition': + _bufferPositionSubject.add(event['data'] as Duration); + break; } }); _mediaItemSubscription = audioHandler.mediaItem.listen((mediaItem) async { @@ -366,8 +372,9 @@ class AudioPlayerTask extends BaseAudioHandler { int _queueAutoIncrement = 0; //Stream subscriptions - StreamSubscription? _eventSub; - StreamSubscription? _audioSessionSub; + StreamSubscription? _eventSubscription; + StreamSubscription? _bufferPositionSubscription; + StreamSubscription? _audioSessionSubscription; StreamSubscription? _visualizerSubscription; /// Android Auto helper class for navigation @@ -430,7 +437,7 @@ class AudioPlayerTask extends BaseAudioHandler { _box = await Hive.openLazyBox('playback', path: (await getTemporaryDirectory()).path); - + _player = AudioPlayer( handleInterruptions: !initArgs.ignoreInterruptions, androidApplyAudioAttributes: true, @@ -455,7 +462,7 @@ class AudioPlayerTask extends BaseAudioHandler { } }); //Update state on all clients on change - _eventSub = _player.playbackEventStream.listen((event) { + _eventSubscription = _player.playbackEventStream.listen((event) { //Update _broadcastState(); }, onError: (Object e, StackTrace st) { @@ -466,10 +473,8 @@ class AudioPlayerTask extends BaseAudioHandler { case ProcessingState.completed: //Player ended, get more songs if (_queueIndex == queue.value.length - 1) { - customEvent.add({ - 'action': 'queueEnd', - 'queueSource': queueSource!.toJson() - }); + customEvent.add( + {'action': 'queueEnd', 'queueSource': queueSource!.toJson()}); } break; default: @@ -477,8 +482,14 @@ class AudioPlayerTask extends BaseAudioHandler { } }); + _bufferPositionSubscription = + _player.bufferedPositionStream.listen((bufferPosition) { + customEvent.add({'action': 'bufferPosition', 'data': bufferPosition}); + }); + //Audio session - _audioSessionSub = _player.androidAudioSessionIdStream.listen((event) { + _audioSessionSubscription = + _player.androidAudioSessionIdStream.listen((event) { customEvent.add({'action': 'audioSession', 'id': event}); }); @@ -772,7 +783,9 @@ class AudioPlayerTask extends BaseAudioHandler { //Load in just_audio try { await _player.setAudioSource(_audioSource, - initialIndex: _queueIndex, initialPosition: Duration.zero, preload: kIsWeb || defaultTargetPlatform != TargetPlatform.linux); + initialIndex: _queueIndex, + initialPosition: Duration.zero, + preload: kIsWeb || defaultTargetPlatform != TargetPlatform.linux); } catch (e) { //Error loading tracks } @@ -931,9 +944,10 @@ class AudioPlayerTask extends BaseAudioHandler { Future stop() async { await _saveQueue(); _player.stop(); - _eventSub?.cancel(); - _audioSessionSub?.cancel(); + _eventSubscription?.cancel(); + _audioSessionSubscription?.cancel(); _visualizerSubscription?.cancel(); + _bufferPositionSubscription?.cancel(); await super.stop(); } diff --git a/lib/ui/player_bar.dart b/lib/ui/player_bar.dart index 67816d4..70f19f4 100644 --- a/lib/ui/player_bar.dart +++ b/lib/ui/player_bar.dart @@ -406,19 +406,21 @@ class _PlayPauseButtonState extends State late final AnimationController _controller; late final Animation _animation; late StreamSubscription _subscription; - late bool _canPlay = audioHandler.playbackState.value.playing || + late bool _canPlay = audioHandler.playbackState.value.processingState == + AudioProcessingState.ready || audioHandler.playbackState.value.processingState == - AudioProcessingState.ready; + AudioProcessingState.idle; @override void initState() { _controller = AnimationController( - vsync: this, duration: const Duration(milliseconds: 200)); + vsync: this, duration: const Duration(milliseconds: 200)); _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); - + _subscription = audioHandler.playbackState.listen((playbackState) { - if (playbackState.playing || - playbackState.processingState == AudioProcessingState.ready) { + if (playbackState.processingState == AudioProcessingState.ready || + audioHandler.playbackState.value.processingState == + AudioProcessingState.idle) { if (playbackState.playing) { _controller.forward(); } else { @@ -480,7 +482,6 @@ class _PlayPauseButtonState extends State switch (audioHandler.playbackState.value.processingState) { //Stopped/Error case AudioProcessingState.error: - case AudioProcessingState.idle: child = null; break; //Loading, connecting, rewinding... diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index 54456c8..003d0be 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -933,25 +933,31 @@ class _SeekBarState extends State { children: [ ValueListenableBuilder( valueListenable: position, - builder: (context, value, _) => Slider( - focusNode: FocusNode( - canRequestFocus: false, - skipTraversal: - true), // Don't focus on Slider - it doesn't work (and not needed) - value: parseDuration(value), - max: duration.inMilliseconds.toDouble(), - onChangeStart: (double d) { - _seeking = true; - position.value = Duration(milliseconds: d.toInt()); - }, - onChanged: (double d) { - position.value = Duration(milliseconds: d.toInt()); - }, - onChangeEnd: (double d) { - _seeking = false; - audioHandler.seek(Duration(milliseconds: d.toInt())); - }, - )), + builder: (context, value, _) => StreamBuilder( + stream: playerHelper.bufferPosition, + builder: (context, snapshot) { + return Slider( + secondaryTrackValue: + parseDuration(snapshot.data ?? Duration.zero), + focusNode: FocusNode( + canRequestFocus: false, + skipTraversal: + true), // Don't focus on Slider - it doesn't work (and not needed) + value: parseDuration(value), + max: duration.inMilliseconds.toDouble(), + onChangeStart: (double d) { + _seeking = true; + position.value = Duration(milliseconds: d.toInt()); + }, + onChanged: (double d) { + position.value = Duration(milliseconds: d.toInt()); + }, + onChangeEnd: (double d) { + _seeking = false; + audioHandler.seek(Duration(milliseconds: d.toInt())); + }, + ); + })), Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Row( diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index c54dc17..606c5a6 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); + media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 13157ca..f1b77ef 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,10 +5,12 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color isar_flutter_libs + media_kit_libs_linux url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + media_kit_native_event_loop ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.lock b/pubspec.lock index cd360d3..156ac19 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + archive: + dependency: transitive + description: + name: archive + sha256: "7e0d52067d05f2e0324268097ba723b71cb41ac8a6a2b24d1edf9c536b987b03" + url: "https://pub.dev" + source: hosted + version: "3.4.6" args: dependency: transitive description: @@ -363,14 +371,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.5" - eventify: - dependency: transitive - description: - name: eventify - sha256: b829429f08586cc2001c628e7499e3e3c2493a1d895fd73b00ecb23351aa5a66 - url: "https://pub.dev" - source: hosted - version: "1.0.1" fading_edge_scrollview: dependency: transitive description: @@ -663,6 +663,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.0.2" + image: + dependency: transitive + description: + name: image + sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + url: "https://pub.dev" + source: hosted + version: "4.1.3" infinite_listview: dependency: transitive description: @@ -743,14 +751,13 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.35" - just_audio_mpv: + just_audio_media_kit: dependency: "direct main" description: - name: just_audio_mpv - sha256: d6e4e9fd20bfb9d2fd5e3dcd7906c90ed07f83d1d2f44f31204160821ab62fed - url: "https://pub.dev" - source: hosted - version: "0.1.7" + path: "../just_audio_media_kit" + relative: true + source: path + version: "0.0.1" just_audio_platform_interface: dependency: transitive description: @@ -767,14 +774,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.8" - just_audio_windows: - dependency: "direct main" - description: - name: just_audio_windows - sha256: "7b8801f3987e98a2002cd23b5600b2daf162248ff1413266fb44c84448c1c0d3" - url: "https://pub.dev" - source: hosted - version: "0.2.0" lints: dependency: transitive description: @@ -815,6 +814,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + media_kit: + dependency: transitive + description: + name: media_kit + sha256: "1283b500341d41f033478706204a2b4ae2612e9b331c934bc4fad8c4bb869f6d" + url: "https://pub.dev" + source: hosted + version: "1.1.8+2" + media_kit_libs_linux: + dependency: transitive + description: + name: media_kit_libs_linux + sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + media_kit_libs_windows_audio: + dependency: transitive + description: + name: media_kit_libs_windows_audio + sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53 + url: "https://pub.dev" + source: hosted + version: "1.0.9" + media_kit_native_event_loop: + dependency: transitive + description: + name: media_kit_native_event_loop + sha256: a605cf185499d14d58935b8784955a92a4bf0ff4e19a23de3d17a9106303930e + url: "https://pub.dev" + source: hosted + version: "1.0.8" meta: dependency: transitive description: @@ -839,14 +870,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - mpv_dart: - dependency: transitive - description: - name: mpv_dart - sha256: a33bd9a68439b496b7a5f36fecd3aa3cf6cbf0176ae15b9b60b12ae96e58f5a4 - url: "https://pub.dev" - source: hosted - version: "0.0.1" nested: dependency: transitive description: @@ -1143,6 +1166,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + safe_local_storage: + dependency: transitive + description: + name: safe_local_storage + sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440 + url: "https://pub.dev" + source: hosted + version: "1.0.2" scrobblenaut: dependency: "direct main" description: @@ -1365,6 +1396,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + uri_parser: + dependency: transitive + description: + name: uri_parser + sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835" + url: "https://pub.dev" + source: hosted + version: "2.0.2" url_launcher: dependency: "direct main" description: @@ -1534,5 +1581,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.1.3 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index d7d2aa6..14e0d82 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,8 +86,8 @@ dependencies: ref: main logging: ^1.2.0 just_audio: ^0.9.35 - just_audio_mpv: ^0.1.7 - just_audio_windows: ^0.2.0 + just_audio_media_kit: + git: https://github.com/Pato05/just_audio_media_kit.git rxdart: ^0.27.7 flutter_isolate: ^2.0.4 isar: ^3.1.0+1 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5e26b86..1dc0921 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include #include @@ -21,8 +21,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); - JustAudioWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("JustAudioWindowsPlugin")); + MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1232d43..79c3036 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,13 +6,14 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus dynamic_color isar_flutter_libs - just_audio_windows + media_kit_libs_windows_audio permission_handler_windows share_plus url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + media_kit_native_event_loop ) set(PLUGIN_BUNDLED_LIBRARIES)