Environment
- MistServer version: 3.10
- Input: SCP2100 encoder via RIST or SRT
- Output: TS over SRT (
OutTSSRT) - Audio codec: AAC (AAC-LC or HE-AAC)
- OS: Ubuntu
Symptoms
Two distinct but related issues on the SRT output side. The input stream has been verified clean.
- Periodic micro-freezes affecting audio and video simultaneously, at regular intervals.
- AAC PTS duplication after long uptime — MistServer emits duplicate AAC frames without incrementing the PTS. The decoder receives two (or more) audio frames with the same timestamp. The condition self-resolves after approximately 3 minutes but recurs.
Root Cause Analysis
Bug 1 — AAC PTS alignment produces duplicate timestamps (output_ts_base.cpp:203–215)
When muxing AAC audio into MPEG-TS, MistServer applies a quantization step to align the PTS to 1024-sample boundaries:
if (codec == "AAC"){
tempLen += 7;
uint32_t freq = M.getRate(thisIdx);
if (freq){
uint64_t aacSamples = packTime * freq / 90000;
//round to nearest packet, assuming all 1024 samples (probably wrong, but meh)
aacSamples += 256; // quarter-frame offset for rounding
aacSamples &= ~0x3FF; // align to 1024-sample boundary
packTime = aacSamples * 90000 / freq;
}
}
The comment in the source — "probably wrong, but meh" — is accurate. The mask ~0x3FF aligns to multiples of 1024 samples. This is exact at 48 kHz:
1024 × 90000 / 48000 = 1920 ticks (integer, no rounding error)
At 44100 Hz, however:
1024 × 90000 / 44100 = 2089.795... (not an integer)
Due to integer division, the raw sample increment between two consecutive frames is either 1023 or 1024. When the increment is 1023 and (aacSamples_N + 256) happens to land exactly on a 1024-sample boundary, both frame N and frame N+1 map to the same aligned value → identical PTS.
Concrete example of the collision:
aacSamples_N + 256 = 1024k (exactly on boundary)
aacSamples_{N+1} + 256 = 1024k + 1023
(1024k + 1023) & ~0x3FF = 1024k ← same block → duplicate PTS
For HE-AAC with 960-sample frames (instead of 1024), the mismatch is systematic: the mask quantizes to 1024-sample steps while frames advance by 960 samples, causing periodic collisions regardless of sample rate.
The "after long uptime" characteristic is explained by the fact that the collision requires a specific alignment of the accumulated timestamp modulo 1024, which only occurs at particular absolute times in the stream.
Bug 2 — Live-edge page scan re-reads last packet (output.cpp:2496–2519)
When the output reaches the last packet on a live buffer page and the next page has not yet been committed to metadata, the following sequence occurs:
- Packet
P (last on page N) is read and delivered. nxt.offset advances past the end of page N. - Next call:
atPageEnd = true, nextPage = 0 (next page not registered yet). M.getPageNumbersForTime(nxt.tid, nxt.time, ...) returns targetPage = N (current page), since nxt.time equals the time of P.- The condition at line 2481 is true (
M.getLastms() >= nxt.time), so nxt.offset is reset to 0. - The
if (atPageEnd && nextPage && ...) branch is not taken because nextPage == 0. - The else branch re-loads page
N and scans for the first packet with time >= nxt.time. - It finds
P again (same timestamp, >= condition matches). Sets nxt.ghostPacket = false and returns. - Next call:
atPageEnd = false (offset is now within page N). Packet P is loaded and sent again — duplicate.
This loop repeats until the next page appears in metadata. The duration of the loop depends on the encoder's key interval and buffer commit latency, which can explain multi-minute windows of continuous duplication on some configurations.
Relevant code path (else branch, output.cpp:2496–2516):
} else {
// Load the current page, since we're not at the end (or there is no next page yet)
loadPageForKey(nxt.tid, targetPage); // re-loads the SAME page N
IPC::sharedPage & cPage = curPage[nxt.tid];
if (cPage.mapped && cPage.mapped[0] == 'D') {
DTSC::Packet pkt(cPage.mapped, 0, true);
while (pkt) {
if (pkt.getTime() >= nxt.time) { // finds P again!
nxt.time = pkt.getTime();
break;
}
nxt.offset += pkt.getDataLen();
pkt.reInit(cPage.mapped + nxt.offset, 0, true);
}
nxt.ghostPacket = false; // clears ghost flag → packet will be re-sent
nxt.unavailable = false;
}
...
}
Bug 3 — Micro-freezes from aggressive live seek (output.cpp:1491–1493)
When the output falls more than 3000 ms behind the live edge, liveSeek() unconditionally calls seek() to jump to the latest keyframe:
if (!rateOnly && diff > 3000){
noReturn = true;
newSpeed = 1000;
}
This skips forward on all selected tracks simultaneously, which produces brief but perceptible audio+video freezes at the decoder. For low-latency SRT output, this threshold may be too aggressive.
Proposed Fixes
Fix 1 — Remove broken AAC PTS alignment (output_ts_base.cpp)
The simplest and safest fix is to remove the alignment code entirely. PTS accuracy is more important than sample alignment; misalignment of up to 256 samples (~5 ms) introduced by this code can also cause A/V sync drift.
if (codec == "AAC"){
tempLen += 7;
- // Make sure TS timestamp is sample-aligned, if possible
- uint32_t freq = M.getRate(thisIdx);
- if (freq){
- uint64_t aacSamples = packTime * freq / 90000;
- //round to nearest packet, assuming all 1024 samples (probably wrong, but meh)
- aacSamples += 256;//Add a quarter frame of offset to encourage correct rounding
- aacSamples &= ~0x3FF;
- //Get closest 90kHz clock time to perfect sample alignment
- packTime = aacSamples * 90000 / freq;
- }
}
If sample-alignment is desired in the future, the frame size should be read from the codec init data rather than assumed to be 1024, and the quantization should guarantee strict monotonicity (e.g., track last output PTS per track and ensure each new PTS > last).
Fix 2 — Prevent live-edge page re-scan from duplicating last packet (output.cpp)
When at the end of a page and no next page is registered yet, the output should wait rather than re-scan the current page:
- } else {
- // Load the current page, since we're not at the end (or there is no next page yet)
- loadPageForKey(nxt.tid, targetPage);
- IPC::sharedPage & cPage = curPage[nxt.tid];
- if (cPage.mapped && cPage.mapped[0] == 'D') {
- // Find the next timestamp greater or equal to nxt.time and set the offset to it
- DTSC::Packet pkt(cPage.mapped, 0, true);
- while (pkt) {
- if (pkt.getTime() >= nxt.time) {
- nxt.time = pkt.getTime();
- break;
- }
- nxt.offset += pkt.getDataLen();
- pkt.reInit(cPage.mapped + nxt.offset, 0, true);
- }
- nxt.ghostPacket = false;
- nxt.unavailable = false;
- } else {
- nxt.ghostPacket = true;
- nxt.unavailable = true;
- }
- }
+ } else if (atPageEnd && !nextPage && targetPage == currentPage[nxt.tid]) {
+ // At live edge: end of current page, next page not registered yet.
+ // Do NOT re-scan the current page — that would re-deliver the last packet.
+ // Wait for the next page to become available.
+ nxt.ghostPacket = true;
+ nxt.unavailable = true;
+ } else {
+ // Load the target page and scan for the correct packet position.
+ loadPageForKey(nxt.tid, targetPage);
+ IPC::sharedPage & cPage = curPage[nxt.tid];
+ if (cPage.mapped && cPage.mapped[0] == 'D') {
+ DTSC::Packet pkt(cPage.mapped, 0, true);
+ while (pkt) {
+ if (pkt.getTime() >= nxt.time) {
+ nxt.time = pkt.getTime();
+ break;
+ }
+ nxt.offset += pkt.getDataLen();
+ pkt.reInit(cPage.mapped + nxt.offset, 0, true);
+ }
+ nxt.ghostPacket = false;
+ nxt.unavailable = false;
+ } else {
+ nxt.ghostPacket = true;
+ nxt.unavailable = true;
+ }
+ }
Files Affected
File | Change
-- | --
src/output/output_ts_base.cpp | Remove AAC PTS alignment block (lines 205–214)
src/output/output.cpp | Add live-edge guard before current-page rescan (around line 2496)
Additional Notes
- The input stream (verified via RIST/SRT capture) shows no timestamp anomalies — all defects originate in the MistServer output path.
- Both bugs are reproducible on long-running live streams; Bug 1 is also reproducible at specific sample rates (44100 Hz) with short test runs by crafting timestamps that hit the collision condition.
- Bug 3 (micro-freezes) may warrant a configurable
liveSeek threshold for SRT output, or at minimum an option to disable the seek-forward behavior (maxSkipAhead = 1 disables it per the existing code).
Environment
OutTSSRT)Symptoms
Two distinct but related issues on the SRT output side. The input stream has been verified clean.
Root Cause Analysis
Bug 1 — AAC PTS alignment produces duplicate timestamps (
output_ts_base.cpp:203–215)When muxing AAC audio into MPEG-TS, MistServer applies a quantization step to align the PTS to 1024-sample boundaries:
The comment in the source — "probably wrong, but meh" — is accurate. The mask
~0x3FFaligns to multiples of 1024 samples. This is exact at 48 kHz:At 44100 Hz, however:
Due to integer division, the raw sample increment between two consecutive frames is either 1023 or 1024. When the increment is 1023 and
(aacSamples_N + 256)happens to land exactly on a 1024-sample boundary, both frame N and frame N+1 map to the same aligned value → identical PTS.Concrete example of the collision:
For HE-AAC with 960-sample frames (instead of 1024), the mismatch is systematic: the mask quantizes to 1024-sample steps while frames advance by 960 samples, causing periodic collisions regardless of sample rate.
The "after long uptime" characteristic is explained by the fact that the collision requires a specific alignment of the accumulated timestamp modulo 1024, which only occurs at particular absolute times in the stream.
Bug 2 — Live-edge page scan re-reads last packet (
output.cpp:2496–2519)When the output reaches the last packet on a live buffer page and the next page has not yet been committed to metadata, the following sequence occurs:
P(last on pageN) is read and delivered.nxt.offsetadvances past the end of pageN.atPageEnd = true,nextPage = 0(next page not registered yet).M.getPageNumbersForTime(nxt.tid, nxt.time, ...)returnstargetPage = N(current page), sincenxt.timeequals the time ofP.M.getLastms() >= nxt.time), sonxt.offsetis reset to0.if (atPageEnd && nextPage && ...)branch is not taken becausenextPage == 0.Nand scans for the first packet withtime >= nxt.time.Pagain (same timestamp,>=condition matches). Setsnxt.ghostPacket = falseand returns.atPageEnd = false(offset is now within pageN). PacketPis loaded and sent again — duplicate.This loop repeats until the next page appears in metadata. The duration of the loop depends on the encoder's key interval and buffer commit latency, which can explain multi-minute windows of continuous duplication on some configurations.
Relevant code path (else branch,
output.cpp:2496–2516):Bug 3 — Micro-freezes from aggressive live seek (
output.cpp:1491–1493)When the output falls more than 3000 ms behind the live edge,
liveSeek()unconditionally callsseek()to jump to the latest keyframe:This skips forward on all selected tracks simultaneously, which produces brief but perceptible audio+video freezes at the decoder. For low-latency SRT output, this threshold may be too aggressive.
Proposed Fixes
Fix 1 — Remove broken AAC PTS alignment (
output_ts_base.cpp)The simplest and safest fix is to remove the alignment code entirely. PTS accuracy is more important than sample alignment; misalignment of up to 256 samples (~5 ms) introduced by this code can also cause A/V sync drift.
If sample-alignment is desired in the future, the frame size should be read from the codec init data rather than assumed to be 1024, and the quantization should guarantee strict monotonicity (e.g., track last output PTS per track and ensure each new PTS > last).
Fix 2 — Prevent live-edge page re-scan from duplicating last packet (
output.cpp)When at the end of a page and no next page is registered yet, the output should wait rather than re-scan the current page:
Files Affected
File | Change -- | -- src/output/output_ts_base.cpp | Remove AAC PTS alignment block (lines 205–214) src/output/output.cpp | Add live-edge guard before current-page rescan (around line 2496)Additional Notes
liveSeekthreshold for SRT output, or at minimum an option to disable the seek-forward behavior (maxSkipAhead = 1disables it per the existing code).