From 823130ea9429ba7c01e92eef15b919b5a1b77b97 Mon Sep 17 00:00:00 2001 From: ThibaultBee <37510686+ThibaultBee@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:24:20 +0200 Subject: [PATCH] fix(core): fix TS clocks computation to avoid an overflow after 94h See https://github.com/ThibaultBee/StreamPack/issues/301 --- .../muxers/ts/descriptors/AdaptationField.kt | 55 +++++++++---------- .../composites/muxers/ts/packets/PesHeader.kt | 8 +-- .../composites/muxers/ts/utils/TSConst.kt | 1 - .../composites/muxers/ts/utils/TSTimeUtils.kt | 39 +++++++++++++ .../muxers/ts/utils/TSTimeUtilsTest.kt | 21 +++++++ 5 files changed, 89 insertions(+), 35 deletions(-) create mode 100644 core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSTimeUtils.kt create mode 100644 core/src/test/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSTimeUtilsTest.kt diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationField.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationField.kt index be451e5d5..0ffe5baf9 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationField.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationField.kt @@ -16,12 +16,11 @@ package io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.descriptors import io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.data.ITSElement -import io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.utils.TSConst +import io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.utils.TSTimeUtils import io.github.thibaultbee.streampack.core.elements.utils.extensions.put import io.github.thibaultbee.streampack.core.elements.utils.extensions.putShort import io.github.thibaultbee.streampack.core.elements.utils.extensions.shl import java.nio.ByteBuffer -import kotlin.math.pow class AdaptationField( private val discontinuityIndicator: Boolean = false, @@ -53,15 +52,16 @@ class AdaptationField( val buffer = ByteBuffer.allocate(size) buffer.put(adaptationFieldLength) - buffer.put(((discontinuityIndicator shl 7) - or (randomAccessIndicator shl 6) - or (elementaryStreamPriorityIndicator shl 5) - or ((programClockReference?.let { 1 } ?: 0) shl 4) - or ((originalProgramClockReference?.let { 1 } ?: 0) shl 3) - or ((spliceCountdown?.let { 1 } ?: 0) shl 2) - or ((transportPrivateData?.let { 1 } ?: 0) shl 1) - or (adaptationFieldExtension?.let { 1 } ?: 0) - )) + buffer.put( + ((discontinuityIndicator shl 7) + or (randomAccessIndicator shl 6) + or (elementaryStreamPriorityIndicator shl 5) + or ((programClockReference?.let { 1 } ?: 0) shl 4) + or ((originalProgramClockReference?.let { 1 } ?: 0) shl 3) + or ((spliceCountdown?.let { 1 } ?: 0) shl 2) + or ((transportPrivateData?.let { 1 } ?: 0) shl 1) + or (adaptationFieldExtension?.let { 1 } ?: 0) + )) programClockReference?.let { addClockReference(buffer, it) @@ -86,23 +86,22 @@ class AdaptationField( return buffer } - private fun addClockReference(buffer: ByteBuffer, timestamp: Long) { - val pcrBase = - (TSConst.SYSTEM_CLOCK_FREQ * timestamp / 1000000 /* µs -> s */ / 300) % 2.toDouble() - .pow(33) - .toLong() - val pcrExt = (TSConst.SYSTEM_CLOCK_FREQ * timestamp / 1000000 /* µs -> s */) % 300 + companion object { + private fun addClockReference(buffer: ByteBuffer, timestamp: Long) { + val pcrBase = TSTimeUtils.computeTimestamp90kHz(timestamp) + val pcrExt = TSTimeUtils.computePcrExt(timestamp) - /** - * PCR Base -> 33 bits - * Reserved -> 6 bits (0b111111) - * PCR Ext -> 9 bits - */ - buffer.putInt((pcrBase shr 1).toInt()) - buffer.putShort( - (((pcrBase and 0x1) shl 15) - or (0b111111 shl 9) - or (pcrExt and 0x1FF)) - ) + /** + * PCR Base -> 33 bits + * Reserved -> 6 bits (0b111111) + * PCR Ext -> 9 bits + */ + buffer.putInt((pcrBase shr 1).toInt()) + buffer.putShort( + (((pcrBase and 0x1) shl 15) + or (0b111111 shl 9) + or (pcrExt and 0x1FF)) + ) + } } } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/packets/PesHeader.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/packets/PesHeader.kt index 2960606d1..6602f21b3 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/packets/PesHeader.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/packets/PesHeader.kt @@ -16,14 +16,13 @@ package io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.packets import io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.data.ITSElement -import io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.utils.TSConst +import io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.utils.TSTimeUtils import io.github.thibaultbee.streampack.core.elements.utils.extensions.put import io.github.thibaultbee.streampack.core.elements.utils.extensions.putShort import io.github.thibaultbee.streampack.core.elements.utils.extensions.shl import io.github.thibaultbee.streampack.core.elements.utils.extensions.toInt import java.nio.ByteBuffer import kotlin.experimental.and -import kotlin.math.pow class PesHeader( private val streamId: Short, @@ -129,10 +128,7 @@ class PesHeader( } private fun addTimestamp(buffer: ByteBuffer, timestamp: Long, fourBits: Byte) { - val pts = - (TSConst.SYSTEM_CLOCK_FREQ * timestamp / 1000000 /* µs -> s */ / 300) % 2.toDouble() - .pow(33) - .toLong() + val pts = TSTimeUtils.computeTimestamp90kHz(timestamp) buffer.put( (((fourBits and 0xF).toInt() shl 4) diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSConst.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSConst.kt index fb602b426..149ad0b8c 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSConst.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSConst.kt @@ -16,6 +16,5 @@ package io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.utils object TSConst { - const val SYSTEM_CLOCK_FREQ = 27000000 const val BASE_PID = 0x00020.toShort() } \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSTimeUtils.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSTimeUtils.kt new file mode 100644 index 000000000..f56de2491 --- /dev/null +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSTimeUtils.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2026 Thibault B. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.utils + +object TSTimeUtils { + private const val PCR_BASE_MASK = 0x1FFFFFFFFL // 33 bits mask + + /** + * Converts a timestamp in microseconds to a 33-bit 90 kHz timestamp. + * Used for PCR Base, PTS, and DTS. + */ + fun computeTimestamp90kHz(timestamp: Long): Long { + // 90 kHz ticks per microsecond = 90,000 / 1,000,000 = 9 / 100 + return (timestamp * 9 / 100) and PCR_BASE_MASK + } + + /** + * Converts a timestamp in microseconds to a 9-bit 27 MHz extension clock. + * Used for PCR Ext. + */ + fun computePcrExt(timestamp: Long): Long { + // To convert microseconds to clock ticks, we use fractions to avoid early integer overflow. + // 27 MHz ticks per microsecond = 27,000,000 / 1,000,000 = 27 + return (timestamp * 27 % 300) + } +} diff --git a/core/src/test/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSTimeUtilsTest.kt b/core/src/test/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSTimeUtilsTest.kt new file mode 100644 index 000000000..71b24358c --- /dev/null +++ b/core/src/test/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/utils/TSTimeUtilsTest.kt @@ -0,0 +1,21 @@ +package io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.utils + +import org.junit.Assert.assertEquals +import org.junit.Test + +class TSTimeUtilsTest { + @Test + fun `test computeTimestamp90kHz with very large timestamp avoiding overflow`() { + // 400,000,000,000 µs (about 111 hours) + val timestamp = 400_000_000_000L + val expectedPcrBase = 1_640_261_632L + assertEquals(expectedPcrBase, TSTimeUtils.computeTimestamp90kHz(timestamp)) + } + + @Test + fun `test computePcrExt with very large timestamp avoiding overflow`() { + val timestamp = 400_000_000_000L + val expectedPcrExt = 0L + assertEquals(expectedPcrExt, TSTimeUtils.computePcrExt(timestamp)) + } +}