fix DeezerAudioSource and playback on Windows and Linux

This commit is contained in:
Pato05 2023-10-15 16:48:55 +02:00
parent 0667c5a7a2
commit 5ba7e932e3
No known key found for this signature in database
GPG key ID: F53CA394104BA0CB
10 changed files with 226 additions and 136 deletions

View file

@ -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<List<int>> 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<List<int>> decryptionStream(Stream<List<int>> source,
{required int start, required String trackId}) async* {
var dropBytes = start % 2048;
final deezerStart = start - dropBytes;
int counter = deezerStart ~/ chunkSize;
final buffer = List<int>.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<StreamAudioResponse> 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<List<int>>.empty(),
contentType: _cachedContentType!);
}
}
_logger.fine("authorizing...");
if (!await deezerAPI.authorize()) {
_logger.severe("authorization failed! cannot continue!");
throw Exception("Authorization failed!");
}
late final StreamController<List<int>> 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 = <int>[];
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<List<int>>(
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");
}
}

View file

@ -50,6 +50,9 @@ class PlayerHelper {
final _streamInfoSubject = BehaviorSubject<StreamQualityInfo>();
ValueStream<StreamQualityInfo> get streamInfo => _streamInfoSubject.stream;
final _bufferPositionSubject = BehaviorSubject<Duration>();
ValueStream<Duration> 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
@ -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<void> stop() async {
await _saveQueue();
_player.stop();
_eventSub?.cancel();
_audioSessionSub?.cancel();
_eventSubscription?.cancel();
_audioSessionSubscription?.cancel();
_visualizerSubscription?.cancel();
_bufferPositionSubscription?.cancel();
await super.stop();
}

View file

@ -406,9 +406,10 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
late final AnimationController _controller;
late final Animation<double> _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() {
@ -417,8 +418,9 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
_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<PlayPauseButton>
switch (audioHandler.playbackState.value.processingState) {
//Stopped/Error
case AudioProcessingState.error:
case AudioProcessingState.idle:
child = null;
break;
//Loading, connecting, rewinding...

View file

@ -933,7 +933,12 @@ class _SeekBarState extends State<SeekBar> {
children: <Widget>[
ValueListenableBuilder<Duration>(
valueListenable: position,
builder: (context, value, _) => Slider(
builder: (context, value, _) => StreamBuilder<Duration>(
stream: playerHelper.bufferPosition,
builder: (context, snapshot) {
return Slider(
secondaryTrackValue:
parseDuration(snapshot.data ?? Duration.zero),
focusNode: FocusNode(
canRequestFocus: false,
skipTraversal:
@ -951,7 +956,8 @@ class _SeekBarState extends State<SeekBar> {
_seeking = false;
audioHandler.seek(Duration(milliseconds: d.toInt()));
},
)),
);
})),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(

View file

@ -8,6 +8,7 @@
#include <dynamic_color/dynamic_color_plugin.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
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);

View file

@ -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)

View file

@ -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"

View file

@ -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

View file

@ -9,7 +9,7 @@
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <just_audio_windows/just_audio_windows_plugin.h>
#include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
@ -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(

View file

@ -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)