From f4ddd33ecb3e40c26ec920e7aeeebd48c1e60ae8 Mon Sep 17 00:00:00 2001 From: James Harvey <44349936+jmshrv@users.noreply.github.com> Date: Wed, 31 Jul 2024 00:28:10 +0100 Subject: [PATCH] Initial (not working) ffmpeg experiment --- android/app/build.gradle | 2 +- lib/main.dart | 1 + lib/models/ffmpeg_audio_source.dart | 51 +++++++++++++ lib/services/queue_service.dart | 111 +++++++++++++++++----------- pubspec.lock | 16 ++++ pubspec.yaml | 1 + 6 files changed, 137 insertions(+), 45 deletions(-) create mode 100644 lib/models/ffmpeg_audio_source.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 99bf0a8e6..3a7a9f9b8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -41,7 +41,7 @@ android { defaultConfig { applicationId "com.unicornsonlsd.finamp" - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/main.dart b/lib/main.dart index b44ca92b7..46bad0f53 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -213,6 +213,7 @@ Future setupHive() async { Hive.registerAdapter(LyricLineAdapter()); Hive.registerAdapter(LyricDtoAdapter()); Hive.registerAdapter(LyricsAlignmentAdapter()); + Hive.registerAdapter(LyricsFontSizeAdapter()); final dir = (Platform.isAndroid || Platform.isIOS) ? await getApplicationDocumentsDirectory() diff --git a/lib/models/ffmpeg_audio_source.dart b/lib/models/ffmpeg_audio_source.dart new file mode 100644 index 000000000..16ac811a8 --- /dev/null +++ b/lib/models/ffmpeg_audio_source.dart @@ -0,0 +1,51 @@ +import 'dart:io'; + +import 'package:ffmpeg_kit_flutter/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter/ffmpeg_kit_config.dart'; +import 'package:ffmpeg_kit_flutter/ffmpeg_session.dart'; +import 'package:just_audio/just_audio.dart'; + +class FFmpegAudioSource extends StreamAudioSource { + final Uri _uri; + + File? _pipe; + FFmpegSession? _session; + + FFmpegAudioSource(Uri uri, {super.tag}) : _uri = uri; + + Future createSession() async { + final newPipe = await FFmpegKitConfig.registerNewFFmpegPipe(); + + if (newPipe == null) { + throw "Pipe is null!"; + } + + _pipe = File(newPipe); + + return await FFmpegKit.executeAsync( + "-i $_uri -map 0:a -f wav -y $newPipe", + null, + (log) => print(log.getMessage()), + ); + } + + @override + Future request([int? start, int? end]) async { + _session ??= await createSession(); + + final stat = await _pipe!.stat(); + final size = stat.size; + + start ??= 0; + end ??= size; + + print("FD size: $size"); + + return StreamAudioResponse( + sourceLength: size, + contentLength: end - start, + offset: start, + stream: _pipe!.openRead(start, end), + contentType: "audio/vnd.wave"); + } +} diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 80b8488f0..efc7c9c3c 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -6,6 +6,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:collection/collection.dart'; import 'package:finamp/components/global_snackbar.dart'; import 'package:finamp/gen/assets.gen.dart'; +import 'package:finamp/models/ffmpeg_audio_source.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -27,10 +28,9 @@ import 'music_player_background_task.dart'; /// A track queueing service for Finamp. class QueueService { - /// Used to build content:// URIs that are handled by Finamp's built-in content provider. static final contentProviderPackageName = "com.unicornsonlsd.finamp"; - + final _jellyfinApiHelper = GetIt.instance(); final _audioHandler = GetIt.instance(); final _finampUserHelper = GetIt.instance(); @@ -488,8 +488,8 @@ class QueueService { for (int i = 0; i < itemList.length; i++) { jellyfin_models.BaseItemDto item = itemList[i]; try { - MediaItem mediaItem = - await generateMediaItem(item, contextNormalizationGain: source.contextNormalizationGain); + MediaItem mediaItem = await generateMediaItem(item, + contextNormalizationGain: source.contextNormalizationGain); newItems.add(FinampQueueItem( item: mediaItem, source: source, @@ -583,21 +583,22 @@ class QueueService { required List items, QueueItemSource? source, }) async { - if (_queueAudioSource.length == 0) { return _replaceWholeQueue( itemList: items, - source: source ?? QueueItemSource( - type: QueueItemSourceType.queue, - name: const QueueItemSourceName(type: QueueItemSourceNameType.queue), - id: "queue", - item: null, - ), + source: source ?? + QueueItemSource( + type: QueueItemSourceType.queue, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.queue), + id: "queue", + item: null, + ), initialIndex: 0, beginPlaying: false, ); } - + try { if (_savedQueueState == SavedQueueState.pendingSave) { _savedQueueState = SavedQueueState.saving; @@ -605,8 +606,8 @@ class QueueService { List queueItems = []; for (final item in items) { queueItems.add(FinampQueueItem( - item: - await generateMediaItem(item, contextNormalizationGain: source?.contextNormalizationGain), + item: await generateMediaItem(item, + contextNormalizationGain: source?.contextNormalizationGain), source: source ?? _order.originalSource, type: QueueItemQueueType.queue, )); @@ -631,16 +632,17 @@ class QueueService { required List items, QueueItemSource? source, }) async { - if (_queueAudioSource.length == 0) { return _replaceWholeQueue( itemList: items, - source: source ?? QueueItemSource( - type: QueueItemSourceType.queue, - name: const QueueItemSourceName(type: QueueItemSourceNameType.queue), - id: "queue", - item: null, - ), + source: source ?? + QueueItemSource( + type: QueueItemSourceType.queue, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.queue), + id: "queue", + item: null, + ), initialIndex: 0, beginPlaying: false, ); @@ -653,8 +655,8 @@ class QueueService { List queueItems = []; for (final item in items) { queueItems.add(FinampQueueItem( - item: - await generateMediaItem(item, contextNormalizationGain: source?.contextNormalizationGain), + item: await generateMediaItem(item, + contextNormalizationGain: source?.contextNormalizationGain), source: source ?? QueueItemSource( id: "next-up", @@ -687,21 +689,22 @@ class QueueService { required List items, QueueItemSource? source, }) async { - if (_queueAudioSource.length == 0) { return _replaceWholeQueue( itemList: items, - source: source ?? QueueItemSource( - type: QueueItemSourceType.queue, - name: const QueueItemSourceName(type: QueueItemSourceNameType.queue), - id: "queue", - item: null, - ), + source: source ?? + QueueItemSource( + type: QueueItemSourceType.queue, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.queue), + id: "queue", + item: null, + ), initialIndex: 0, beginPlaying: false, ); } - + try { if (_savedQueueState == SavedQueueState.pendingSave) { _savedQueueState = SavedQueueState.saving; @@ -709,8 +712,8 @@ class QueueService { List queueItems = []; for (final item in items) { queueItems.add(FinampQueueItem( - item: - await generateMediaItem(item, contextNormalizationGain: source?.contextNormalizationGain), + item: await generateMediaItem(item, + contextNormalizationGain: source?.contextNormalizationGain), source: source ?? QueueItemSource( id: "next-up", @@ -979,7 +982,9 @@ class QueueService { double? contextNormalizationGain, MediaItemParentType? parentType, String? parentId, - bool Function({ jellyfin_models.BaseItemDto? item, TabContentType? contentType })? isPlayable, + bool Function( + {jellyfin_models.BaseItemDto? item, TabContentType? contentType})? + isPlayable, }) async { const uuid = Uuid(); @@ -1005,9 +1010,11 @@ class QueueService { downloadedSong = downloadsService.getSongDownload(item: item); isDownloaded = downloadedSong != null; } else { - downloadedCollection = await downloadsService.getCollectionInfo(item: item); + downloadedCollection = + await downloadsService.getCollectionInfo(item: item); if (downloadedCollection != null) { - final downloadStatus = downloadsService.getStatus(downloadedCollection, null); + final downloadStatus = + downloadsService.getStatus(downloadedCollection, null); isDownloaded = downloadStatus != DownloadItemStatus.notNeeded; } } @@ -1015,7 +1022,8 @@ class QueueService { try { downloadedImage = downloadsService.getImageDownload(item: item); } catch (e) { - _queueServiceLogger.warning("Couldn't get the offline image for track '${item.name}' because it's not downloaded or missing a blurhash"); + _queueServiceLogger.warning( + "Couldn't get the offline image for track '${item.name}' because it's not downloaded or missing a blurhash"); } Uri? artUri; @@ -1028,12 +1036,15 @@ class QueueService { // try to get image file (Android Automotive needs this) if (artUri != null) { try { - final fileInfo = await AudioService.cacheManager.getFileFromCache(item.id); + final fileInfo = + await AudioService.cacheManager.getFileFromCache(item.id); if (fileInfo != null) { artUri = fileInfo.file.uri; } } catch (e) { - _queueServiceLogger.severe("Error setting new media artwork uri for item: ${item.id} name: ${item.name}", e); + _queueServiceLogger.severe( + "Error setting new media artwork uri for item: ${item.id} name: ${item.name}", + e); } } } @@ -1042,17 +1053,29 @@ class QueueService { if (Platform.isAndroid) { // replace with placeholder art if (artUri == null) { - final applicationSupportDirectory = await getApplicationSupportDirectory(); - artUri = Uri(scheme: "content", host: contentProviderPackageName, path: path_helper.join(applicationSupportDirectory.absolute.path, Assets.images.albumWhite.path)); + final applicationSupportDirectory = + await getApplicationSupportDirectory(); + artUri = Uri( + scheme: "content", + host: contentProviderPackageName, + path: path_helper.join(applicationSupportDirectory.absolute.path, + Assets.images.albumWhite.path)); } else { // store the origin in fragment since it should be unused - artUri = Uri(scheme: "content", host: contentProviderPackageName, path: artUri.path, fragment: ["http", "https"].contains(artUri.scheme) ? artUri.origin : null); + artUri = Uri( + scheme: "content", + host: contentProviderPackageName, + path: artUri.path, + fragment: ["http", "https"].contains(artUri.scheme) + ? artUri.origin + : null); } } return MediaItem( id: itemId?.toString() ?? uuid.v4(), - playable: isItemPlayable, // this dictates whether clicking on an item will try to play it or browse it in media browsers like Android Auto + playable: + isItemPlayable, // this dictates whether clicking on an item will try to play it or browse it in media browsers like Android Auto album: item.album, artist: item.artists?.join(", ") ?? item.albumArtist, artUri: artUri, @@ -1087,7 +1110,7 @@ class QueueService { if (queueItem.item.extras!["shouldTranscode"] == true) { return HlsAudioSource(await _songUri(queueItem.item), tag: queueItem); } else { - return AudioSource.uri(await _songUri(queueItem.item), + return FFmpegAudioSource(await _songUri(queueItem.item), tag: queueItem); } } diff --git a/pubspec.lock b/pubspec.lock index 493878bbf..1241a6507 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -410,6 +410,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + ffmpeg_kit_flutter: + dependency: "direct main" + description: + name: ffmpeg_kit_flutter + sha256: "843aae41823ca94a0988d975b4b6cdc6948744b9b7e2707d81a3a9cd237b0100" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + ffmpeg_kit_flutter_platform_interface: + dependency: transitive + description: + name: ffmpeg_kit_flutter_platform_interface + sha256: addf046ae44e190ad0101b2fde2ad909a3cd08a2a109f6106d2f7048b7abedee + url: "https://pub.dev" + source: hosted + version: "0.2.1" file: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 495becbd1..6c5a6892f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -101,6 +101,7 @@ dependencies: scroll_to_index: ^3.0.1 window_manager: ^0.3.8 url_launcher: ^6.2.6 + ffmpeg_kit_flutter: ^6.0.3 dev_dependencies: flutter_test: