perf: remux indexed Cast video at keyframe boundaries
This commit is contained in:
+80
-31
@@ -164,7 +164,9 @@ struct Input {
|
||||
buffered->start(indices, offset(), true);
|
||||
}
|
||||
void seek(double seconds, double audioTarget = NAN) {
|
||||
const auto timestamp = int64_t((seconds + offset()) * AV_TIME_BASE);
|
||||
// Round forward so a sub-microsecond truncation cannot select the
|
||||
// previous GOP when a playlist boundary is an exact stream timestamp.
|
||||
const auto timestamp = int64_t(std::ceil((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.
|
||||
@@ -196,6 +198,15 @@ 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);
|
||||
}
|
||||
bool remuxVideo(Input &input, AVStream &stream) {
|
||||
const auto &codec = *stream.codecpar;
|
||||
const int profile = codec.profile & ~AV_PROFILE_H264_CONSTRAINED;
|
||||
const auto rate = av_guess_frame_rate(input.format, &stream, nullptr);
|
||||
return codec.codec_id == AV_CODEC_ID_H264 && codec.video_delay == 0 &&
|
||||
(profile == AV_PROFILE_H264_BASELINE || profile == AV_PROFILE_H264_MAIN || profile == AV_PROFILE_H264_HIGH) &&
|
||||
codec.level > 0 && codec.level <= 41 && codec.width > 0 && codec.width <= 1920 &&
|
||||
codec.height > 0 && codec.height <= 1080 && rate.num > 0 && av_q2d(rate) <= 30.001;
|
||||
}
|
||||
struct Track {
|
||||
Input *input;
|
||||
AVStream *stream = nullptr;
|
||||
@@ -204,31 +215,33 @@ struct Track {
|
||||
Frame decoded;
|
||||
bool eof = false, draining = false, remux = false;
|
||||
double nextTime = 0;
|
||||
Track(Input &in, int index, AVMediaType type, double start, bool live) : input(&in) {
|
||||
Track(Input &in, int index, AVMediaType type, double start, bool live, bool aligned = false) : 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) {
|
||||
const int profile = stream->codecpar->profile & ~AV_PROFILE_H264_CONSTRAINED;
|
||||
auto rate = av_guess_frame_rate(input->format, stream, nullptr);
|
||||
remux = remux && (profile == AV_PROFILE_H264_BASELINE || profile == AV_PROFILE_H264_MAIN || profile == AV_PROFILE_H264_HIGH) &&
|
||||
stream->codecpar->level > 0 && stream->codecpar->level <= 41 &&
|
||||
stream->codecpar->width <= 1920 && stream->codecpar->height <= 1080 &&
|
||||
rate.num > 0 && av_q2d(rate) <= 30.001;
|
||||
} else {
|
||||
remux = remuxAudio(*stream->codecpar);
|
||||
}
|
||||
remux = type == AVMEDIA_TYPE_VIDEO ? remuxVideo(in, *stream) : 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->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.
|
||||
if (type == AVMEDIA_TYPE_VIDEO)
|
||||
remux = remux && !live && stream->codecpar->video_delay == 0 && pending &&
|
||||
(pending->flags & AV_PKT_FLAG_KEY) && hasIDR(*pending, *stream->codecpar) &&
|
||||
std::abs(packetTime() - start) < .001;
|
||||
// Indexed VOD can advertise source GOP boundaries; every actual start
|
||||
// still has to be an IDR. The opening frame may follow AAC priming.
|
||||
if (type == AVMEDIA_TYPE_VIDEO) {
|
||||
// Cached seeks can return the preceding GOP while read-ahead has
|
||||
// reached this boundary but not yet delivered its IDR packet.
|
||||
if (aligned && remux)
|
||||
while (pending && packetTime() < start - .001) nextPacket();
|
||||
const auto rate = av_guess_frame_rate(input->format, stream, nullptr);
|
||||
const double openingGap = rate.num > 0 ? av_q2d(av_inv_q(rate)) : 0;
|
||||
remux = remux && !live && pending && (pending->flags & AV_PKT_FLAG_KEY) &&
|
||||
hasIDR(*pending, *stream->codecpar) &&
|
||||
(std::abs(packetTime() - start) < .001 ||
|
||||
(start == 0 && packetTime() >= 0 && packetTime() <= openingGap));
|
||||
if (aligned && !remux) {
|
||||
// A keyframe index is not proof of an IDR; recover full decoder preroll.
|
||||
input->seek(0); eof = false; nextPacket();
|
||||
}
|
||||
}
|
||||
if (!live && type == AVMEDIA_TYPE_VIDEO && pending && (pending->flags & AV_PKT_FLAG_KEY) &&
|
||||
!hasIDR(*pending, *stream->codecpar) && stream->codecpar->codec_id == AV_CODEC_ID_H264) {
|
||||
// ponytail: rare intra-refresh seeks decode from the beginning; index real IDRs if this becomes slow.
|
||||
@@ -499,6 +512,7 @@ struct Pipeline {
|
||||
bool live;
|
||||
std::atomic<bool> &cancel;
|
||||
double duration = 0;
|
||||
std::vector<double> boundaries;
|
||||
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);
|
||||
duration = std::max(source.duration, videoInput->duration());
|
||||
@@ -519,9 +533,32 @@ struct Pipeline {
|
||||
}
|
||||
}
|
||||
require(videoIndex >= 0 || audioIndex >= 0);
|
||||
// Read only a complete, already-loaded sample index, before read-ahead
|
||||
// owns the demuxer. No media scan or upstream seeks to build a playlist.
|
||||
if (!live && videoIndex >= 0) {
|
||||
auto *stream = videoInput->format->streams[videoIndex];
|
||||
const int count = avformat_index_get_entries_count(stream);
|
||||
if (remuxVideo(*videoInput, *stream) && count > 0 && stream->nb_frames == count) {
|
||||
boundaries.push_back(0);
|
||||
double previous = -INFINITY;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const auto *entry = avformat_index_get_entry(stream, i);
|
||||
const double time = entry->timestamp * av_q2d(stream->time_base) - videoInput->offset();
|
||||
if (entry->timestamp == AV_NOPTS_VALUE || time < previous) { boundaries.clear(); break; }
|
||||
previous = time;
|
||||
if (!(entry->flags & AVINDEX_KEYFRAME) || time < boundaries.back() + segmentSeconds) continue;
|
||||
// ponytail: cap indexed GOP segments at twelve seconds; longer GOPs use the fixed-grid transcode.
|
||||
if (time - boundaries.back() > 2 * segmentSeconds || time >= duration) break;
|
||||
boundaries.push_back(time);
|
||||
}
|
||||
if (boundaries.size() < 2 || duration - boundaries.back() > 2 * segmentSeconds ||
|
||||
duration <= boundaries.back()) boundaries.clear();
|
||||
else boundaries.push_back(duration);
|
||||
}
|
||||
}
|
||||
videoInput->select({audioSource == videoInput.get() ? audioIndex : -1, videoIndex}, cacheBudget);
|
||||
if (audioInput) audioInput->select({audioIndex, -1}, cacheBudget);
|
||||
reset(start);
|
||||
reset(start, !boundaries.empty());
|
||||
if (!source.audio.empty()) require(audio && audio->stream);
|
||||
require(video || audio);
|
||||
if (!source.live) require(std::isfinite(duration) && duration > 0 && duration <= 86400 * 14);
|
||||
@@ -537,18 +574,18 @@ struct Pipeline {
|
||||
videoInput->operationDeadline = std::numeric_limits<int64_t>::max();
|
||||
if (audioInput) audioInput->operationDeadline = std::numeric_limits<int64_t>::max();
|
||||
}
|
||||
void reset(double start) {
|
||||
void reset(double start, bool aligned = false) {
|
||||
armOperation();
|
||||
videoOutput.reset(); audioOutput.reset();
|
||||
video.reset(); audio.reset();
|
||||
// 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;
|
||||
aacPreroll(start) : NAN;
|
||||
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 (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);
|
||||
}
|
||||
Segment segment(int64_t id, double start, double end) {
|
||||
@@ -606,6 +643,7 @@ struct CastMedia {
|
||||
std::thread producer;
|
||||
std::string base, token;
|
||||
double duration = 0;
|
||||
std::vector<double> boundaries;
|
||||
std::deque<std::shared_ptr<const Segment>> segments;
|
||||
std::map<int, std::shared_ptr<const std::vector<uint8_t>>> subtitles;
|
||||
#ifdef CAST_MEDIA_CHECK
|
||||
@@ -681,16 +719,26 @@ struct CastMedia {
|
||||
}
|
||||
} catch (...) { std::lock_guard guard(lock); liveFailed = true; wake.notify_all(); }
|
||||
}
|
||||
int64_t segmentCount() const {
|
||||
return boundaries.empty() ? int64_t(std::ceil(duration / segmentSeconds)) : int64_t(boundaries.size() - 1);
|
||||
}
|
||||
double boundary(int64_t id) const {
|
||||
return boundaries.empty() ? std::min(duration, id * segmentSeconds) : boundaries[size_t(id)];
|
||||
}
|
||||
std::vector<uint8_t> playlist() {
|
||||
std::string text = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:6\n";
|
||||
int64_t first = 0, count = int64_t(std::ceil(duration / segmentSeconds));
|
||||
double longest = segmentSeconds;
|
||||
for (size_t i = 1; i < boundaries.size(); ++i)
|
||||
longest = std::max(longest, boundaries[i] - boundaries[i - 1]);
|
||||
std::string text = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:" +
|
||||
std::to_string(int(std::ceil(longest))) + "\n";
|
||||
int64_t first = 0, count = segmentCount();
|
||||
std::lock_guard guard(lock);
|
||||
if (source.live) { require(!segments.empty() && !liveFailed); first = segments.front()->id; count = int64_t(segments.size()); }
|
||||
else text += "#EXT-X-PLAYLIST-TYPE:VOD\n";
|
||||
text += "#EXT-X-MEDIA-SEQUENCE:" + std::to_string(first) + "\n";
|
||||
if (source.live) text += "#EXT-X-DISCONTINUITY-SEQUENCE:" + std::to_string(first) + "\n";
|
||||
for (int64_t id = first; id < first + count; ++id) {
|
||||
char value[64]; double length = source.live ? segments[size_t(id - first)]->duration : std::min(segmentSeconds, duration - id * segmentSeconds);
|
||||
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);
|
||||
// Each independently generated segment resets codec/mux state.
|
||||
if (id > first) text += "#EXT-X-DISCONTINUITY\n";
|
||||
@@ -712,10 +760,10 @@ struct CastMedia {
|
||||
require(!cancelled);
|
||||
if (auto segment = cached()) return segment;
|
||||
try {
|
||||
if (vodPipeline) vodPipeline->reset(id * segmentSeconds);
|
||||
else vodPipeline = std::make_unique<Pipeline>(source, cancelled, id * segmentSeconds);
|
||||
auto segment = std::make_shared<const Segment>(vodPipeline->segment(
|
||||
id, id * segmentSeconds, std::min(duration, (id + 1) * segmentSeconds)));
|
||||
const double start = boundary(id), end = boundary(id + 1);
|
||||
if (vodPipeline) 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));
|
||||
{ std::lock_guard guard(lock); segments.push_back(segment); while (segments.size() > windowSegments) segments.pop_front(); }
|
||||
return segment;
|
||||
} catch (...) {
|
||||
@@ -798,7 +846,7 @@ struct CastMedia {
|
||||
else if (source.live) {
|
||||
std::lock_guard guard(lock); auto found = std::find_if(segments.begin(), segments.end(), [&](const auto &segment) { return segment->id == id; });
|
||||
if (found == segments.end()) status = 404; else retained = std::shared_ptr<const std::vector<uint8_t>>(*found, &(*found)->bytes);
|
||||
} else if (double(id) * segmentSeconds >= duration) status = 404;
|
||||
} else if (id >= segmentCount()) status = 404;
|
||||
else {
|
||||
auto segment = vodSegment(id);
|
||||
retained = std::shared_ptr<const std::vector<uint8_t>>(segment, &segment->bytes);
|
||||
@@ -840,6 +888,7 @@ extern "C" bool cast_media_start(CastMedia *media, const char *host, char url[51
|
||||
require(media && host && url && duration && !media->cancelled);
|
||||
auto pipeline = std::make_unique<Pipeline>(media->source, media->cancelled, 0);
|
||||
media->duration = pipeline->duration;
|
||||
media->boundaries = std::move(pipeline->boundaries);
|
||||
media->bindReceiver(host);
|
||||
if (media->source.live) {
|
||||
media->producer = std::thread([media, pipe = std::move(pipeline)]() mutable { media->produce(std::move(pipe)); });
|
||||
|
||||
Reference in New Issue
Block a user