fix: force native FLAC muxer when decrypting to .flac output

Downloads from providers that stream FLAC inside an fMP4 container (e.g. Amazon Music) were being written to disk with a .flac extension while the payload still carried ISO-BMFF atoms. The container-conversion guard then saw codec=flac and skipped the remux, leaving native FLAC tag writers to fail with 'fLaC head incorrect'.

Force '-f flac' on the decryption command whenever the target extension is .flac so FFmpeg emits a real FLAC stream, and add an 'fLaC' magic-byte probe on both the Dart and Kotlin container-conversion guards so a FLAC-in-MP4 source is remuxed rather than silently passed through as a tag-writer hazard.
This commit is contained in:
zarzet committed 2026-05-10 18:50:49 +07:00
1 parent 4b7146afe4
commit 8f5c59683a
3 files changed
+80 -8

No files matched your search

+12 -4
View File
@@ -6160,9 +6160,13 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
String? flacPath;
try {
final codec = await FFmpegService.probePrimaryAudioCodec(tempPath);
if (!FFmpegService.isLosslessAudioCodec(codec) || codec == 'flac') {
final isAlreadyNativeFlac =
codec == 'flac' && await FFmpegService.isNativeFlacFile(tempPath);
if (!FFmpegService.isLosslessAudioCodec(codec) || isAlreadyNativeFlac) {
_log.d(
'Preserving native container; audio codec is ${codec ?? 'unknown'}, not a lossless source needing FLAC conversion.',
'Preserving native container; audio codec is ${codec ?? 'unknown'}'
'${isAlreadyNativeFlac ? ' (native FLAC)' : ''}, '
'no FLAC container conversion needed.',
);
return filePath;
}
@@ -6203,9 +6207,13 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
}
final codec = await FFmpegService.probePrimaryAudioCodec(filePath);
if (!FFmpegService.isLosslessAudioCodec(codec) || codec == 'flac') {
final isAlreadyNativeFlac =
codec == 'flac' && await FFmpegService.isNativeFlacFile(filePath);
if (!FFmpegService.isLosslessAudioCodec(codec) || isAlreadyNativeFlac) {
_log.d(
'Preserving native container; audio codec is ${codec ?? 'unknown'}, not a lossless source needing FLAC conversion.',
'Preserving native container; audio codec is ${codec ?? 'unknown'}'
'${isAlreadyNativeFlac ? ' (native FLAC)' : ''}, '
'no FLAC container conversion needed.',
);
return filePath;
}
+32 -1
View File
@@ -283,6 +283,28 @@ class FFmpegService {
}.contains(normalized);
}
/// Returns `true` when [filePath] starts with the native FLAC magic bytes
/// (`fLaC`). Useful to distinguish a real FLAC file from a FLAC-in-MP4
/// container that carries a `.flac` extension or claims codec=flac.
static Future<bool> isNativeFlacFile(String filePath) async {
try {
final raf = await File(filePath).open();
try {
final header = await raf.read(4);
return header.length == 4 &&
header[0] == 0x66 && // 'f'
header[1] == 0x4C && // 'L'
header[2] == 0x61 && // 'a'
header[3] == 0x43; // 'C'
} finally {
await raf.close();
}
} catch (e) {
_log.w('Native FLAC magic probe failed for $filePath: $e');
return false;
}
}
static Future<String?> convertM4aToFlac(String inputPath) async {
final outputPath = _buildOutputPath(inputPath, '.flac');
@@ -435,7 +457,16 @@ class FFmpegService {
// Force MOV demuxer: -decryption_key is only supported by the MOV/MP4
// demuxer. The input may carry a .flac extension (SAF mode) while actually
// containing an encrypted M4A stream, so we must override auto-detection.
return '-v error -decryption_key "$key" -f $demuxerFormat -i "$inputPath" $audioMap-c copy "$outputPath" -y';
//
// When the requested output is a native .flac we also force the flac
// muxer (-f flac). Without it, FFmpeg infers the muxer from the output
// extension AND keeps the input container's stream layout, which for
// FLAC-in-MP4 sources would still emit an ISO-BMFF payload under a
// .flac filename. That file fails native FLAC tag writers later on.
final muxerOverride = outputPath.toLowerCase().endsWith('.flac')
? '-f flac '
: '';
return '-v error -decryption_key "$key" -f $demuxerFormat -i "$inputPath" $audioMap-c copy $muxerOverride"$outputPath" -y';
}
final keyCandidates = _buildDecryptionKeyCandidates(decryptionKey);