perf: remux indexed Cast video at keyframe boundaries

This commit is contained in:
2026-10-17 00:28:10 +02:00
parent 880b596765
commit acb69fbe8a
5 changed files with 299 additions and 35 deletions
+198 -2
View File
@@ -43,7 +43,7 @@ static void writeFile(const std::string &path, const std::vector<uint8_t> &bytes
std::ofstream file(path, std::ios::binary); file.write(reinterpret_cast<const char *>(bytes.data()), bytes.size()); assert(file.good());
}
// Real source A/V built with the same bundled encoders; no PATH ffmpeg or fixtures.
static void fixture(const std::string &path, int seconds = 90, int width = 96, int height = 64, int bitrate = 300000) {
static void fixture(const std::string &path, int seconds = 90, int width = 96, int height = 64, int bitrate = 300000, bool compatible = false) {
AVFormatContext *format = nullptr; ok(avformat_alloc_output_context2(&format, nullptr, "mpegts", path.c_str()));
AVCodecContext *video = avcodec_alloc_context3(avcodec_find_encoder_by_name("h264_videotoolbox"));
AVCodecContext *audio = avcodec_alloc_context3(avcodec_find_encoder(AV_CODEC_ID_AAC)); assert(video && audio);
@@ -51,6 +51,10 @@ static void fixture(const std::string &path, int seconds = 90, int width = 96, i
video->time_base = {1, 10}; video->framerate = {10, 1}; video->bit_rate = bitrate; video->gop_size = 70; video->max_b_frames = 0;
video->sample_aspect_ratio = {4, 3};
av_opt_set(video->priv_data, "allow_sw", "1", 0); av_opt_set(video->priv_data, "realtime", "1", 0);
if (compatible) {
ok(av_opt_set(video->priv_data, "profile", "main", 0));
ok(av_opt_set(video->priv_data, "level", "4.1", 0));
}
audio->sample_rate = 48000; audio->sample_fmt = AV_SAMPLE_FMT_FLTP; audio->time_base = {1, 48000}; audio->bit_rate = 128000;
av_channel_layout_default(&audio->ch_layout, 2);
ok(avcodec_open2(video, video->codec, nullptr)); ok(avcodec_open2(audio, audio->codec, nullptr));
@@ -484,14 +488,206 @@ static void hlsWork(Upstream &upstream, const std::string &segment) {
}
assert(upstream.foreignRequests == 0);
}
struct CompressedSample {
int64_t dts;
std::vector<uint8_t> bytes;
bool operator==(const CompressedSample &) const = default;
};
using Samples = std::map<std::pair<int, int64_t>, CompressedSample>;
// Compare actual AAC payloads and H.264 slices, excluding container-inserted
// AUD/parameter sets. Also decode each TS independently with strict errors.
static Samples samples(const std::string &path, double origin = NAN) {
AVFormatContext *format = nullptr;
ok(avformat_open_input(&format, path.c_str(), nullptr, nullptr)); ok(avformat_find_stream_info(format, nullptr));
if (std::isnan(origin)) origin = double(format->start_time) / AV_TIME_BASE;
Samples result;
std::map<int, AVCodecContext *> decoders;
std::map<int, int64_t> lastDTS;
std::map<int, int> decoded;
auto *packet = av_packet_alloc(); auto *frame = av_frame_alloc(); assert(packet && frame);
while (av_read_frame(format, packet) >= 0) {
auto *stream = format->streams[packet->stream_index];
const bool video = stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO;
assert(stream->codecpar->codec_id == (video ? AV_CODEC_ID_H264 : AV_CODEC_ID_AAC));
assert(packet->pts != AV_NOPTS_VALUE && packet->dts != AV_NOPTS_VALUE);
const int64_t pts = std::llround((packet->pts * av_q2d(stream->time_base) - origin) * 90000);
const int64_t dts = std::llround((packet->dts * av_q2d(stream->time_base) - origin) * 90000);
if (lastDTS.contains(video)) assert(dts > lastDTS[video]);
lastDTS[video] = dts;
std::vector<uint8_t> payload;
if (video) {
bool idr = false;
for (int at = 0; at + 3 < packet->size;) {
if (packet->data[at] || packet->data[at + 1] || packet->data[at + 2] != 1) { ++at; continue; }
const int begin = at + 3, type = packet->data[begin] & 31;
at = begin + 1;
while (at + 3 < packet->size &&
(packet->data[at] || packet->data[at + 1] || packet->data[at + 2] != 1)) ++at;
int end = at + 3 < packet->size ? at : packet->size;
while (end > begin && packet->data[end - 1] == 0) --end;
if (type == 1 || type == 5) payload.insert(payload.end(), packet->data + begin, packet->data + end);
idr = idr || type == 5;
}
if (!decoders.contains(video)) assert((packet->flags & AV_PKT_FLAG_KEY) && idr);
assert(!payload.empty());
} else payload.assign(packet->data, packet->data + packet->size);
assert(result.emplace(std::pair{int(video), pts}, CompressedSample{dts, std::move(payload)}).second);
if (!decoders.contains(video)) {
auto *decoder = avcodec_alloc_context3(avcodec_find_decoder(stream->codecpar->codec_id)); assert(decoder);
ok(avcodec_parameters_to_context(decoder, stream->codecpar));
decoder->err_recognition = AV_EF_EXPLODE;
ok(avcodec_open2(decoder, decoder->codec, nullptr)); decoders[video] = decoder;
}
ok(avcodec_send_packet(decoders[video], packet));
while (avcodec_receive_frame(decoders[video], frame) >= 0) { ++decoded[video]; av_frame_unref(frame); }
av_packet_unref(packet);
}
for (auto &[type, decoder] : decoders) {
ok(avcodec_send_packet(decoder, nullptr));
while (avcodec_receive_frame(decoder, frame) >= 0) { ++decoded[type]; av_frame_unref(frame); }
assert(decoded[type] == int(std::count_if(result.begin(), result.end(), [&](const auto &row) { return row.first.first == type; })));
avcodec_free_context(&decoder);
}
assert(decoded[0] > 0 && decoded[1] > 0);
av_packet_free(&packet); av_frame_free(&frame); avformat_close_input(&format);
return result;
}
// Indexed, seven-second GOPs used to transcode at every six-second boundary.
static void remuxWork(const std::filesystem::path &directory) {
const auto source = (directory / "remux.ts").string(), indexed = (directory / "remux.mp4").string();
fixture(source, 28, 1280, 720, 3000000, true);
// Keep audio with the indexed video so the check covers both packet lanes.
const char *arguments[] = {"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
"-i", source.c_str(), "-map", "0", "-c", "copy", "-movflags", "+faststart", indexed.c_str(), nullptr};
pid_t child = 0;
assert(posix_spawnp(&child, arguments[0], nullptr, nullptr, const_cast<char *const *>(arguments), environ) == 0);
int status = 0; assert(waitpid(child, &status, 0) == child);
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
std::string url; double duration = 0;
auto began = std::chrono::steady_clock::now();
auto *media = start({indexed.c_str(), "", nullptr, nullptr, true, false, false, 0}, url, duration);
const auto prepared = std::chrono::steady_clock::now();
auto manifest = request(url); assert(manifest.status == 200);
std::string text(manifest.bytes.begin(), manifest.bytes.end()), base = url.substr(0, url.rfind('/') + 1);
std::vector<double> boundaries{0};
for (size_t at = 0; (at = text.find("#EXTINF:", at)) != std::string::npos; ++at)
boundaries.push_back(boundaries.back() + std::stod(text.substr(at + 8)));
size_t bytes = 0;
std::vector<HTTP> outputs;
for (size_t id = 0; id + 1 < boundaries.size(); ++id) {
auto response = request(base + "segment/" + std::to_string(id) + ".ts");
assert(response.status == 200);
bytes += response.bytes.size(); outputs.push_back(std::move(response));
}
const auto finished = std::chrono::steady_clock::now();
double longest = 0;
for (size_t i = 1; i < boundaries.size(); ++i) longest = std::max(longest, boundaries[i] - boundaries[i - 1]);
std::cout << "remux-work prepare_ms=" << std::chrono::duration<double, std::milli>(prepared - began).count()
<< " generate_ms=" << std::chrono::duration<double, std::milli>(finished - prepared).count()
<< " segments=" << outputs.size() << " bytes=" << bytes
<< " max_seconds=" << longest << std::endl;
assert(request(base + "segment/" + std::to_string(outputs.size()) + ".ts").status == 404);
assert(boundaries.size() == 5);
for (size_t i = 0; i < boundaries.size(); ++i) assert(std::abs(boundaries[i] - i * 7.0) < .1);
assert(text.find("#EXT-X-TARGETDURATION:8\n") != std::string::npos);
Samples delivered;
for (size_t id = 0; id < outputs.size(); ++id) {
const auto path = (directory / "output.ts").string(); writeFile(path, outputs[id].bytes);
auto rows = samples(path, 1);
for (auto &[key, value] : rows) {
assert(double(key.second) / 90000 >= boundaries[id] - .00002);
assert(double(key.second) / 90000 < boundaries[id + 1] + .00002);
assert(delivered.emplace(key, std::move(value)).second);
}
}
assert(delivered == samples(source));
cast_media_destroy(media);
const auto video = (directory / "indexed-video.mp4").string(), audio = (directory / "indexed-audio.m4a").string();
splitFixture(source, video, AVMEDIA_TYPE_VIDEO); splitFixture(source, audio, AVMEDIA_TYPE_AUDIO);
Upstream upstream(readFile(indexed), wav());
upstream.resources["/video.mp4"] = readFile(video); upstream.resources["/audio.m4a"] = readFile(audio);
upstream.start();
for (bool split : {false, true}) {
const auto videoURL = upstream.url(split ? "/video.mp4" : "/source.ts"), audioURL = upstream.url("/audio.m4a");
media = start({videoURL.c_str(), "X-Video: video-secret\r\n", split ? audioURL.c_str() : nullptr,
split ? "X-Audio: audio-secret\r\n" : nullptr, false, false, false, 0}, url, duration);
base = url.substr(0, url.rfind('/') + 1);
manifest = request(url); assert(manifest.status == 200);
text.assign(manifest.bytes.begin(), manifest.bytes.end());
std::vector<double> networkBoundaries{0};
for (size_t at = 0; (at = text.find("#EXTINF:", at)) != std::string::npos; ++at)
networkBoundaries.push_back(networkBoundaries.back() + std::stod(text.substr(at + 8)));
const int64_t videoOrigin = split ? delivered.lower_bound({1, INT64_MIN})->first.second : 0;
for (int id : {3, 0, 2, 1}) {
auto response = request(base + "segment/" + std::to_string(id) + ".ts");
assert(response.status == 200);
const auto path = (directory / "network-output.ts").string(); writeFile(path, response.bytes);
auto rows = samples(path, 1);
Samples expected;
for (const auto &[key, value] : delivered) {
const int64_t shift = key.first ? videoOrigin : 0;
const auto adjusted = std::pair{key.first, key.second - shift};
if (adjusted.second / 90000.0 >= networkBoundaries[id] - .00002 &&
adjusted.second / 90000.0 < networkBoundaries[id + 1] - .00002)
expected.emplace(adjusted, CompressedSample{value.dts - shift, value.bytes});
}
if (rows != expected)
std::cerr << "remux-work split=" << split << " id=" << id << " actual=" << rows.size()
<< " expected=" << expected.size() << std::endl;
assert(rows == expected);
assert(request(base + "segment/" + std::to_string(id) + ".ts").bytes == response.bytes);
}
assert(upstream.foreignRequests == 0);
cast_media_destroy(media);
}
// AAC transcoding must stay continuous across non-six-second boundaries.
const auto videoURL = upstream.url("/video.mp4"), audioURL = upstream.url("/audio.wav");
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);
base = url.substr(0, url.rfind('/') + 1);
auto before = audioSpan(request(base + "segment/0.ts"), (directory / "audio-output.ts").string());
auto after = audioSpan(request(base + "segment/1.ts"), (directory / "audio-output.ts").string());
assert(std::abs(after.first - before.second) < 1.0 / 48000);
cast_media_destroy(media);
// Reordered video and GOPs beyond the duration ceiling keep the fixed-grid
// fallback, including random-access independently decodable output.
for (bool reordered : {true, false}) {
const auto fallback = (directory / "fallback.mp4").string();
const char *args[] = {"ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", source.c_str(),
"-c:v", "libx264", "-profile:v", "main", "-level:v", "4.1",
"-bf", reordered ? "2" : "0", "-g", reordered ? "70" : "140",
"-keyint_min", reordered ? "70" : "140", "-sc_threshold", "0",
"-c:a", "copy", "-movflags", "+faststart", fallback.c_str(), nullptr};
assert(posix_spawnp(&child, args[0], nullptr, nullptr, const_cast<char *const *>(args), environ) == 0);
assert(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0);
media = start({fallback.c_str(), "", nullptr, nullptr, true, false, false, 0}, url, duration);
manifest = request(url); text.assign(manifest.bytes.begin(), manifest.bytes.end());
assert(text.find("#EXT-X-TARGETDURATION:6\n") != std::string::npos);
base = url.substr(0, url.rfind('/') + 1);
auto response = request(base + "segment/3.ts"); assert(response.status == 200);
const auto path = (directory / "fallback-output.ts").string(); writeFile(path, response.bytes);
const auto rows = samples(path, 1);
for (const auto &row : rows) assert(row.first.second >= 18 * 90000 && row.first.second < 24 * 90000);
cast_media_destroy(media);
}
}
int main(int argc, char **argv) {
bool liveCheck = false, seekCheck = false, hlsCheck = false;
bool liveCheck = false, seekCheck = false, hlsCheck = false, remuxCheck = false;
for (int argument = 1; argument < argc; ++argument) {
if (std::string(argv[argument]) == "--live") liveCheck = true;
if (std::string(argv[argument]) == "--seek-work") seekCheck = true;
if (std::string(argv[argument]) == "--hls-work") hlsCheck = true;
if (std::string(argv[argument]) == "--remux-work") remuxCheck = true;
}
char temp[] = "/tmp/terebi-cast-check.XXXXXX"; assert(mkdtemp(temp)); std::filesystem::path directory(temp);
if (remuxCheck) {
remuxWork(directory);
std::filesystem::remove_all(directory);
std::cout << "cast-media: keyframe-aligned indexed remux passed\n";
return 0;
}
const auto video = (directory / "source.ts").string(), audio = (directory / "source.wav").string(), segment = (directory / "segment.ts").string();
fixture(video); writeFile(audio, wav());
const auto splitVideo = (directory / "video.mp4").string(), splitAudio = (directory / "audio.m4a").string();