fix: share buffered Cast HLS input and select streams

This commit is contained in:
2026-10-13 22:47:07 +02:00
parent b0636c5af7
commit ad4b2da5c8
6 changed files with 362 additions and 63 deletions
+111 -37
View File
@@ -1,5 +1,6 @@
#include "cast_media.h"
#include "network_policy.hpp"
#include "network_input.hpp"
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
@@ -84,8 +85,13 @@ struct Input {
std::atomic<bool> &cancel;
std::string origin, headers;
std::atomic<int64_t> deadline{0};
int64_t operationDeadline = std::numeric_limits<int64_t>::max();
std::atomic<int64_t> operationDeadline{std::numeric_limits<int64_t>::max()};
bool network, restricted, live;
std::atomic<uint64_t> epoch{0};
std::mutex waitLock;
std::condition_variable wake;
bool positioned = false;
std::unique_ptr<playback::NetworkInput> buffered;
Input(std::atomic<bool> &stop, const std::string &url, const std::string &fields, bool local, bool restricted, bool live) :
cancel(stop), headers(fields), network(!local), restricted(restricted), live(live) {
origin = local ? "" : playback::networkOrigin(url.c_str());
@@ -115,14 +121,21 @@ struct Input {
if (result >= 0) { arm(); result = avformat_find_stream_info(format, nullptr); }
if (result < 0) { avformat_close_input(&format); check(result); }
}
~Input() { avformat_close_input(&format); }
~Input() {
if (buffered) buffered->stop();
else avformat_close_input(&format);
}
void arm() {
const auto ioDeadline = std::chrono::duration_cast<std::chrono::nanoseconds>((Clock::now() + std::chrono::seconds(15)).time_since_epoch()).count();
deadline = std::min<int64_t>(ioDeadline, operationDeadline);
deadline = std::min<int64_t>(ioDeadline, operationDeadline.load());
}
bool interrupted() const {
const auto now = std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now().time_since_epoch()).count();
return cancel || now >= operationDeadline.load() || (!buffered && now >= deadline.load());
}
static int interrupt(void *opaque) {
auto &in = *static_cast<Input *>(opaque);
return in.cancel || std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now().time_since_epoch()).count() > in.deadline;
return in.interrupted() || (in.buffered && playback::NetworkInput::interrupt(in.buffered.get()));
}
static int openResource(AVFormatContext *context, AVIOContext **io, const char *url, int flags, AVDictionary **options) noexcept {
try {
@@ -141,10 +154,48 @@ struct Input {
}
double offset() const { return format->start_time == AV_NOPTS_VALUE ? 0 : double(format->start_time) / AV_TIME_BASE; }
double duration() const { return format->duration == AV_NOPTS_VALUE ? 0 : double(format->duration) / AV_TIME_BASE; }
void seek(double seconds) {
arm(); check(av_seek_frame(format, -1, int64_t((seconds + offset()) * AV_TIME_BASE), AVSEEK_FLAG_BACKWARD));
void select(std::array<int, 2> indices, playback::StreamCacheBudget &budget) {
for (unsigned i = 0; i < format->nb_streams; ++i)
format->streams[i]->discard = int(i) == indices[0] || int(i) == indices[1] ?
AVDISCARD_DEFAULT : AVDISCARD_ALL;
if (!network || live) return;
buffered = std::make_unique<playback::NetworkInput>(cancel, epoch, 0, wake, budget);
buffered->format = format;
buffered->start(indices, offset(), true);
}
void seek(double seconds, double audioTarget = NAN) {
const auto timestamp = int64_t((seconds + offset()) * AV_TIME_BASE);
if (buffered) {
// Probing retains the opening packets. Do not reopen their HLS media
// resource just to establish the initial zero-position cursors.
if (!positioned && seconds == 0) { positioned = true; return; }
positioned = true;
// A nearby forward request can outrun the asynchronous fetch frontier.
// Let the existing read complete rather than interrupting and refetching it.
while (buffered->readAheadPending(seconds, audioTarget)) {
require(!interrupted());
std::unique_lock lock(waitLock);
wake.wait_for(lock, std::chrono::milliseconds(20));
}
buffered->seek(-1, timestamp, audioTarget);
} else { arm(); check(av_seek_frame(format, -1, timestamp, AVSEEK_FLAG_BACKWARD)); }
}
int read(int slot, AVPacket *destination) {
if (!buffered) return av_read_frame(format, destination);
for (;;) {
require(!interrupted());
double end = 0;
int result = buffered->take(slot, destination, end);
if (result != 0) return result == 1 ? 0 : result;
std::unique_lock lock(waitLock);
wake.wait_for(lock, std::chrono::milliseconds(20));
}
}
};
bool remuxAudio(const AVCodecParameters &codec) {
return codec.codec_id == AV_CODEC_ID_AAC && codec.ch_layout.nb_channels <= 2 &&
(codec.profile == AV_PROFILE_AAC_LOW || codec.profile == AV_PROFILE_AAC_HE || codec.profile == AV_PROFILE_AAC_HE_V2);
}
struct Track {
Input *input;
AVStream *stream = nullptr;
@@ -153,9 +204,8 @@ struct Track {
Frame decoded;
bool eof = false, draining = false, remux = false;
double nextTime = 0;
Track(Input &in, AVMediaType type, double start, bool live) : input(&in) {
int index = av_find_best_stream(input->format, type, -1, -1, nullptr, 0);
if (index < 0) return;
Track(Input &in, int index, AVMediaType type, double start, bool live) : input(&in) {
require(index >= 0);
stream = input->format->streams[index];
remux = stream->codecpar->codec_id == (type == AVMEDIA_TYPE_VIDEO ? AV_CODEC_ID_H264 : AV_CODEC_ID_AAC);
if (type == AVMEDIA_TYPE_VIDEO) {
@@ -166,13 +216,12 @@ struct Track {
stream->codecpar->width <= 1920 && stream->codecpar->height <= 1080 &&
rate.num > 0 && av_q2d(rate) <= 30.001;
} else {
remux = remux && stream->codecpar->ch_layout.nb_channels <= 2 &&
(stream->codecpar->profile == AV_PROFILE_AAC_LOW || stream->codecpar->profile == AV_PROFILE_AAC_HE || stream->codecpar->profile == AV_PROFILE_AAC_HE_V2);
remux = remuxAudio(*stream->codecpar);
}
const double seekTarget = type == AVMEDIA_TYPE_AUDIO && !remux ?
std::max(0.0, aacPreroll(start) - double(std::max(0, stream->codecpar->seek_preroll)) /
std::max(1, stream->codecpar->sample_rate)) : start;
if (!live) input->seek(seekTarget);
if (!live && !input->buffered) input->seek(seekTarget);
nextPacket();
// Every VOD segment must start at its advertised time and on an IDR. If the
// source GOP doesn't align, decode preroll and use VideoToolbox, not overlap.
@@ -212,8 +261,8 @@ struct Track {
auto p = packet();
input->arm();
for (;;) {
require(!Input::interrupt(input));
int result = av_read_frame(input->format, p.get());
require(!input->interrupted());
int result = input->read(stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO ? 0 : 1, p.get());
if (result == AVERROR_EOF) { eof = true; return; }
check(result);
require(p->size >= 0 && size_t(p->size) <= segmentLimit);
@@ -229,7 +278,7 @@ struct Track {
void nextFrame() {
decoded.reset(); auto f = frame();
for (;;) {
require(!Input::interrupt(input));
require(!input->interrupted());
int result = avcodec_receive_frame(decoder, f.get());
if (result == 0) {
decoded = std::move(f);
@@ -440,44 +489,70 @@ struct OutputTrack {
}
};
struct Pipeline {
playback::StreamCacheBudget cacheBudget;
std::unique_ptr<Input> videoInput, audioInput;
Input *audioSource = nullptr;
int videoIndex = -1, audioIndex = -1;
Clock::time_point deadline;
std::unique_ptr<Track> video, audio;
std::unique_ptr<OutputTrack> videoOutput, audioOutput;
bool live;
std::atomic<bool> &cancel;
double duration = 0;
Pipeline(const Source &source, std::atomic<bool> &stop, double start) : live(source.live), cancel(stop) {
auto primary = std::make_unique<Input>(stop, source.video, source.videoHeaders, source.local, source.restricted, source.live);
duration = std::max(source.duration, primary->duration());
bool hasVideo = av_find_best_stream(primary->format, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0) >= 0;
bool hasAudio = av_find_best_stream(primary->format, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0) >= 0;
if (hasVideo) videoInput = std::move(primary);
videoInput = std::make_unique<Input>(stop, source.video, source.videoHeaders, source.local, source.restricted, source.live);
duration = std::max(source.duration, videoInput->duration());
videoIndex = av_find_best_stream(videoInput->format, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (!source.audio.empty()) {
require(videoIndex >= 0);
audioInput = std::make_unique<Input>(stop, source.audio, source.audioHeaders, false, source.restricted, source.live);
} else if (hasAudio) {
if (!primary) primary = std::make_unique<Input>(stop, source.video, source.videoHeaders, source.local, source.restricted, source.live);
audioInput = std::move(primary);
audioSource = audioInput.get();
audioIndex = av_find_best_stream(audioSource->format, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
} else {
audioIndex = av_find_best_stream(videoInput->format, AVMEDIA_TYPE_AUDIO, -1, videoIndex, nullptr, 0);
if (audioIndex >= 0) {
if (videoIndex >= 0 && (source.local || source.live)) {
audioInput = std::make_unique<Input>(stop, source.video, source.videoHeaders, source.local, source.restricted, source.live);
audioIndex = av_find_best_stream(audioInput->format, AVMEDIA_TYPE_AUDIO, -1, videoIndex, nullptr, 0);
audioSource = audioInput.get();
} else audioSource = videoInput.get();
}
}
require(videoIndex >= 0 || audioIndex >= 0);
videoInput->select({audioSource == videoInput.get() ? audioIndex : -1, videoIndex}, cacheBudget);
if (audioInput) audioInput->select({audioIndex, -1}, cacheBudget);
reset(start);
if (!source.audio.empty()) require(audio && audio->stream);
require(video || audio);
if (!source.live) require(std::isfinite(duration) && duration > 0 && duration <= 86400 * 14);
releaseOperation();
}
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;
}
void releaseOperation() {
videoInput->operationDeadline = std::numeric_limits<int64_t>::max();
if (audioInput) audioInput->operationDeadline = std::numeric_limits<int64_t>::max();
}
void reset(double start) {
armOperation();
videoOutput.reset(); audioOutput.reset();
video.reset(); audio.reset();
// Fresh codecs preserve independent segment/AAC priming; demuxers retain
// their already-probed metadata and seek indexes across random requests.
if (videoInput) {
videoInput->operationDeadline = std::numeric_limits<int64_t>::max();
video = std::make_unique<Track>(*videoInput, AVMEDIA_TYPE_VIDEO, start, live);
}
if (audioInput) {
audioInput->operationDeadline = std::numeric_limits<int64_t>::max();
audio = std::make_unique<Track>(*audioInput, AVMEDIA_TYPE_AUDIO, start, live);
}
// Fresh codecs retain independent IDRs/AAC priming. One recorded network
// demuxer atomically rewinds both packet lanes, seeking upstream only on misses.
const double audioTarget = audioIndex >= 0 && !remuxAudio(*audioSource->format->streams[audioIndex]->codecpar) ?
aacPreroll(start) : start;
if (videoInput->buffered)
videoInput->seek(start, audioSource == videoInput.get() ? audioTarget : NAN);
if (audioInput && audioInput->buffered) audioInput->seek(start, audioTarget);
if (videoIndex >= 0) video = std::make_unique<Track>(*videoInput, videoIndex, AVMEDIA_TYPE_VIDEO, start, live);
if (audioIndex >= 0) audio = std::make_unique<Track>(*audioSource, audioIndex, AVMEDIA_TYPE_AUDIO, start, live);
}
Segment segment(int64_t id, double start, double end) {
if (live) armOperation();
Mux mux;
if (video) {
videoOutput.reset();
@@ -492,10 +567,6 @@ 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);
const auto deadline = Clock::now() + std::chrono::seconds(45);
const auto limit = std::chrono::duration_cast<std::chrono::nanoseconds>(deadline.time_since_epoch()).count();
if (video) video->input->operationDeadline = limit;
if (audio) audio->input->operationDeadline = limit;
const double audioEnd = audio && !audio->remux && !live ? double(aacBoundary(end)) / 48000 : end;
for (;;) {
require(!cancel && Clock::now() < deadline);
@@ -509,6 +580,9 @@ struct Pipeline {
if (audioOutput) audioOutput->finish(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);
releaseOperation();
return {id, end - start, std::move(mux.bytes)};
}
};