fix(download): normalize MP4 audio extension by codec (#512)

This commit is contained in:
zarzet
2026-08-11 19:07:00 +07:00
parent 4693e2f1d2
commit 5374883875
6 changed files with 146 additions and 13 deletions
@@ -542,7 +542,12 @@ object NativeDownloadFinalizer {
if (successPath != null) break
}
val decryptedPath = successPath ?: throw IllegalStateException("decrypt failed: $lastOutput")
val rawDecryptedPath = successPath ?: throw IllegalStateException("decrypt failed: $lastOutput")
val decryptedPath = normalizeDecryptedIsoBmffAudioPath(
rawDecryptedPath,
state,
shouldCancel,
)
replaceStatePath(context, input, state, decryptedPath, deleteOld = true)
} finally {
if (successPath == null) {
@@ -554,6 +559,37 @@ object NativeDownloadFinalizer {
}
}
private fun normalizeDecryptedIsoBmffAudioPath(
path: String,
state: FinalizeState,
shouldCancel: () -> Boolean,
): String {
if (!isMP4ContainerFile(path)) return path
val probedCodec = probePrimaryAudioCodec(path, shouldCancel)
val codec = normalizeAudioCodec(probedCodec.ifBlank { state.audioCodec.orEmpty() })
val desiredExt = NativeFinalizationPolicy.isoBmffAudioExtension(codec)
state.audioCodec = codec
if (path.lowercase(Locale.ROOT).endsWith(desiredExt)) return path
val target = File(buildOutputPath(path, desiredExt))
if (target.exists()) {
Log.w(TAG, "Cannot normalize ISO-BMFF audio extension; ${target.name} already exists")
return path
}
val source = File(path)
if (!source.renameTo(target)) {
Log.w(TAG, "Failed to normalize ISO-BMFF audio extension to ${target.name}")
return path
}
Log.i(
TAG,
"ISO-BMFF audio renamed: ${source.name} -> ${target.name} " +
"(codec=${codec.orEmpty().ifBlank { "unknown" }})",
)
return target.absolutePath
}
private fun finalizeHighConversion(
context: Context,
input: FinalizeInput,
@@ -814,10 +850,10 @@ object NativeDownloadFinalizer {
if (!currentFile.name.lowercase(Locale.ROOT).endsWith(".flac")) return
val newExt = when {
codec == "aac" && isMP4ContainerFile(localInput) -> ".m4a"
isMP4ContainerFile(localInput) ->
NativeFinalizationPolicy.isoBmffAudioExtension(codec)
codec == "mp3" -> ".mp3"
codec == "opus" -> ".opus"
isMP4ContainerFile(localInput) -> ".m4a"
else -> return
}
val renamed = File(
@@ -68,6 +68,15 @@ internal object NativeFinalizationPolicy {
}
}
/**
* Audio-only ISO-BMFF files conventionally use .m4a. AC-4 remains .mp4
* because its passthrough and standards-repair path depends on that
* container identity.
*/
fun isoBmffAudioExtension(audioCodec: String?): String {
return if (normalizeAudioCodec(audioCodec) == "ac4") ".mp4" else ".m4a"
}
fun audioFormatForCodec(codec: String?): String? {
return when (normalizeAudioCodec(codec)) {
"flac" -> "FLAC"
@@ -76,6 +76,26 @@ class NativeFinalizationPolicyTest {
assertFalse(NativeFinalizationPolicy.isLosslessAudioCodec("aac"))
}
@Test
fun isoBmffAudioUsesM4aExceptForAc4Passthrough() {
assertEquals(
".m4a",
NativeFinalizationPolicy.isoBmffAudioExtension("opus"),
)
assertEquals(
".m4a",
NativeFinalizationPolicy.isoBmffAudioExtension("ec-3"),
)
assertEquals(
".m4a",
NativeFinalizationPolicy.isoBmffAudioExtension("aac"),
)
assertEquals(
".mp4",
NativeFinalizationPolicy.isoBmffAudioExtension("ac-4"),
)
}
@Test
fun displayQualityUsesMeasuredLosslessSpecifications() {
assertEquals(