Skip to content

Commit 73814ee

Browse files
authored
Merge pull request #24 from themactep/feature/rtsp-stability-improvements
fix: harden RTSP timestamp handling
2 parents e480833 + 044d588 commit 73814ee

10 files changed

Lines changed: 248 additions & 63 deletions

src/AudioReframer.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
AudioReframer::AudioReframer(unsigned int inputSampleRate, unsigned int inputSamplesPerFrame,
66
unsigned int outputSamplesPerFrame)
77
: inputSampleRate(inputSampleRate), inputSamplesPerFrame(inputSamplesPerFrame),
8-
outputSamplesPerFrame(outputSamplesPerFrame), currentTimestamp(0), samplesAccumulated(0),
8+
outputSamplesPerFrame(outputSamplesPerFrame), currentTimestamp(0), timestampRemainder(0), samplesAccumulated(0),
99
buffer(2 * std::max(inputSamplesPerFrame, outputSamplesPerFrame) * sizeof(uint16_t)) {
1010
if (inputSamplesPerFrame == 0 || outputSamplesPerFrame == 0) {
1111
throw std::invalid_argument("Number of samples per frame must be greater than zero.");
@@ -22,6 +22,7 @@ void AudioReframer::addFrame(const uint8_t *frameData, int64_t timestamp) {
2222

2323
if (samplesAccumulated == 0) {
2424
currentTimestamp = timestamp; // Initialize timestamp with the first frame
25+
timestampRemainder = 0;
2526
}
2627

2728
samplesAccumulated += inputSamplesPerFrame;
@@ -42,7 +43,14 @@ void AudioReframer::getReframedFrame(uint8_t *frameData, int64_t &timestamp) {
4243

4344
timestamp = currentTimestamp;
4445
if (inputSampleRate > 0) {
45-
currentTimestamp += (outputSamplesPerFrame * 1000) / inputSampleRate;
46+
// Timestamp is in microseconds; preserve fractional precision between frames.
47+
int64_t numer = static_cast<int64_t>(outputSamplesPerFrame) * 1000000LL + timestampRemainder;
48+
int64_t step_us = numer / static_cast<int64_t>(inputSampleRate);
49+
timestampRemainder = numer % static_cast<int64_t>(inputSampleRate);
50+
if (step_us < 1) {
51+
step_us = 1;
52+
}
53+
currentTimestamp += step_us;
4654
} // else: keep currentTimestamp stable if misconfigured
4755
}
4856

src/AudioReframer.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class AudioReframer {
2020
unsigned int inputSamplesPerFrame;
2121
unsigned int outputSamplesPerFrame;
2222
int64_t currentTimestamp;
23+
int64_t timestampRemainder;
2324
size_t samplesAccumulated;
2425

2526
RingBuffer buffer;

src/Config.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,8 +470,8 @@ std::vector<ConfigItem<int>> CFG::getIntItems() {
470470
{"audio.spk_gain", audio.output_gain, 20, [](const int &v) { return v >= 0 && v <= 31; }},
471471
{"audio.spk_vol", audio.output_vol, 60, [](const int &v) { return v >= -30 && v <= 120; }},
472472
#endif
473-
{"audio.buffer_warn_frames", audio.buffer_warn_frames, 80, [](const int &v) { return v >= 10 && v <= 1000; }},
474-
{"audio.buffer_cap_frames", audio.buffer_cap_frames, 100, [](const int &v) { return v >= 10 && v <= 1000; }},
473+
{"audio.buffer_warn_frames", audio.buffer_warn_frames, 240, [](const int &v) { return v >= 10 && v <= 1000; }},
474+
{"audio.buffer_cap_frames", audio.buffer_cap_frames, 400, [](const int &v) { return v >= 10 && v <= 1000; }},
475475
{"daynight.switch_below_percent", daynight.switch_below_percent, 15, [](const int &v) { return v >= 0 && v <= 100; }},
476476
{"daynight.switch_above_percent", daynight.switch_above_percent, 80, [](const int &v) { return v >= 0 && v <= 100; }},
477477
{"daynight.tolerance_percent", daynight.tolerance_percent, 50, [](const int &v) { return v >= 0 && v <= 100; }},

src/IMPAudioServerMediaSubsession.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ FramedSource *IMPAudioServerMediaSubsession::createNewStreamSource(unsigned clie
2929
return nullptr;
3030
}
3131

32+
if (global_audio[audioChn]->msgChannel) {
33+
global_audio[audioChn]->msgChannel->clear();
34+
}
3235
FramedSource *audioSourceReplica = replicator->createStreamReplica();
3336
if (audioSourceReplica) {
3437
global_audio[audioChn]->rtsp_client_count.fetch_add(1, std::memory_order_relaxed);

src/IMPDeviceSource.cpp

Lines changed: 133 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
#include "IMPDeviceSource.hpp"
22
#include "GroupsockHelper.hh"
3+
#include <cstring>
34
#include <iostream>
45
#include <type_traits>
56

7+
static inline int64_t tv_to_us(const struct timeval &tv) {
8+
return static_cast<int64_t>(tv.tv_sec) * 1000000LL + static_cast<int64_t>(tv.tv_usec);
9+
}
10+
11+
static inline struct timeval us_to_tv(int64_t us) {
12+
struct timeval tv;
13+
tv.tv_sec = static_cast<time_t>(us / 1000000LL);
14+
tv.tv_usec = static_cast<suseconds_t>(us % 1000000LL);
15+
if (tv.tv_usec < 0) {
16+
tv.tv_sec -= 1;
17+
tv.tv_usec += 1000000;
18+
}
19+
return tv;
20+
}
21+
22+
static inline bool is_plausible_wallclock_tv(const struct timeval &tv) {
23+
if (tv.tv_usec < 0 || tv.tv_usec >= 1000000) {
24+
return false;
25+
}
26+
constexpr time_t kMinUnixTime = 946684800; // 2000-01-01
27+
return tv.tv_sec >= kMinUnixTime;
28+
}
29+
630
// explicit instantiation
731
template class IMPDeviceSource<H264NALUnit, video_stream>;
832
template class IMPDeviceSource<AudioFrame, audio_stream>;
@@ -56,7 +80,7 @@ template <typename FrameType, typename Stream> void IMPDeviceSource<FrameType, S
5680
}
5781

5882
FrameType nal;
59-
if (stream->msgChannel->read(&nal)) {
83+
while (stream->msgChannel->read(&nal)) {
6084
if (nal.data.size() > fMaxSize) {
6185
fFrameSize = fMaxSize;
6286
fNumTruncatedBytes = nal.data.size() - fMaxSize;
@@ -68,15 +92,40 @@ template <typename FrameType, typename Stream> void IMPDeviceSource<FrameType, S
6892
steady +1024 RTP PTS increments and avoid gettimeofday jitter. */
6993
if constexpr (std::is_same_v<FrameType, AudioFrame>) {
7094
auto *imp_audio = global_audio[encChn]->imp_audio;
71-
if (imp_audio && imp_audio->format == IMPAudioFormat::AAC) {
95+
bool use_aac_clock = false;
96+
int sampleRate = 16000;
97+
if (cfg && cfg->audio.input_sample_rate > 0) {
98+
sampleRate = cfg->audio.input_sample_rate;
99+
}
100+
if (imp_audio) {
101+
use_aac_clock = (imp_audio->format == IMPAudioFormat::AAC);
102+
if (imp_audio->sample_rate > 0) {
103+
sampleRate = imp_audio->sample_rate;
104+
}
105+
} else if (cfg && cfg->audio.input_format && std::strcmp(cfg->audio.input_format, "AAC") == 0) {
106+
// During transient audio restarts, keep AAC timing behavior stable.
107+
use_aac_clock = true;
108+
}
109+
110+
bool has_audio_time = is_plausible_wallclock_tv(nal.time);
111+
if (has_audio_time) {
112+
fPresentationTime = nal.time;
113+
} else if (use_aac_clock) {
72114
if (audioFirstFrame) {
73115
gettimeofday(&audioStartTime, NULL);
74116
audioFrameCount = 0;
117+
audioClockSampleRate = sampleRate;
75118
audioFirstFrame = false;
119+
} else if (audioClockSampleRate <= 0 && sampleRate > 0) {
120+
audioClockSampleRate = sampleRate;
121+
}
122+
int effectiveSampleRate = (audioClockSampleRate > 0) ? audioClockSampleRate : sampleRate;
123+
if (effectiveSampleRate <= 0) {
124+
effectiveSampleRate = 16000;
76125
}
77-
int sampleRate = (imp_audio->sample_rate > 0) ? imp_audio->sample_rate : 16000;
78126
constexpr uint64_t kSamplesPerFrame = 1024;
79-
uint64_t usec_offset = (audioFrameCount * kSamplesPerFrame * 1000000ULL) / static_cast<uint64_t>(sampleRate);
127+
uint64_t usec_offset =
128+
(audioFrameCount * kSamplesPerFrame * 1000000ULL) / static_cast<uint64_t>(effectiveSampleRate);
80129
fPresentationTime.tv_sec = audioStartTime.tv_sec + static_cast<time_t>(usec_offset / 1000000ULL);
81130
fPresentationTime.tv_usec = audioStartTime.tv_usec + static_cast<suseconds_t>(usec_offset % 1000000ULL);
82131
if (fPresentationTime.tv_usec >= 1000000) {
@@ -87,53 +136,96 @@ template <typename FrameType, typename Stream> void IMPDeviceSource<FrameType, S
87136
} else {
88137
gettimeofday(&fPresentationTime, NULL);
89138
}
90-
} else {
91-
// Video: use encoder timestamps for stable, jitter-free PTS.
92-
// The IMP encoder provides a monotonic microsecond timestamp per NAL.
93-
// We anchor it to wall-clock at the first frame, then derive all
94-
// subsequent presentation times from the encoder's own clock.
95-
if (videoFirstFrame) {
96-
gettimeofday(&videoBaseTime, NULL);
97-
videoFirstImpTs = nal.imp_ts;
98-
videoFirstFrame = false;
139+
140+
int64_t pts_us = tv_to_us(fPresentationTime);
141+
int rate_for_tick = sampleRate > 0 ? sampleRate : 16000;
142+
int64_t min_audio_step_us = (1000000LL + rate_for_tick - 1) / rate_for_tick;
143+
if (use_aac_clock) {
144+
min_audio_step_us = (1024LL * 1000000LL + rate_for_tick - 1) / rate_for_tick;
99145
}
100-
int64_t delta_us = nal.imp_ts - videoFirstImpTs;
101-
// Re-anchor on negative delta (wrap/reset) or any step > 2s relative to
102-
// the previous frame — forward (encoder uptime jump) or backward (encoder
103-
// counter reset that doesn't go below the anchor).
104-
bool needs_reanchor = delta_us < 0;
105-
if (!needs_reanchor && videoLastDelta >= 0) {
106-
int64_t step = delta_us - videoLastDelta;
107-
needs_reanchor = (step > 2000000LL) || (step < -500000LL);
146+
if (min_audio_step_us < 1) {
147+
min_audio_step_us = 1;
108148
}
109-
if (needs_reanchor) {
110-
gettimeofday(&videoBaseTime, NULL);
111-
videoFirstImpTs = nal.imp_ts;
112-
delta_us = 0;
113-
} else if (videoLastDelta >= 0 && delta_us <= videoLastDelta) {
114-
// Clamp small backward jitter (<500ms) to keep PTS strictly
115-
// monotonically increasing. The <= also handles the SPS/PPS/IDR
116-
// triplet where all three NALs share the same imp_ts (delta_us ==
117-
// videoLastDelta): each gets nudged forward by one 90 kHz tick
118-
// (≈11 µs) so the RTP sender never emits two packets with the
119-
// same timestamp.
120-
delta_us = videoLastDelta + 12;
149+
if (audioLastPtsUs >= 0) {
150+
int64_t delta_pts_us = pts_us - audioLastPtsUs;
151+
int64_t max_forward_jump_us = 1000000LL;
152+
if (use_aac_clock && min_audio_step_us > 0) {
153+
int64_t aac_jump_limit = min_audio_step_us * 120;
154+
if (aac_jump_limit > max_forward_jump_us) {
155+
max_forward_jump_us = aac_jump_limit;
156+
}
157+
}
158+
if (delta_pts_us <= 0 || delta_pts_us > max_forward_jump_us) {
159+
pts_us = audioLastPtsUs + min_audio_step_us;
160+
fPresentationTime = us_to_tv(pts_us);
161+
}
121162
}
122-
videoLastDelta = delta_us;
123-
fPresentationTime.tv_sec = videoBaseTime.tv_sec + static_cast<time_t>(delta_us / 1000000LL);
124-
fPresentationTime.tv_usec = videoBaseTime.tv_usec + static_cast<suseconds_t>(delta_us % 1000000LL);
125-
if (fPresentationTime.tv_usec >= 1000000) {
126-
fPresentationTime.tv_sec++;
127-
fPresentationTime.tv_usec -= 1000000;
163+
audioLastPtsUs = pts_us;
164+
} else {
165+
if (is_plausible_wallclock_tv(nal.time)) {
166+
fPresentationTime = nal.time;
167+
} else {
168+
// Video fallback path for sources that don't provide explicit timeval.
169+
int64_t nominal_step_us = 33333;
170+
if (stream && stream->stream && stream->stream->fps > 0) {
171+
nominal_step_us = 1000000LL / stream->stream->fps;
172+
}
173+
if (nominal_step_us < 12) {
174+
nominal_step_us = 12;
175+
}
176+
if (videoFirstFrame) {
177+
gettimeofday(&videoBaseTime, NULL);
178+
videoFirstImpTs = nal.imp_ts;
179+
videoFirstFrame = false;
180+
}
181+
182+
int64_t delta_us = nal.imp_ts - videoFirstImpTs;
183+
184+
bool needs_reanchor = delta_us < 0;
185+
if (!needs_reanchor && videoLastDelta >= 0) {
186+
int64_t step = delta_us - videoLastDelta;
187+
needs_reanchor = (step > 2000000LL) || (step < -500000LL);
188+
}
189+
if (needs_reanchor) {
190+
int64_t target_delta = (videoLastDelta >= 0) ? (videoLastDelta + nominal_step_us) : 0;
191+
videoFirstImpTs = nal.imp_ts - target_delta;
192+
delta_us = target_delta;
193+
} else if (videoLastDelta >= 0 && delta_us <= videoLastDelta) {
194+
delta_us = videoLastDelta + 12;
195+
}
196+
videoLastDelta = delta_us;
197+
fPresentationTime.tv_sec = videoBaseTime.tv_sec + static_cast<time_t>(delta_us / 1000000LL);
198+
fPresentationTime.tv_usec = videoBaseTime.tv_usec + static_cast<suseconds_t>(delta_us % 1000000LL);
199+
if (fPresentationTime.tv_usec >= 1000000) {
200+
fPresentationTime.tv_sec++;
201+
fPresentationTime.tv_usec -= 1000000;
202+
}
203+
}
204+
205+
int64_t pts_us = tv_to_us(fPresentationTime);
206+
int64_t min_video_step_us = 12;
207+
if (stream && stream->stream && stream->stream->fps > 0) {
208+
min_video_step_us = 1000000LL / stream->stream->fps;
209+
if (min_video_step_us < 12) {
210+
min_video_step_us = 12;
211+
}
212+
}
213+
if (videoLastPtsUs >= 0) {
214+
int64_t delta_pts_us = pts_us - videoLastPtsUs;
215+
if (delta_pts_us <= 0 || delta_pts_us > 2000000LL) {
216+
pts_us = videoLastPtsUs + min_video_step_us;
217+
fPresentationTime = us_to_tv(pts_us);
218+
}
128219
}
220+
videoLastPtsUs = pts_us;
129221
}
130222

131223
memcpy(fTo, &nal.data[0], fFrameSize);
132224

133225
if (fFrameSize > 0) {
134226
FramedSource::afterGetting(this);
227+
return;
135228
}
136-
} else {
137-
fFrameSize = 0;
138229
}
230+
fFrameSize = 0;
139231
}

src/IMPDeviceSource.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@ template <typename FrameType, typename Stream> class IMPDeviceSource : public Fr
3434
bool audioFirstFrame{true};
3535
struct timeval audioStartTime{};
3636
uint64_t audioFrameCount{0};
37+
int audioClockSampleRate{0};
38+
int64_t audioLastPtsUs{-1};
3739

3840
// Video encoder timestamp tracking (avoids gettimeofday jitter)
3941
bool videoFirstFrame{true};
4042
int64_t videoFirstImpTs{0};
4143
int64_t videoLastDelta{-1};
44+
int64_t videoLastPtsUs{-1};
4245
struct timeval videoBaseTime{};
4346
};
4447

src/IMPEncoder.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ IMPEncoder *IMPEncoder::createNew(_stream *stream, int encChn, int encGrp, const
3939
void IMPEncoder::flush(int encChn) {
4040
LOG_DDEBUG("flush(" << encChn << ")");
4141
IMP_Encoder_RequestIDR(encChn);
42-
IMP_Encoder_FlushStream(encChn);
4342
}
4443

4544
void MakeTables(int q, uint8_t *lqt, uint8_t *cqt) {

src/IMPServerMediaSubsession.hpp

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,19 @@ class IMPServerMediaSubsession : public OnDemandServerMediaSubsession {
3333
void *rtcpRRHandlerClientData, unsigned short &rtpSeqNum, unsigned &rtpTimestamp,
3434
ServerRequestAlternativeByteHandler *serverRequestAlternativeByteHandler,
3535
void *serverRequestAlternativeByteHandlerClientData) override {
36+
// Drop any stale queued NAL units so a new client starts from a fresh IDR
37+
// timeline and does not see pre-flush timestamp history.
38+
if (encChn >= 0 && encChn < NUM_VIDEO_CHANNELS && global_video[encChn] && global_video[encChn]->msgChannel) {
39+
global_video[encChn]->msgChannel->clear();
40+
}
41+
// request idr frame every second for the next x seconds
42+
global_video[encChn]->idr_fix = 5;
43+
IMPEncoder::flush(encChn);
44+
3645
OnDemandServerMediaSubsession::startStream(clientSessionId, streamToken, rtcpRRHandler, rtcpRRHandlerClientData,
3746
rtpSeqNum, rtpTimestamp, serverRequestAlternativeByteHandler,
3847
serverRequestAlternativeByteHandlerClientData);
39-
4048
global_rtsp_clients.fetch_add(1, std::memory_order_relaxed);
41-
// request idr frame every second for the next x seconds
42-
global_video[encChn]->idr_fix = 5;
43-
IMPEncoder::flush(encChn);
4449
}
4550

4651
virtual void deleteStream(unsigned clientSessionId, void *&streamToken) override {

0 commit comments

Comments
 (0)