fix: separate Cast launch readiness, preserve volume intent and reuse seek inputs

This commit is contained in:
2026-10-13 21:40:19 +02:00
parent 697158aa02
commit b9e63cd760
8 changed files with 365 additions and 55 deletions
+75 -3
View File
@@ -25,6 +25,10 @@ extern "C" {
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <spawn.h>
#include <sys/wait.h>
extern char **environ;
static void ok(int result) { assert(result >= 0); }
static std::vector<uint8_t> readFile(const std::string &path) {
@@ -99,6 +103,20 @@ static void fixture(const std::string &path) {
av_bsf_free(&aspect);
avio_closep(&format->pb); avformat_free_context(format); avcodec_free_context(&video); avcodec_free_context(&audio);
}
// Indexed split MP4 is the resolved YouTube VOD shape. As with check-fixtures,
// the check requires host ffmpeg for MP4 muxing; production uses only bundled libraries.
static void splitFixture(const std::string &source, const std::string &path, AVMediaType type) {
const char *arguments[] = {"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
"-i", source.c_str(), "-map", type == AVMEDIA_TYPE_VIDEO ? "0:v:0" : "0:a:0",
"-c", "copy", "-movflags", "+faststart", path.c_str(), nullptr};
pid_t child = 0;
int result = posix_spawnp(&child, arguments[0], nullptr, nullptr,
const_cast<char *const *>(arguments), environ);
if (result != 0) std::cerr << "cast-media split fixture requires host ffmpeg (same as make check-fixtures)\n";
assert(result == 0);
int status = 0; assert(waitpid(child, &status, 0) == child);
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
static std::vector<uint8_t> wav() {
std::vector<uint8_t> bytes(44 + 90 * 48000 * 2);
auto put = [&](size_t at, uint32_t value, int length) { for (int i = 0; i < length; ++i) bytes[at + i] = uint8_t(value >> (8 * i)); };
@@ -128,6 +146,7 @@ static HTTP request(const std::string &url, const std::string &method = "GET", c
struct Upstream {
int listener; uint16_t port; std::thread threads[4]; std::atomic<bool> stopped{false};
std::atomic<int> foreignRequests{0}, videoRequests{0}, audioRequests{0};
std::atomic<int> videoStarts{0}, audioStarts{0};
std::map<std::string, std::vector<uint8_t>> resources;
std::string foreign;
explicit Upstream(std::vector<uint8_t> video, std::vector<uint8_t> audio) {
@@ -158,7 +177,7 @@ struct Upstream {
(path == "/public-cross.m3u8" ? "/public.ts" : "/source.ts") + "\n#EXT-X-ENDLIST\n";
data.assign(playlist.begin(), playlist.end());
} else if (auto resource = resources.find(path); resource != resources.end()) {
bool audio = path == "/audio.wav";
bool audio = path == "/audio.wav" || path == "/audio.m4a";
bool publicMedia = path == "/public.ts";
bool authorized = publicMedia || request.find(audio ? "X-Audio: audio-secret" : "X-Video: video-secret") != std::string::npos;
bool wrong = request.find(publicMedia ? "secret" : (audio ? "video-secret" : "audio-secret")) != std::string::npos;
@@ -173,6 +192,9 @@ struct Upstream {
if (low >= high) { status = 416; low = high = 0; }
else { status = 206; extra += "Content-Range: bytes " + std::to_string(low) + "-" + std::to_string(high - 1) + "/" + std::to_string(data.size()) + "\r\n"; }
}
if ((status == 200 || status == 206) && low == 0 && resources.contains(path)) {
if (path == "/audio.wav" || path == "/audio.m4a") ++audioStarts; else ++videoStarts;
}
std::string header = "HTTP/1.1 " + std::to_string(status) + " Test\r\nConnection: close\r\nAccept-Ranges: bytes\r\n" + extra + "Content-Length: " + std::to_string(high - low) + "\r\n\r\n";
send(client, header.data(), header.size(), 0);
if (!request.starts_with("HEAD")) while (high > low) { auto n = send(client, data.data() + low, high - low, 0); if (n <= 0) break; low += size_t(n); }
@@ -244,15 +266,65 @@ static CastMedia *start(CastMediaSource source, std::string &url, double &durati
char error[512]{}, bytes[512]{}; auto *media = cast_media_create(&source, error); assert(media);
assert(cast_media_start(media, "127.0.0.1", bytes, &duration, error)); url = bytes; return media;
}
static void seekWork(Upstream &upstream, const std::string &segment) {
std::string video = upstream.url("/video.mp4"), audio = upstream.url("/audio.m4a"), url;
double duration = 0;
CastMediaSource source{video.c_str(), "X-Video: video-secret\r\n", audio.c_str(), "X-Audio: audio-secret\r\n", false, false, false, 90};
auto *media = start(source, url, duration);
std::string base = url.substr(0, url.rfind('/') + 1);
int videoStarts = upstream.videoStarts, audioStarts = upstream.audioStarts;
auto distant = request(base + "segment/10.ts");
playable(distant, segment, true, 60);
int videoRequests = upstream.videoRequests, audioRequests = upstream.audioRequests;
auto repeated = request(base + "segment/10.ts");
std::cout << "seek-work repeated upstream requests: video=" << int(upstream.videoRequests) - videoRequests
<< " audio=" << int(upstream.audioRequests) - audioRequests << std::endl;
bool repeatedHasNoIO = upstream.videoRequests == videoRequests && upstream.audioRequests == audioRequests;
bool repeatedIdentical = repeated.status == 200 && repeated.bytes == distant.bytes;
auto adjacent = request(base + "segment/11.ts");
playable(adjacent, segment, true, 66);
auto before = audioSpan(distant, segment), after = audioSpan(adjacent, segment);
assert(std::abs(after.first - before.second) < 1.0 / 48000);
playable(request(base + "segment/2.ts"), segment, true, 12);
playable(request(base + "segment/12.ts"), segment, true, 72);
std::cout << "seek-work new origin-zero requests across distant/adjacent seeks: video="
<< int(upstream.videoStarts) - videoStarts << " audio=" << int(upstream.audioStarts) - audioStarts << std::endl;
bool seeksDoNotReprobe = upstream.videoStarts == videoStarts && upstream.audioStarts == audioStarts;
// A previous random segment remains independently replayable without upstream IO.
videoRequests = upstream.videoRequests; audioRequests = upstream.audioRequests;
bool replayIdentical = request(base + "segment/10.ts").bytes == distant.bytes;
auto head = request(base + "segment/10.ts", "HEAD");
auto range = request(base + "segment/10.ts", "GET", "Range: bytes=0-187\r\n");
assert(head.status == 200 && head.bytes.empty());
assert(range.status == 206 && range.bytes == std::vector<uint8_t>(distant.bytes.begin(), distant.bytes.begin() + 188));
bool replayHasNoIO = upstream.videoRequests == videoRequests && upstream.audioRequests == audioRequests;
cast_media_destroy(media);
assert(seeksDoNotReprobe);
assert(repeatedHasNoIO && repeatedIdentical && replayHasNoIO && replayIdentical);
}
int main(int argc, char **argv) {
bool liveCheck = false;
for (int argument = 1; argument < argc; ++argument) if (std::string(argv[argument]) == "--live") liveCheck = true;
bool liveCheck = false, seekCheck = 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;
}
char temp[] = "/tmp/terebi-cast-check.XXXXXX"; assert(mkdtemp(temp)); std::filesystem::path directory(temp);
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();
splitFixture(video, splitVideo, AVMEDIA_TYPE_VIDEO);
splitFixture(video, splitAudio, AVMEDIA_TYPE_AUDIO);
auto videoBytes = readFile(video), audioBytes = readFile(audio);
Upstream upstream(videoBytes, audioBytes), foreign(videoBytes, audioBytes);
upstream.resources["/video.mp4"] = readFile(splitVideo);
upstream.resources["/audio.m4a"] = readFile(splitAudio);
upstream.foreign = foreign.url(""); upstream.start(); foreign.start();
seekWork(upstream, segment);
if (seekCheck) {
std::filesystem::remove_all(directory);
std::cout << "cast-media: split VOD seek work passed\n";
return 0;
}
std::string url; double duration = 0;
CastMediaSource local{video.c_str(), "", nullptr, nullptr, true, false, false, 0};
auto *media = start(local, url, duration); assert(duration >= 89 && duration <= 92);