fix: share buffered Cast HLS input and select streams
This commit is contained in:
+111
-37
@@ -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)};
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user