perf: retain Cast codecs across sequential VOD segments

This commit is contained in:
2026-10-17 00:56:20 +02:00
parent caa10c063f
commit 599c1492ee
4 changed files with 182 additions and 36 deletions
+94 -35
View File
@@ -342,9 +342,12 @@ struct OutputTrack {
int64_t audioPTS = AV_NOPTS_VALUE;
bool firstVideo = true;
double lastVideoTime = -INFINITY;
bool continuous;
int64_t audioKeepFrom = AV_NOPTS_VALUE, audioKeepUntil = INT64_MAX;
OutputTrack(Track &track, Mux &output, bool live) : source(track), mux(&output), continuous(live) {
bool live, drained = false;
std::deque<Packet> packets;
size_t packetBytes = 0;
int64_t lastPacketPTS = AV_NOPTS_VALUE;
int64_t keepFrom = INT64_MIN, keepUntil = INT64_MAX;
OutputTrack(Track &track, Mux &output, bool live) : source(track), mux(&output), live(live) {
try {
if (!source.stream) return;
stream = avformat_new_stream(mux->format, nullptr); require(stream != nullptr);
@@ -397,7 +400,6 @@ struct OutputTrack {
stream->time_base = encoder->time_base;
check(avcodec_parameters_from_context(stream->codecpar, encoder));
}
firstVideo = true;
}
void writePacket(AVPacket *packet) {
// A shared, positive TS clock origin prevents per-segment AAC priming shifts.
@@ -413,10 +415,16 @@ struct OutputTrack {
int result = avcodec_receive_packet(encoder, p.get());
if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) break;
check(result);
if (fifo && !continuous && audioKeepFrom != AV_NOPTS_VALUE &&
(p->pts < audioKeepFrom || p->pts >= audioKeepUntil)) {
av_packet_unref(p.get());
continue;
lastPacketPTS = p->pts;
if (!live) {
require(p->pts != AV_NOPTS_VALUE && p->size >= 0);
if (p->pts >= keepUntil) {
require(packetBytes + size_t(p->size) <= segmentLimit);
packetBytes += size_t(p->size);
packets.push_back(std::move(p)); p = packet();
continue;
}
if (p->pts < keepFrom) { av_packet_unref(p.get()); continue; }
}
av_packet_rescale_ts(p.get(), encoder->time_base, stream->time_base);
writePacket(p.get()); av_packet_unref(p.get());
@@ -463,12 +471,8 @@ struct OutputTrack {
}
} else {
require(f->sample_rate > 0 && f->nb_samples >= 0 && f->nb_samples <= 65536 && f->ch_layout.nb_channels <= 32);
const double begin = continuous ? start : aacPreroll(start);
const double limit = continuous ? end : double(aacBoundary(end)) / 48000;
if (!continuous) {
audioKeepFrom = aacBoundary(start);
audioKeepUntil = int64_t(std::llround(end * 48000));
}
const double begin = live ? start : aacPreroll(start);
const double limit = end;
if (time + double(f->nb_samples) / f->sample_rate > begin && time < limit) {
if (!resample) {
check(swr_alloc_set_opts2(&resample, &encoder->ch_layout, encoder->sample_fmt, 48000, &f->ch_layout, AVSampleFormat(f->format), f->sample_rate, 0, nullptr));
@@ -479,10 +483,9 @@ struct OutputTrack {
auto out = frame(); out->format = encoder->sample_fmt; out->sample_rate = 48000;
check(av_channel_layout_copy(&out->ch_layout, &encoder->ch_layout)); out->nb_samples = capacity; check(av_frame_get_buffer(out.get(), 0));
int count = swr_convert(resample, out->data, capacity, const_cast<const uint8_t **>(f->extended_data), f->nb_samples); check(count);
int skip = continuous && audioPTS != AV_NOPTS_VALUE ? 0 :
int skip = audioPTS != AV_NOPTS_VALUE ? 0 :
std::clamp(int(std::llround((begin - time) * 48000)), 0, count);
int take = continuous ? count - skip :
std::clamp(int(std::llround((limit - time) * 48000)) - skip, 0, count - skip);
int take = count - skip;
if (audioPTS == AV_NOPTS_VALUE) audioPTS = int64_t(std::llround(std::max(begin, time) * 48000));
uint8_t *data[2] = {out->data[0] + skip * 4, out->data[1] + skip * 4};
require(av_audio_fifo_size(fifo) + take <= 131072);
@@ -491,13 +494,52 @@ struct OutputTrack {
}
source.nextFrame();
}
void finish(double end) {
void beginPackets(double start, double end) {
if (!encoder || live) return;
keepFrom = fifo ? aacBoundary(start) : int64_t(std::llround(start * 90000));
keepUntil = fifo ? int64_t(std::llround(end * 48000)) : int64_t(std::llround(end * 90000));
while (!packets.empty() && packets.front()->pts < keepUntil) {
auto p = std::move(packets.front()); packets.pop_front();
packetBytes -= size_t(p->size);
if (p->pts < keepFrom) continue;
av_packet_rescale_ts(p.get(), encoder->time_base, stream->time_base);
writePacket(p.get());
}
}
void finish(double start, double end) {
if (!encoder) return;
if (continuous) {
if (fifo) audioFrames(false, end);
} else {
if (fifo) audioFrames(true);
encoded(nullptr);
if (live) {
if (fifo) audioFrames(false, end); else encoded(nullptr);
return;
}
// Neither encoder can resume after a drain. Feed just enough future
// input to receive this segment's delayed packets; retain the rest.
const int64_t target = fifo ? aacBoundary(end) - encoder->frame_size :
(std::isfinite(lastVideoTime) ? int64_t(std::llround(lastVideoTime * 90000)) : AV_NOPTS_VALUE);
if (!fifo) firstVideo = true;
while (!drained && target != AV_NOPTS_VALUE && (lastPacketPTS == AV_NOPTS_VALUE || lastPacketPTS < target)) {
if (fifo) audioFrames(false);
if (lastPacketPTS != AV_NOPTS_VALUE && lastPacketPTS >= target) break;
const double time = source.time();
// ponytail: cap lookahead at six seconds; drain and reset on the
// next segment rather than following a sparse stream arbitrarily far.
if (!std::isfinite(time) || time >= end + segmentSeconds) {
if (fifo) {
// Drain resampler delay before padding the final AAC frame.
if (resample) {
auto tail = frame(); tail->format = encoder->sample_fmt;
tail->nb_samples = 4096;
check(av_channel_layout_copy(&tail->ch_layout, &encoder->ch_layout));
check(av_frame_get_buffer(tail.get(), 0));
int count = swr_convert(resample, tail->data, tail->nb_samples, nullptr, 0); check(count);
require(av_audio_fifo_write(fifo, reinterpret_cast<void **>(tail->data), count) == count);
}
audioFrames(true);
}
encoded(nullptr); drained = true;
break;
}
consume(fifo ? start : end, INFINITY);
}
}
};
@@ -513,6 +555,7 @@ struct Pipeline {
std::atomic<bool> &cancel;
double duration = 0;
std::vector<double> boundaries;
double nextStart = 0;
Pipeline(const Source &source, std::atomic<bool> &stop, double start) : live(source.live), cancel(stop) {
videoInput = std::make_unique<Input>(stop, source.video, source.videoHeaders, source.local, source.restricted, source.live);
duration = std::max(source.duration, videoInput->duration());
@@ -567,8 +610,8 @@ struct Pipeline {
void armOperation() {
deadline = Clock::now() + std::chrono::seconds(45);
const auto limit = std::chrono::duration_cast<std::chrono::nanoseconds>(deadline.time_since_epoch()).count();
videoInput->operationDeadline = limit;
if (audioInput) audioInput->operationDeadline = limit;
videoInput->operationDeadline = limit; videoInput->arm();
if (audioInput) { audioInput->operationDeadline = limit; audioInput->arm(); }
}
void releaseOperation() {
videoInput->operationDeadline = std::numeric_limits<int64_t>::max();
@@ -578,8 +621,8 @@ struct Pipeline {
armOperation();
videoOutput.reset(); audioOutput.reset();
video.reset(); audio.reset();
// Fresh codecs retain independent IDRs/AAC priming. One recorded network
// demuxer atomically rewinds both packet lanes, seeking upstream only on misses.
// Random access resets codec history; the network demuxer rewinds both
// packet lanes atomically, seeking upstream only on cache misses.
const double audioTarget = audioIndex >= 0 && !remuxAudio(*audioSource->format->streams[audioIndex]->codecpar) ?
aacPreroll(start) : NAN;
if (videoInput->buffered)
@@ -587,13 +630,15 @@ struct Pipeline {
if (audioInput && audioInput->buffered) audioInput->seek(start, audioTarget);
if (videoIndex >= 0) video = std::make_unique<Track>(*videoInput, videoIndex, AVMEDIA_TYPE_VIDEO, start, live, aligned);
if (audioIndex >= 0) audio = std::make_unique<Track>(*audioSource, audioIndex, AVMEDIA_TYPE_AUDIO, start, live);
nextStart = start;
}
Segment segment(int64_t id, double start, double end) {
if (live) armOperation();
armOperation();
Mux mux;
if (video) {
videoOutput.reset();
videoOutput = std::make_unique<OutputTrack>(*video, mux, false);
if (live) videoOutput.reset();
if (videoOutput) videoOutput->beginSegment(mux);
else videoOutput = std::make_unique<OutputTrack>(*video, mux, live);
}
if (audio) {
if (audioOutput) audioOutput->beginSegment(mux);
@@ -604,6 +649,8 @@ struct Pipeline {
av_dict_set(&options, "mpegts_copyts", "1", 0);
av_dict_set(&options, "muxdelay", "0", 0);
int result = avformat_write_header(mux.format, &options); av_dict_free(&options); check(result);
if (videoOutput) videoOutput->beginPackets(start, end);
if (audioOutput) audioOutput->beginPackets(start, end);
const double audioEnd = audio && !audio->remux && !live ? double(aacBoundary(end)) / 48000 : end;
for (;;) {
require(!cancel && Clock::now() < deadline);
@@ -613,12 +660,13 @@ struct Pipeline {
if (!std::isfinite(vt) && !std::isfinite(at)) break;
if (vt <= at) videoOutput->consume(start, end); else audioOutput->consume(start, end);
}
if (videoOutput) videoOutput->finish(end);
if (audioOutput) audioOutput->finish(end);
if (videoOutput) videoOutput->finish(start, end);
if (audioOutput) audioOutput->finish(start, end);
check(av_write_trailer(mux.format)); avio_flush(mux.io);
require(mux.io->error >= 0 && !mux.bytes.empty());
if (videoInput->buffered) videoInput->buffered->updatePosition(end);
if (audioInput && audioInput->buffered) audioInput->buffered->updatePosition(end);
nextStart = end;
releaseOperation();
return {id, end - start, std::move(mux.bytes)};
}
@@ -740,7 +788,7 @@ struct CastMedia {
for (int64_t id = first; id < first + count; ++id) {
char value[64]; double length = source.live ? segments[size_t(id - first)]->duration : boundary(id + 1) - boundary(id);
std::snprintf(value, sizeof(value), "#EXTINF:%.6f,\n", length);
// Each independently generated segment resets codec/mux state.
// Each segment retains an independent mux and random-access entry.
if (id > first) text += "#EXT-X-DISCONTINUITY\n";
text += value; text += "segment/" + std::to_string(id) + ".ts\n";
}
@@ -761,8 +809,19 @@ struct CastMedia {
if (auto segment = cached()) return segment;
try {
const double start = boundary(id), end = boundary(id + 1);
if (vodPipeline) vodPipeline->reset(start, !boundaries.empty());
else vodPipeline = std::make_unique<Pipeline>(source, cancelled, start);
if (vodPipeline) {
const auto &video = vodPipeline->video;
// Fixed-grid remux is safe only when the next boundary is an
// actual IDR. Otherwise reset once into the transcode path.
const bool independent = !video || !video->remux ||
(video->pending && std::abs(video->packetTime() - start) < .001 &&
(video->pending->flags & AV_PKT_FLAG_KEY) &&
hasIDR(*video->pending, *video->stream->codecpar));
const bool drained = (vodPipeline->videoOutput && vodPipeline->videoOutput->drained) ||
(vodPipeline->audioOutput && vodPipeline->audioOutput->drained);
if (vodPipeline->nextStart != start || !independent || drained)
vodPipeline->reset(start, !boundaries.empty());
} else vodPipeline = std::make_unique<Pipeline>(source, cancelled, start);
auto segment = std::make_shared<const Segment>(vodPipeline->segment(id, start, end));
{ std::lock_guard guard(lock); segments.push_back(segment); while (segments.size() > windowSegments) segments.pop_front(); }
return segment;