From 39dc17f9de1f8e2587f00157380f81d763f50963 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:03:49 +0700 Subject: [PATCH] refactor(ffmpeg): remove unused live DASH subsystem Remove the unreachable live streaming and DASH manifest call graph, its state, result model, enum, and exclusive imports. Remove the uncalled availability probe while retaining active file conversion and decryption paths. --- lib/services/ffmpeg_models.dart | 14 - lib/services/ffmpeg_service.dart | 452 ------------------------------- 2 files changed, 466 deletions(-) diff --git a/lib/services/ffmpeg_models.dart b/lib/services/ffmpeg_models.dart index d3dd13df..b905d7e3 100644 --- a/lib/services/ffmpeg_models.dart +++ b/lib/services/ffmpeg_models.dart @@ -1,5 +1,3 @@ -import 'package:ffmpeg_kit_flutter_new_full/ffmpeg_session.dart'; - /// Describes an extension-requested decryption step without coupling callers /// to the FFmpeg execution service. class DownloadDecryptionDescriptor { @@ -116,18 +114,6 @@ class FFmpegResult { }); } -class LiveDecryptedStreamResult { - final String localUrl; - final String format; - final FFmpegSession session; - - LiveDecryptedStreamResult({ - required this.localUrl, - required this.format, - required this.session, - }); -} - /// Result of an EBU R128 loudness scan, used to compute ReplayGain tags. class ReplayGainResult { /// Track gain in dB, e.g. "-6.50 dB". diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index 890d3006..63e15e52 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -3,11 +3,8 @@ import 'dart:io'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:ffmpeg_kit_flutter_new_full/ffmpeg_kit.dart'; -import 'package:ffmpeg_kit_flutter_new_full/ffmpeg_kit_config.dart'; -import 'package:ffmpeg_kit_flutter_new_full/ffmpeg_session.dart'; import 'package:ffmpeg_kit_flutter_new_full/ffprobe_kit.dart'; import 'package:ffmpeg_kit_flutter_new_full/return_code.dart'; -import 'package:ffmpeg_kit_flutter_new_full/session_state.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/services/audio_metadata_mapper.dart'; import 'package:spotiflac_android/services/ffmpeg_models.dart'; @@ -61,21 +58,8 @@ class _ConversionOutputPlan { class FFmpegService { static const int _commandLogPreviewLength = 300; static const int _audioProbeCacheMaxEntries = 64; - static const Duration _liveTunnelStartupTimeout = Duration(seconds: 8); - static const Duration _liveTunnelStartupPollInterval = Duration( - milliseconds: 200, - ); - static const Duration _liveTunnelStabilizationDelay = Duration( - milliseconds: 900, - ); static const String _genericMovKeyDecryptionStrategy = 'ffmpeg.mov_key'; static int _tempEmbedCounter = 0; - static FFmpegSession? _activeLiveDecryptSession; - static String? _activeLiveDecryptUrl; - static String? _activeLiveTempInputPath; - static String? _activeNativeDashManifestPath; - static String? _activeNativeDashManifestUrl; - static final Set _preparedNativeDashManifestPaths = {}; static final Map _audioProbeCache = {}; @@ -992,440 +976,6 @@ class FFmpegService { } } - static bool isActiveLiveDecryptedUrl(String url) { - final active = _activeLiveDecryptUrl; - if (active == null || active.isEmpty) return false; - return active == url.trim(); - } - - static bool isActiveNativeDashManifestUrl(String url) { - final activeUrl = _activeNativeDashManifestUrl; - if (activeUrl == null || activeUrl.isEmpty) return false; - - final normalized = url.trim(); - if (activeUrl == normalized) return true; - - try { - final activePath = Uri.parse(activeUrl).toFilePath(); - final incomingPath = Uri.parse(normalized).toFilePath(); - return activePath == incomingPath; - } catch (_) { - return false; - } - } - - static Future prepareDashManifestForNativePlayback({ - required String manifestPayload, - bool registerAsActive = true, - }) async { - final rawPayload = manifestPayload.trim(); - if (rawPayload.isEmpty) return null; - - final payload = rawPayload.startsWith('MANIFEST:') - ? rawPayload.substring('MANIFEST:'.length) - : rawPayload; - - final manifestPath = await _writeTempManifestFile(payload); - if (manifestPath == null) { - _log.e('Failed to prepare DASH manifest for native playback'); - return null; - } - - final manifestUrl = Uri.file(manifestPath).toString(); - _preparedNativeDashManifestPaths.add(manifestPath); - if (registerAsActive) { - await activatePreparedNativeDashManifest(manifestUrl); - } - return manifestUrl; - } - - static Future activatePreparedNativeDashManifest(String url) async { - final normalized = url.trim(); - if (normalized.isEmpty) return; - - final manifestPath = _nativeDashManifestPathFromUrl(normalized); - if (manifestPath == null || - !_preparedNativeDashManifestPaths.contains(manifestPath)) { - return; - } - - final previousPath = _activeNativeDashManifestPath; - _activeNativeDashManifestPath = manifestPath; - _activeNativeDashManifestUrl = Uri.file(manifestPath).toString(); - - if (previousPath != null && - previousPath.isNotEmpty && - previousPath != manifestPath) { - _preparedNativeDashManifestPaths.remove(previousPath); - await _deleteNativeDashManifestFile(previousPath); - } - } - - static Future stopNativeDashManifestPlayback() async { - final manifestPath = _activeNativeDashManifestPath; - _activeNativeDashManifestPath = null; - _activeNativeDashManifestUrl = null; - - if (manifestPath == null || manifestPath.isEmpty) return; - _preparedNativeDashManifestPaths.remove(manifestPath); - await _deleteNativeDashManifestFile(manifestPath); - } - - static Future cleanupInactivePreparedNativeDashManifests() async { - final activePath = _activeNativeDashManifestPath; - final stalePaths = _preparedNativeDashManifestPaths - .where((path) => path != activePath) - .toList(growable: false); - - for (final path in stalePaths) { - _preparedNativeDashManifestPaths.remove(path); - await _deleteNativeDashManifestFile(path); - } - } - - static String? _nativeDashManifestPathFromUrl(String url) { - try { - final uri = Uri.parse(url); - if (uri.scheme.toLowerCase() != 'file') { - return null; - } - final path = uri.toFilePath(); - return path.trim().isEmpty ? null : path; - } catch (_) { - return null; - } - } - - static Future _deleteNativeDashManifestFile(String path) async { - try { - final file = File(path); - if (await file.exists()) { - await file.delete(); - } - } catch (_) {} - } - - static Future stopLiveDecryptedStream() async { - final session = _activeLiveDecryptSession; - final tempInputPath = _activeLiveTempInputPath; - _activeLiveDecryptSession = null; - _activeLiveDecryptUrl = null; - _activeLiveTempInputPath = null; - - if (session != null) { - try { - await session.cancel(); - } catch (e) { - final sessionId = session.getSessionId(); - if (sessionId != null) { - try { - await FFmpegKit.cancel(sessionId); - } catch (_) {} - } - _log.w('Failed to stop live decrypt session cleanly: $e'); - } - } - - if (tempInputPath != null && tempInputPath.isNotEmpty) { - try { - final file = File(tempInputPath); - if (await file.exists()) { - await file.delete(); - } - } catch (_) {} - } - } - - static Future startDashLiveStream({ - required String manifestPayload, - String preferredFormat = 'm4a', - }) async { - final rawPayload = manifestPayload.trim(); - if (rawPayload.isEmpty) return null; - - final payload = rawPayload.startsWith('MANIFEST:') - ? rawPayload.substring('MANIFEST:'.length) - : rawPayload; - - final manifestPath = await _writeTempManifestFile(payload); - if (manifestPath == null) { - _log.e('Failed to prepare DASH manifest for live stream'); - return null; - } - - await stopLiveDecryptedStream(); - await stopNativeDashManifestPlayback(); - - final attempts = _buildLiveDashFormatAttempts(preferredFormat); - for (final format in attempts) { - final stream = await _tryStartLiveDashAttempt( - manifestPath: manifestPath, - format: format, - ); - if (stream != null) { - _activeLiveDecryptSession = stream.session; - _activeLiveDecryptUrl = stream.localUrl; - _activeLiveTempInputPath = manifestPath; - return stream; - } - } - - try { - final file = File(manifestPath); - if (await file.exists()) { - await file.delete(); - } - } catch (_) {} - return null; - } - - static Future _writeTempManifestFile(String payload) async { - if (payload.trim().isEmpty) return null; - - Uint8List bytes; - try { - bytes = base64Decode(payload); - } catch (_) { - bytes = Uint8List.fromList(utf8.encode(payload)); - } - - final manifestText = utf8.decode(bytes, allowMalformed: true).trim(); - if (manifestText.isEmpty) return null; - - final tempDir = await getTemporaryDirectory(); - final manifestPath = - '${tempDir.path}${Platform.pathSeparator}dash_${DateTime.now().microsecondsSinceEpoch}.mpd'; - await File(manifestPath).writeAsString(manifestText, flush: true); - return manifestPath; - } - - static List<_LiveDecryptFormat> _buildLiveDashFormatAttempts( - String preferredFormat, - ) { - final normalized = preferredFormat.trim().toLowerCase(); - if (normalized == 'flac') { - return const [_LiveDecryptFormat.flac, _LiveDecryptFormat.m4a]; - } - return const [_LiveDecryptFormat.m4a, _LiveDecryptFormat.flac]; - } - - static Future _awaitLiveTunnelReady(FFmpegSession session) async { - final deadline = DateTime.now().add(_liveTunnelStartupTimeout); - var seenRunning = false; - - while (DateTime.now().isBefore(deadline)) { - final state = await session.getState(); - if (state == SessionState.running) { - seenRunning = true; - break; - } - if (state != SessionState.created) { - return false; - } - await Future.delayed(_liveTunnelStartupPollInterval); - } - - if (!seenRunning) { - return false; - } - - await Future.delayed(_liveTunnelStabilizationDelay); - return (await session.getState()) == SessionState.running; - } - - static Future _tryStartLiveDashAttempt({ - required String manifestPath, - required _LiveDecryptFormat format, - }) async { - final port = await _allocateLoopbackPort(); - final ext = format == _LiveDecryptFormat.flac ? 'flac' : 'm4a'; - final mimeType = format == _LiveDecryptFormat.flac - ? 'audio/flac' - : 'audio/mp4'; - final localUrl = 'http://localhost:$port/stream.$ext'; - - final commandArguments = [ - '-nostdin', - '-hide_banner', - '-loglevel', - 'error', - '-protocol_whitelist', - 'file,http,https,tcp,tls,crypto,data', - '-i', - manifestPath, - '-map', - '0:a:0', - '-c:a', - 'copy', - if (format == _LiveDecryptFormat.flac) ...['-f', 'flac'], - if (format == _LiveDecryptFormat.m4a) ...[ - '-movflags', - '+frag_keyframe+empty_moov+default_base_moof', - '-f', - 'mp4', - ], - '-content_type', - mimeType, - '-listen', - '1', - localUrl, - ]; - - _log.d( - 'Starting DASH tunnel: ${_previewCommandForLog(commandArguments.join(' '))}', - ); - - final session = await FFmpegKit.executeWithArgumentsAsync(commandArguments); - final isReady = await _awaitLiveTunnelReady(session); - if (isReady) { - return LiveDecryptedStreamResult( - localUrl: localUrl, - format: ext, - session: session, - ); - } - - final state = await session.getState(); - final output = (await session.getOutput() ?? '').trim(); - if (output.isNotEmpty) { - _log.w('DASH tunnel failed ($ext): $output'); - } else { - _log.w('DASH tunnel failed ($ext) with session state: $state'); - } - - try { - await session.cancel(); - } catch (_) {} - return null; - } - - static Future startEncryptedLiveDecryptedStream({ - required String encryptedStreamUrl, - required String decryptionKey, - String preferredFormat = 'flac', - }) async { - final inputUrl = encryptedStreamUrl.trim(); - if (inputUrl.isEmpty) return null; - - final keyCandidates = _buildDecryptionKeyCandidates(decryptionKey); - if (keyCandidates.isEmpty) { - _log.e('No usable decryption key candidates for live stream'); - return null; - } - - await stopLiveDecryptedStream(); - - final attempts = _buildLiveDecryptFormatAttempts(preferredFormat); - for (final format in attempts) { - for (final keyCandidate in keyCandidates) { - final stream = await _tryStartLiveDecryptAttempt( - inputUrl: inputUrl, - decryptionKey: keyCandidate, - format: format, - ); - if (stream != null) { - _activeLiveDecryptSession = stream.session; - _activeLiveDecryptUrl = stream.localUrl; - _activeLiveTempInputPath = null; - return stream; - } - } - } - - return null; - } - - static List<_LiveDecryptFormat> _buildLiveDecryptFormatAttempts( - String preferredFormat, - ) { - final normalized = preferredFormat.trim().toLowerCase(); - if (normalized == 'm4a' || normalized == 'mp4' || normalized == 'aac') { - return const [_LiveDecryptFormat.m4a, _LiveDecryptFormat.flac]; - } - return const [_LiveDecryptFormat.flac, _LiveDecryptFormat.m4a]; - } - - static Future _tryStartLiveDecryptAttempt({ - required String inputUrl, - required String decryptionKey, - required _LiveDecryptFormat format, - }) async { - final port = await _allocateLoopbackPort(); - final ext = format == _LiveDecryptFormat.flac ? 'flac' : 'm4a'; - final mimeType = format == _LiveDecryptFormat.flac - ? 'audio/flac' - : 'audio/mp4'; - final localUrl = 'http://localhost:$port/stream.$ext'; - - final commandArguments = [ - '-nostdin', - '-hide_banner', - '-loglevel', - 'error', - '-decryption_key', - decryptionKey, - '-i', - inputUrl, - '-map', - '0:a:0', - '-c:a', - 'copy', - if (format == _LiveDecryptFormat.flac) ...['-f', 'flac'], - if (format == _LiveDecryptFormat.m4a) ...[ - '-movflags', - '+frag_keyframe+empty_moov+default_base_moof', - '-f', - 'mp4', - ], - '-content_type', - mimeType, - '-listen', - '1', - localUrl, - ]; - - _log.d('Starting live decrypt tunnel (format=$ext)'); - - final session = await FFmpegKit.executeWithArgumentsAsync(commandArguments); - final isReady = await _awaitLiveTunnelReady(session); - if (isReady) { - return LiveDecryptedStreamResult( - localUrl: localUrl, - format: ext, - session: session, - ); - } - - final state = await session.getState(); - final output = (await session.getOutput() ?? '').trim(); - if (output.isNotEmpty) { - _log.w('Live decrypt attempt failed ($ext): $output'); - } else { - _log.w('Live decrypt attempt failed ($ext) with session state: $state'); - } - - try { - await session.cancel(); - } catch (_) {} - return null; - } - - static Future _allocateLoopbackPort() async { - final socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); - final port = socket.port; - await socket.close(); - return port; - } - - static Future isAvailable() async { - try { - final version = await FFmpegKitConfig.getFFmpegVersion(); - return version?.isNotEmpty ?? false; - } catch (e) { - return false; - } - } - /// Scan an audio file for EBU R128 loudness and compute ReplayGain values. /// /// Uses the FFmpeg `ebur128` audio filter to measure integrated loudness (LUFS) @@ -2818,5 +2368,3 @@ class FFmpegService { return '${hours.toString().padLeft(2, '0')}:${mins.toInt().toString().padLeft(2, '0')}:${secs.toStringAsFixed(3).padLeft(6, '0')}'; } } - -enum _LiveDecryptFormat { flac, m4a }