From 90a356b29d74a16ee727f49743ade645aed76bd2 Mon Sep 17 00:00:00 2001 From: Milan Stevanovic Date: Wed, 9 Apr 2025 14:55:05 -0700 Subject: [PATCH] Fix incorrect embedded druation in certain MP4 files. The root cause: - some MP4 files come with H.264/H.265 streams which explicitly state their timescale. In such cases, it is wise that MP4 muxer adopts these values - unfortunately, the recent trend has been that such values coming from video stream SPS (vui_parameters/timing info) are exorbitantly high - instead of being FPS *1000, they tend to be FPS * 100,000,000 - when trying to express the duration of the movie, the MP4 muxer normally tries to find the adequate timescale value which will fit both audio and video timescaling domains. The most suitable approach is that the LCM (least common multiplier) value is taken which mathematically will be the least disruptive. HOWEVER: - in cases when video and timescale numeric values are mutually 'odd', say 30*100,000,000 and 44100, the LCM ends up being a huge number, which outgrows the 32-bit storage capacity granted by the ISO MP4 spec (MVHD box). Problem solution: 1) identifying when the LCM timescale exceeds 32-bit storage space 2) scaling down its value by nearest larger 10X factor, which will guarantee its value fitting the 32-bit space. Given the afore mentioned video timescale factors, dividing by 10X is harmless 3) rescaling the duration 64-bit value based on the new timescale --- .../video/videoconverter/muxer/Mp4Writer.java | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/muxer/Mp4Writer.java b/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/muxer/Mp4Writer.java index 088f9facc5..2d7484d73d 100644 --- a/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/muxer/Mp4Writer.java +++ b/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/muxer/Mp4Writer.java @@ -226,8 +226,38 @@ final class Mp4Writer extends DefaultBoxes implements SampleSink { } - mvhd.setTimescale(Mp4Math.lcm(timescales)); - mvhd.setDuration((long) (Mp4Math.lcm(timescales) * duration)); + long chosenTimescale = Mp4Math.lcm(timescales); + Log.d(TAG, "chosenTimescale = " + chosenTimescale); + final long MAX_UNSIGNED_INT = 0xFFFFFFFFL; + if (chosenTimescale > MAX_UNSIGNED_INT) { + int nRatio = (int)(chosenTimescale / MAX_UNSIGNED_INT); + Log.d(TAG, "chosenTimescale exceeds 32-bit range " + nRatio + " times !"); + int nDownscaleFactor = 1; + if (nRatio < 10) { + nDownscaleFactor = 10; + } else if (nRatio < 100) { + nDownscaleFactor = 100; + } else if (nRatio < 1000) { + nDownscaleFactor = 1000; + } else if (nRatio < 10000) { + nDownscaleFactor = 10000; + } + chosenTimescale /= nDownscaleFactor; + Log.d(TAG, "chosenTimescale is scaled down by factor of " + nDownscaleFactor + " to value " + chosenTimescale); + } + + double fDurationTicks = chosenTimescale * duration; + Log.d(TAG, "fDurationTicks = chosenTimescale * duration = " + fDurationTicks); + final double MAX_UNSIGNED_64_BIT_VALUE = 18446744073709551615.0; + if (fDurationTicks > MAX_UNSIGNED_64_BIT_VALUE) { + // Highly unlikely, as duration (number of seconds) + // would need to be larger than MAX_UNSIGNED_INT + // to produce fDuration = chosenTimescale * duration + // which whould exceed 64-bit storage + Log.d(TAG, "Numeric overflow !!!"); + } + mvhd.setTimescale(chosenTimescale); + mvhd.setDuration((long) (fDurationTicks)); // find the next available trackId mvhd.setNextTrackId(maxTrackId + 1); return mvhd;