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
+85
View File
@@ -14,6 +14,7 @@ extern "C" {
#include <condition_variable> #include <condition_variable>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
#include <ctime>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <future> #include <future>
@@ -673,6 +674,84 @@ static void remuxWork(const std::filesystem::path &directory) {
cast_media_destroy(media); cast_media_destroy(media);
} }
} }
// Exercise the HTTP relay, not an isolated codec: delayed video, resampled AAC,
// independent segment decoding, cache eviction, and random-to-sequential seeks.
static void codecWork(const std::filesystem::path &directory) {
const auto source = (directory / "codec.mp4").string(), audio = (directory / "codec.wav").string();
const char *args[] = {"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
"-f", "lavfi", "-i", "testsrc2=size=1280x720:rate=30:duration=61",
"-f", "lavfi", "-i", "sine=frequency=440:sample_rate=44100:duration=61",
"-c:v", "libx264", "-preset", "ultrafast", "-bf", "2", "-g", "210",
"-an", source.c_str(), "-vn", "-c:a", "pcm_s16le", audio.c_str(), nullptr};
pid_t child = 0;
assert(posix_spawnp(&child, args[0], nullptr, nullptr, const_cast<char *const *>(args), environ) == 0);
int status = 0; assert(waitpid(child, &status, 0) == child);
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
Upstream upstream(readFile(source), readFile(audio)); upstream.start();
const auto videoURL = upstream.url("/source.ts"), audioURL = upstream.url("/audio.wav");
std::string url; double duration = 0;
const auto began = std::chrono::steady_clock::now();
auto *media = start({videoURL.c_str(), "X-Video: video-secret\r\n", audioURL.c_str(),
"X-Audio: audio-secret\r\n", false, false, false, 0}, url, duration);
const auto prepared = std::chrono::steady_clock::now();
const auto base = url.substr(0, url.rfind('/') + 1), output = (directory / "codec-output.ts").string();
std::map<int, std::pair<double, double>> spans;
double sequentialMS = 0, randomMS = 0, sequentialCPU = 0;
for (int id : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 0, 1, 10}) {
const auto before = std::chrono::steady_clock::now();
const auto cpuBefore = std::clock();
const auto response = request(base + "segment/" + std::to_string(id) + ".ts");
const auto elapsed = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - before).count();
const double cpuMS = 1000.0 * (std::clock() - cpuBefore) / CLOCKS_PER_SEC;
if (spans.size() < 10) { sequentialMS += elapsed; sequentialCPU += cpuMS; }
else randomMS += elapsed;
assert(response.status == 200); writeFile(output, response.bytes);
const auto rows = samples(output, 1);
int videos = 0;
for (const auto &[key, value] : rows) {
assert(key.second >= id * 6 * 90000 && key.second < std::min(61, (id + 1) * 6) * 90000);
if (key.first) ++videos;
}
assert(videos == (id == 10 ? 30 : 180));
auto span = audioSpan(response, output);
if (spans.contains(id - 1)) assert(std::abs(span.first - spans.at(id - 1).second) < 1.0 / 48000);
if (spans.contains(id + 1)) assert(std::abs(spans.at(id + 1).first - span.second) < 1.0 / 48000);
spans[id] = span;
assert(request(base + "segment/" + std::to_string(id) + ".ts").bytes == response.bytes);
}
assert(upstream.foreignRequests == 0);
cast_media_destroy(media);
std::cout << "codec-work prepare_ms=" << std::chrono::duration<double, std::milli>(prepared - began).count()
<< " sequential_60s_ms=" << sequentialMS << " sequential_cpu_ms=" << sequentialCPU
<< " seek_sequence_ms=" << randomMS << std::endl;
// A long video gap must not turn bounded encoder lookahead into HTTP 503,
// and resuming video after audio-only segments still needs an independent IDR.
const auto sparsePath = (directory / "sparse.mp4").string();
const char *sparseArgs[] = {"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
"-i", source.c_str(), "-t", "25", "-vf", "select='lt(t,3)+gte(t,18)'",
"-fps_mode", "vfr", "-c:v", "libx264", "-preset", "ultrafast", "-bf", "2",
"-an", sparsePath.c_str(), nullptr};
assert(posix_spawnp(&child, sparseArgs[0], nullptr, nullptr, const_cast<char *const *>(sparseArgs), environ) == 0);
assert(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0);
Upstream sparse(readFile(sparsePath), readFile(audio)); sparse.start();
const auto sparseVideo = sparse.url("/source.ts"), sparseAudio = sparse.url("/audio.wav");
media = start({sparseVideo.c_str(), "X-Video: video-secret\r\n", sparseAudio.c_str(),
"X-Audio: audio-secret\r\n", false, false, false, 0}, url, duration);
const auto sparseBase = url.substr(0, url.rfind('/') + 1);
double audioEnd = NAN;
for (int id = 0; id < 5; ++id) {
const auto response = request(sparseBase + "segment/" + std::to_string(id) + ".ts");
const auto span = audioSpan(response, output);
if (id) assert(std::abs(span.first - audioEnd) < 1.0 / 48000);
audioEnd = span.second;
if (id == 0 || id >= 3) {
const auto rows = samples(output, 1);
const auto videos = std::count_if(rows.begin(), rows.end(), [](const auto &row) { return row.first.first == 1; });
assert(videos == (id == 0 ? 90 : id == 3 ? 180 : 30));
}
}
cast_media_destroy(media);
}
int main(int argc, char **argv) { int main(int argc, char **argv) {
bool liveCheck = false, seekCheck = false, hlsCheck = false, remuxCheck = false; bool liveCheck = false, seekCheck = false, hlsCheck = false, remuxCheck = false;
for (int argument = 1; argument < argc; ++argument) { for (int argument = 1; argument < argc; ++argument) {
@@ -680,6 +759,12 @@ int main(int argc, char **argv) {
if (std::string(argv[argument]) == "--seek-work") seekCheck = true; if (std::string(argv[argument]) == "--seek-work") seekCheck = true;
if (std::string(argv[argument]) == "--hls-work") hlsCheck = true; if (std::string(argv[argument]) == "--hls-work") hlsCheck = true;
if (std::string(argv[argument]) == "--remux-work") remuxCheck = true; if (std::string(argv[argument]) == "--remux-work") remuxCheck = true;
if (std::string(argv[argument]) == "--codec-work") {
char temp[] = "/tmp/terebi-codec-check.XXXXXX"; assert(mkdtemp(temp));
codecWork(temp); std::filesystem::remove_all(temp);
std::cout << "cast-media: sequential codec reuse, independent IDRs, AAC continuity and random seeks passed\n";
return 0;
}
} }
char temp[] = "/tmp/terebi-cast-check.XXXXXX"; assert(mkdtemp(temp)); std::filesystem::path directory(temp); char temp[] = "/tmp/terebi-cast-check.XXXXXX"; assert(mkdtemp(temp)); std::filesystem::path directory(temp);
if (remuxCheck) { if (remuxCheck) {
+1
View File
@@ -249,6 +249,7 @@ check-cast: $(BUILD_DIR)/cast-session-check $(BUILD_DIR)/cast-media-check $(BUIL
$(BUILD_DIR)/cast-session-check $(BUILD_DIR)/cast-session-check
$(BUILD_DIR)/cast-media-check --live $(BUILD_DIR)/cast-media-check --live
$(BUILD_DIR)/cast-media-check --remux-work $(BUILD_DIR)/cast-media-check --remux-work
$(BUILD_DIR)/cast-media-check --codec-work
$(BUILD_DIR)/cast-session-check --playback $(BUILD_DIR)/cast-playback-check $(BUILD_DIR)/fixtures/cache.mp4 $(BUILD_DIR)/cast-session-check --playback $(BUILD_DIR)/cast-playback-check $(BUILD_DIR)/fixtures/cache.mp4
$(BUILD_DIR)/app-startup-check.dylib: Checks/app-startup.swift $(SWIFT_SOURCES) $(OBJECTS) $(CONFIG_LIBRARY) Makefile | check-dependencies $(BUILD_DIR)/app-startup-check.dylib: Checks/app-startup.swift $(SWIFT_SOURCES) $(OBJECTS) $(CONFIG_LIBRARY) Makefile | check-dependencies
+93 -34
View File
@@ -342,9 +342,12 @@ struct OutputTrack {
int64_t audioPTS = AV_NOPTS_VALUE; int64_t audioPTS = AV_NOPTS_VALUE;
bool firstVideo = true; bool firstVideo = true;
double lastVideoTime = -INFINITY; double lastVideoTime = -INFINITY;
bool continuous; bool live, drained = false;
int64_t audioKeepFrom = AV_NOPTS_VALUE, audioKeepUntil = INT64_MAX; std::deque<Packet> packets;
OutputTrack(Track &track, Mux &output, bool live) : source(track), mux(&output), continuous(live) { 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 { try {
if (!source.stream) return; if (!source.stream) return;
stream = avformat_new_stream(mux->format, nullptr); require(stream != nullptr); stream = avformat_new_stream(mux->format, nullptr); require(stream != nullptr);
@@ -397,7 +400,6 @@ struct OutputTrack {
stream->time_base = encoder->time_base; stream->time_base = encoder->time_base;
check(avcodec_parameters_from_context(stream->codecpar, encoder)); check(avcodec_parameters_from_context(stream->codecpar, encoder));
} }
firstVideo = true;
} }
void writePacket(AVPacket *packet) { void writePacket(AVPacket *packet) {
// A shared, positive TS clock origin prevents per-segment AAC priming shifts. // A shared, positive TS clock origin prevents per-segment AAC priming shifts.
@@ -413,11 +415,17 @@ struct OutputTrack {
int result = avcodec_receive_packet(encoder, p.get()); int result = avcodec_receive_packet(encoder, p.get());
if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) break; if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) break;
check(result); check(result);
if (fifo && !continuous && audioKeepFrom != AV_NOPTS_VALUE && lastPacketPTS = p->pts;
(p->pts < audioKeepFrom || p->pts >= audioKeepUntil)) { if (!live) {
av_packet_unref(p.get()); 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; continue;
} }
if (p->pts < keepFrom) { av_packet_unref(p.get()); continue; }
}
av_packet_rescale_ts(p.get(), encoder->time_base, stream->time_base); av_packet_rescale_ts(p.get(), encoder->time_base, stream->time_base);
writePacket(p.get()); av_packet_unref(p.get()); writePacket(p.get()); av_packet_unref(p.get());
} }
@@ -463,12 +471,8 @@ struct OutputTrack {
} }
} else { } else {
require(f->sample_rate > 0 && f->nb_samples >= 0 && f->nb_samples <= 65536 && f->ch_layout.nb_channels <= 32); 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 begin = live ? start : aacPreroll(start);
const double limit = continuous ? end : double(aacBoundary(end)) / 48000; const double limit = end;
if (!continuous) {
audioKeepFrom = aacBoundary(start);
audioKeepUntil = int64_t(std::llround(end * 48000));
}
if (time + double(f->nb_samples) / f->sample_rate > begin && time < limit) { if (time + double(f->nb_samples) / f->sample_rate > begin && time < limit) {
if (!resample) { 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)); 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; 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)); 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 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); std::clamp(int(std::llround((begin - time) * 48000)), 0, count);
int take = continuous ? count - skip : int take = count - skip;
std::clamp(int(std::llround((limit - time) * 48000)) - skip, 0, count - skip);
if (audioPTS == AV_NOPTS_VALUE) audioPTS = int64_t(std::llround(std::max(begin, time) * 48000)); 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}; uint8_t *data[2] = {out->data[0] + skip * 4, out->data[1] + skip * 4};
require(av_audio_fifo_size(fifo) + take <= 131072); require(av_audio_fifo_size(fifo) + take <= 131072);
@@ -491,13 +494,52 @@ struct OutputTrack {
} }
source.nextFrame(); 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 (!encoder) return;
if (continuous) { if (live) {
if (fifo) audioFrames(false, end); if (fifo) audioFrames(false, end); else encoded(nullptr);
} else { return;
if (fifo) audioFrames(true); }
encoded(nullptr); // 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; std::atomic<bool> &cancel;
double duration = 0; double duration = 0;
std::vector<double> boundaries; std::vector<double> boundaries;
double nextStart = 0;
Pipeline(const Source &source, std::atomic<bool> &stop, double start) : live(source.live), cancel(stop) { 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); videoInput = std::make_unique<Input>(stop, source.video, source.videoHeaders, source.local, source.restricted, source.live);
duration = std::max(source.duration, videoInput->duration()); duration = std::max(source.duration, videoInput->duration());
@@ -567,8 +610,8 @@ struct Pipeline {
void armOperation() { void armOperation() {
deadline = Clock::now() + std::chrono::seconds(45); deadline = Clock::now() + std::chrono::seconds(45);
const auto limit = std::chrono::duration_cast<std::chrono::nanoseconds>(deadline.time_since_epoch()).count(); const auto limit = std::chrono::duration_cast<std::chrono::nanoseconds>(deadline.time_since_epoch()).count();
videoInput->operationDeadline = limit; videoInput->operationDeadline = limit; videoInput->arm();
if (audioInput) audioInput->operationDeadline = limit; if (audioInput) { audioInput->operationDeadline = limit; audioInput->arm(); }
} }
void releaseOperation() { void releaseOperation() {
videoInput->operationDeadline = std::numeric_limits<int64_t>::max(); videoInput->operationDeadline = std::numeric_limits<int64_t>::max();
@@ -578,8 +621,8 @@ struct Pipeline {
armOperation(); armOperation();
videoOutput.reset(); audioOutput.reset(); videoOutput.reset(); audioOutput.reset();
video.reset(); audio.reset(); video.reset(); audio.reset();
// Fresh codecs retain independent IDRs/AAC priming. One recorded network // Random access resets codec history; the network demuxer rewinds both
// demuxer atomically rewinds both packet lanes, seeking upstream only on misses. // packet lanes atomically, seeking upstream only on cache misses.
const double audioTarget = audioIndex >= 0 && !remuxAudio(*audioSource->format->streams[audioIndex]->codecpar) ? const double audioTarget = audioIndex >= 0 && !remuxAudio(*audioSource->format->streams[audioIndex]->codecpar) ?
aacPreroll(start) : NAN; aacPreroll(start) : NAN;
if (videoInput->buffered) if (videoInput->buffered)
@@ -587,13 +630,15 @@ struct Pipeline {
if (audioInput && audioInput->buffered) audioInput->seek(start, audioTarget); 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 (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); 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) { Segment segment(int64_t id, double start, double end) {
if (live) armOperation(); armOperation();
Mux mux; Mux mux;
if (video) { if (video) {
videoOutput.reset(); if (live) videoOutput.reset();
videoOutput = std::make_unique<OutputTrack>(*video, mux, false); if (videoOutput) videoOutput->beginSegment(mux);
else videoOutput = std::make_unique<OutputTrack>(*video, mux, live);
} }
if (audio) { if (audio) {
if (audioOutput) audioOutput->beginSegment(mux); if (audioOutput) audioOutput->beginSegment(mux);
@@ -604,6 +649,8 @@ struct Pipeline {
av_dict_set(&options, "mpegts_copyts", "1", 0); av_dict_set(&options, "mpegts_copyts", "1", 0);
av_dict_set(&options, "muxdelay", "0", 0); av_dict_set(&options, "muxdelay", "0", 0);
int result = avformat_write_header(mux.format, &options); av_dict_free(&options); check(result); 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; const double audioEnd = audio && !audio->remux && !live ? double(aacBoundary(end)) / 48000 : end;
for (;;) { for (;;) {
require(!cancel && Clock::now() < deadline); require(!cancel && Clock::now() < deadline);
@@ -613,12 +660,13 @@ struct Pipeline {
if (!std::isfinite(vt) && !std::isfinite(at)) break; if (!std::isfinite(vt) && !std::isfinite(at)) break;
if (vt <= at) videoOutput->consume(start, end); else audioOutput->consume(start, end); if (vt <= at) videoOutput->consume(start, end); else audioOutput->consume(start, end);
} }
if (videoOutput) videoOutput->finish(end); if (videoOutput) videoOutput->finish(start, end);
if (audioOutput) audioOutput->finish(end); if (audioOutput) audioOutput->finish(start, end);
check(av_write_trailer(mux.format)); avio_flush(mux.io); check(av_write_trailer(mux.format)); avio_flush(mux.io);
require(mux.io->error >= 0 && !mux.bytes.empty()); require(mux.io->error >= 0 && !mux.bytes.empty());
if (videoInput->buffered) videoInput->buffered->updatePosition(end); if (videoInput->buffered) videoInput->buffered->updatePosition(end);
if (audioInput && audioInput->buffered) audioInput->buffered->updatePosition(end); if (audioInput && audioInput->buffered) audioInput->buffered->updatePosition(end);
nextStart = end;
releaseOperation(); releaseOperation();
return {id, end - start, std::move(mux.bytes)}; return {id, end - start, std::move(mux.bytes)};
} }
@@ -740,7 +788,7 @@ struct CastMedia {
for (int64_t id = first; id < first + count; ++id) { 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); 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); 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"; if (id > first) text += "#EXT-X-DISCONTINUITY\n";
text += value; text += "segment/" + std::to_string(id) + ".ts\n"; text += value; text += "segment/" + std::to_string(id) + ".ts\n";
} }
@@ -761,8 +809,19 @@ struct CastMedia {
if (auto segment = cached()) return segment; if (auto segment = cached()) return segment;
try { try {
const double start = boundary(id), end = boundary(id + 1); const double start = boundary(id), end = boundary(id + 1);
if (vodPipeline) vodPipeline->reset(start, !boundaries.empty()); if (vodPipeline) {
else vodPipeline = std::make_unique<Pipeline>(source, cancelled, start); 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)); 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(); } { std::lock_guard guard(lock); segments.push_back(segment); while (segments.size() > windowSegments) segments.pop_front(); }
return segment; return segment;
+2 -1
View File
@@ -6,7 +6,8 @@
- [ ] evaluate playback optimization opportunities for YouTube and Miruro — measure benefits before changing behavior - [ ] evaluate playback optimization opportunities for YouTube and Miruro — measure benefits before changing behavior
- [x] coalesce superseded, not-yet-started seeks in the player and Cast controls; preserve accumulated relative skips and play/pause ordering - [x] coalesce superseded, not-yet-started seeks in the player and Cast controls; preserve accumulated relative skips and play/pause ordering
- [x] expand Cast H.264/AAC remux coverage with keyframe-aligned segments; preserve seek accuracy, timestamps and receiver compatibility - [x] expand Cast H.264/AAC remux coverage with keyframe-aligned segments; preserve seek accuracy, timestamps and receiver compatibility
- [ ] reuse decoder/encoder state during sequential Cast playback; reset on random seeks and preserve independent keyframes and AAC continuity - [x] reuse decoder/encoder state during sequential Cast playback; reset on random seeks and preserve independent keyframes and AAC continuity
- `.build/cast-media-check --codec-work`: 60s of 720p/30 B-frame video plus 44.1kHz PCM→AAC over loopback HTTP; three paired runs reduced median relay CPU from 3.56s to 2.58s (27%) and generation time from 4.97s to 4.43s (11%); physical-receiver playback still needs manual verification
- [ ] evaluate VideoToolbox hardware video decoding for native and Cast playback; retain software fallback for unsupported formats - [ ] evaluate VideoToolbox hardware video decoding for native and Cast playback; retain software fallback for unsupported formats
- [ ] benchmark cold start, cached/uncached seeks and sustained playback separately to prioritize the actual bottlenecks - [ ] benchmark cold start, cached/uncached seeks and sustained playback separately to prioritize the actual bottlenecks
- do not enable Miruro HTTP persistence without preserving origin/header checks, or increase caches/shorten Cast segments without measuring the memory/request/codec tradeoffs - do not enable Miruro HTTP persistence without preserving origin/header checks, or increase caches/shorten Cast segments without measuring the memory/request/codec tradeoffs