perf: prefer VideoToolbox decoding with software fallback
This commit is contained in:
+25
-23
@@ -1,6 +1,7 @@
|
||||
#include "cast_media.h"
|
||||
#include "network_policy.hpp"
|
||||
#include "network_input.hpp"
|
||||
#include "video_decoder.hpp"
|
||||
extern "C" {
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
@@ -40,6 +41,9 @@ using Clock = std::chrono::steady_clock;
|
||||
constexpr double segmentSeconds = 6;
|
||||
constexpr size_t segmentLimit = 32 * 1024 * 1024;
|
||||
constexpr size_t windowSegments = 8;
|
||||
#ifdef CAST_MEDIA_CHECK
|
||||
std::atomic<uint64_t> hardwareVideoFrames{0}, softwareVideoFrames{0};
|
||||
#endif
|
||||
void check(int value) { if (value < 0) throw std::runtime_error("Cast media operation failed (private details redacted)."); }
|
||||
void require(bool value) { if (!value) throw std::runtime_error("Cast media is unavailable or unsupported (private details redacted)."); }
|
||||
void privateLog(void *, int level, const char *, va_list) {
|
||||
@@ -55,26 +59,6 @@ Frame frame() { auto p = Frame(av_frame_alloc()); require(bool(p)); return p; }
|
||||
Packet packet() { auto p = Packet(av_packet_alloc()); require(bool(p)); return p; }
|
||||
int64_t aacBoundary(double seconds) { return int64_t(std::ceil(seconds * 48000 / 1024)) * 1024; }
|
||||
double aacPreroll(double seconds) { return double(std::max<int64_t>(0, aacBoundary(seconds) - 12 * 1024)) / 48000; }
|
||||
bool hasIDR(const AVPacket &packet, const AVCodecParameters &codec) {
|
||||
const auto *bytes = packet.data;
|
||||
const size_t size = size_t(std::max(0, packet.size));
|
||||
if (codec.extradata_size >= 5 && codec.extradata[0] == 1) {
|
||||
const size_t lengthSize = (codec.extradata[4] & 3) + 1;
|
||||
for (size_t at = 0; at + lengthSize <= size;) {
|
||||
uint32_t length = 0;
|
||||
for (size_t i = 0; i < lengthSize; ++i) length = (length << 8) | bytes[at++];
|
||||
if (!length || length > size - at) return false;
|
||||
if ((bytes[at] & 31) == 5) return true;
|
||||
at += length;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
for (size_t at = 0; at + 3 < size; ++at) {
|
||||
if (bytes[at] == 0 && bytes[at + 1] == 0 && bytes[at + 2] == 1 &&
|
||||
(bytes[at + 3] & 31) == 5) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
struct Source {
|
||||
std::string video, videoHeaders, audio, audioHeaders;
|
||||
bool local, restricted, live;
|
||||
@@ -234,7 +218,7 @@ struct Track {
|
||||
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) &&
|
||||
playback::hasIDR(*pending, *stream->codecpar) &&
|
||||
(std::abs(packetTime() - start) < .001 ||
|
||||
(start == 0 && packetTime() >= 0 && packetTime() <= openingGap));
|
||||
if (aligned && !remux) {
|
||||
@@ -243,7 +227,7 @@ struct Track {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
!playback::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.
|
||||
input->seek(0);
|
||||
nextPacket();
|
||||
@@ -258,6 +242,9 @@ struct Track {
|
||||
decoder->pkt_timebase = stream->time_base;
|
||||
decoder->thread_count = 2;
|
||||
decoder->max_pixels = 4096LL * 2160;
|
||||
if (type == AVMEDIA_TYPE_VIDEO && pending &&
|
||||
playback::independentVideoPacket(*pending, *stream->codecpar))
|
||||
playback::preferVideoToolbox(decoder);
|
||||
check(avcodec_open2(decoder, codec, nullptr));
|
||||
} catch (...) { avcodec_free_context(&decoder); throw; }
|
||||
}
|
||||
@@ -294,6 +281,10 @@ struct Track {
|
||||
require(!input->interrupted());
|
||||
int result = avcodec_receive_frame(decoder, f.get());
|
||||
if (result == 0) {
|
||||
#ifdef CAST_MEDIA_CHECK
|
||||
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
|
||||
(f->format == AV_PIX_FMT_VIDEOTOOLBOX ? hardwareVideoFrames : softwareVideoFrames).fetch_add(1, std::memory_order_relaxed);
|
||||
#endif
|
||||
decoded = std::move(f);
|
||||
const double step = stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO ? double(decoded->nb_samples) / decoded->sample_rate : 1.0 / 30;
|
||||
nextTime = frameTime() + step;
|
||||
@@ -337,6 +328,7 @@ struct OutputTrack {
|
||||
AVStream *stream = nullptr;
|
||||
AVCodecContext *encoder = nullptr;
|
||||
SwsContext *scale = nullptr;
|
||||
Frame readable;
|
||||
SwrContext *resample = nullptr;
|
||||
AVAudioFifo *fifo = nullptr;
|
||||
int64_t audioPTS = AV_NOPTS_VALUE;
|
||||
@@ -463,8 +455,14 @@ struct OutputTrack {
|
||||
auto out = frame(); out->format = encoder->pix_fmt; out->width = encoder->width; out->height = encoder->height;
|
||||
out->sample_aspect_ratio = encoder->sample_aspect_ratio;
|
||||
check(av_frame_get_buffer(out.get(), 32));
|
||||
if (f->format == AV_PIX_FMT_VIDEOTOOLBOX) {
|
||||
if (!readable) readable = frame();
|
||||
check(playback::refVideoFrame(readable.get(), f));
|
||||
f = readable.get();
|
||||
}
|
||||
scale = sws_getCachedContext(scale, f->width, f->height, AVPixelFormat(f->format), out->width, out->height, encoder->pix_fmt, SWS_BILINEAR, nullptr, nullptr, nullptr);
|
||||
require(scale != nullptr); check(sws_scale(scale, f->data, f->linesize, 0, f->height, out->data, out->linesize));
|
||||
if (readable) av_frame_unref(readable.get());
|
||||
out->pts = int64_t(std::llround(std::max(start, time) * 90000));
|
||||
out->pict_type = firstVideo ? AV_PICTURE_TYPE_I : AV_PICTURE_TYPE_NONE; firstVideo = false; encoded(out.get());
|
||||
lastVideoTime = time;
|
||||
@@ -816,7 +814,7 @@ struct CastMedia {
|
||||
const bool independent = !video || !video->remux ||
|
||||
(video->pending && std::abs(video->packetTime() - start) < .001 &&
|
||||
(video->pending->flags & AV_PKT_FLAG_KEY) &&
|
||||
hasIDR(*video->pending, *video->stream->codecpar));
|
||||
playback::hasIDR(*video->pending, *video->stream->codecpar));
|
||||
const bool drained = (vodPipeline->videoOutput && vodPipeline->videoOutput->drained) ||
|
||||
(vodPipeline->audioOutput && vodPipeline->audioOutput->drained);
|
||||
if (vodPipeline->nextStart != start || !independent || drained)
|
||||
@@ -971,6 +969,10 @@ extern "C" bool cast_media_subtitle(CastMedia *media, int id, const void *bytes,
|
||||
extern "C" void cast_media_cancel(CastMedia *media) { if (media) media->cancel(); }
|
||||
extern "C" void cast_media_destroy(CastMedia *media) { delete media; }
|
||||
#ifdef CAST_MEDIA_CHECK
|
||||
extern "C" void cast_media_check_decode_counts(uint64_t *hardware, uint64_t *software) {
|
||||
*hardware = hardwareVideoFrames.load(std::memory_order_relaxed);
|
||||
*software = softwareVideoFrames.load(std::memory_order_relaxed);
|
||||
}
|
||||
extern "C" bool cast_media_check_advance(CastMedia *media, double seconds) {
|
||||
if (!media || !media->source.live || !std::isfinite(seconds) || seconds < 6 || seconds > 3600) return false;
|
||||
std::unique_lock guard(media->lock);
|
||||
|
||||
+14
-4
@@ -2,6 +2,7 @@
|
||||
#include "player_macos.h"
|
||||
#include "output.hpp"
|
||||
#include "progress_reporter.hpp"
|
||||
#include "video_decoder.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
@@ -64,6 +65,7 @@ struct Track {
|
||||
AVPacket *packet = nullptr;
|
||||
bool packetPending = false;
|
||||
bool receiving = false;
|
||||
bool firstPacket = true;
|
||||
double packetBufferedEnd = 0;
|
||||
double decoderBufferedEnd = 0;
|
||||
bool sourceEnded = false;
|
||||
@@ -456,6 +458,7 @@ struct Player {
|
||||
track.codec->pkt_timebase = source->streams[index]->time_base;
|
||||
// A bounded software decoder pool; FFmpeg owns its internal frame reorder buffer.
|
||||
track.codec->thread_count = 2;
|
||||
if (type == AVMEDIA_TYPE_VIDEO) playback::preferVideoToolbox(track.codec);
|
||||
check(avcodec_open2(track.codec, decoder, nullptr), "Opening decoder");
|
||||
}
|
||||
|
||||
@@ -466,6 +469,7 @@ struct Player {
|
||||
if (!track.codec) continue;
|
||||
avcodec_flush_buffers(track.codec);
|
||||
track.drainSent = track.drained = false;
|
||||
track.firstPacket = true;
|
||||
av_packet_unref(track.packet);
|
||||
track.packetPending = track.receiving = track.sourceEnded = false;
|
||||
track.packetBufferedEnd = track.decoderBufferedEnd = 0;
|
||||
@@ -1086,8 +1090,11 @@ struct Player {
|
||||
return;
|
||||
skipPresentedTimestamp = AV_NOPTS_VALUE;
|
||||
}
|
||||
const int size = av_image_get_buffer_size(static_cast<AVPixelFormat>(decoded->format),
|
||||
decoded->width, decoded->height, 1);
|
||||
Frame frame(av_frame_alloc());
|
||||
if (!frame) throw std::bad_alloc();
|
||||
check(playback::refVideoFrame(frame.get(), decoded), "Mapping decoded video");
|
||||
const int size = av_image_get_buffer_size(static_cast<AVPixelFormat>(frame->format),
|
||||
frame->width, frame->height, 1);
|
||||
check(size, "Sizing decoded video");
|
||||
const auto bytes = static_cast<std::size_t>(size);
|
||||
if (bytes > videoByteLimit)
|
||||
@@ -1100,8 +1107,6 @@ struct Player {
|
||||
videoBytes -= video.back().bytes;
|
||||
video.pop_back();
|
||||
}
|
||||
Frame frame(av_frame_clone(decoded));
|
||||
if (!frame) throw std::bad_alloc();
|
||||
VideoFrame item{std::move(frame), pts, pts + frameDuration, bytes, track.timestampOccurrence};
|
||||
auto insertion = std::upper_bound(video.begin(), video.end(), pts,
|
||||
[](double value, const VideoFrame &entry) { return value < entry.pts; });
|
||||
@@ -1137,6 +1142,11 @@ struct Player {
|
||||
return true;
|
||||
}
|
||||
if (track.packetPending) {
|
||||
if (slot == 1 && track.firstPacket) {
|
||||
check(playback::prepareVideoPacket(track.codec, *track.stream()->codecpar, *track.packet),
|
||||
"Preparing video decoder preroll");
|
||||
track.firstPacket = false;
|
||||
}
|
||||
const int result = avcodec_send_packet(track.codec, track.packet);
|
||||
if (result != AVERROR(EAGAIN)) {
|
||||
check(result, "Sending media packet to decoder");
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/hwcontext.h>
|
||||
}
|
||||
|
||||
namespace playback {
|
||||
inline bool hasIDR(const AVPacket &packet, const AVCodecParameters &codec) {
|
||||
const auto *bytes = packet.data;
|
||||
const size_t size = size_t(std::max(0, packet.size));
|
||||
if (codec.extradata_size >= 5 && codec.extradata[0] == 1) {
|
||||
const size_t lengthSize = (codec.extradata[4] & 3) + 1;
|
||||
for (size_t at = 0; at + lengthSize <= size;) {
|
||||
uint32_t length = 0;
|
||||
for (size_t i = 0; i < lengthSize; ++i) length = (length << 8) | bytes[at++];
|
||||
if (!length || length > size - at) return false;
|
||||
if ((bytes[at] & 31) == 5) return true;
|
||||
at += length;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
for (size_t at = 0; at + 3 < size; ++at) {
|
||||
if (bytes[at] == 0 && bytes[at + 1] == 0 && bytes[at + 2] == 1 &&
|
||||
(bytes[at + 3] & 31) == 5) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// FFmpeg retries software if VideoToolbox rejects the stream; HEVC may also use
|
||||
// Apple's platform software fallback. Keep the normal decoder for other codecs.
|
||||
static inline void preferVideoToolbox(AVCodecContext *context) {
|
||||
#ifndef TEREBI_SOFTWARE_DECODE
|
||||
// Prefer only the codecs measured in native playback and the Cast relay.
|
||||
if (context->codec_id != AV_CODEC_ID_H264 && context->codec_id != AV_CODEC_ID_HEVC &&
|
||||
context->codec_id != AV_CODEC_ID_VP9) return;
|
||||
for (int index = 0; const auto *config = avcodec_get_hw_config(context->codec, index); ++index) {
|
||||
if (config->device_type != AV_HWDEVICE_TYPE_VIDEOTOOLBOX ||
|
||||
!(config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) continue;
|
||||
// The default get_format prefers this device and retries software if
|
||||
// VideoToolbox rejects the stream; device creation failure is also safe.
|
||||
av_hwdevice_ctx_create(&context->hw_device_ctx, AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
|
||||
nullptr, nullptr, 0);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// Starting at a dependent packet can make VideoToolbox fail during send_packet,
|
||||
// after format negotiation has already succeeded. Preserve software preroll.
|
||||
inline bool independentVideoPacket(const AVPacket &packet, const AVCodecParameters ¶meters) {
|
||||
return (packet.flags & AV_PKT_FLAG_KEY) &&
|
||||
(parameters.codec_id != AV_CODEC_ID_H264 || hasIDR(packet, parameters));
|
||||
}
|
||||
inline int prepareVideoPacket(AVCodecContext *&context, const AVCodecParameters ¶meters,
|
||||
const AVPacket &packet) {
|
||||
if (!context->hw_device_ctx || independentVideoPacket(packet, parameters)) return 0;
|
||||
auto *software = avcodec_alloc_context3(context->codec);
|
||||
if (!software) return AVERROR(ENOMEM);
|
||||
int result = avcodec_parameters_to_context(software, ¶meters);
|
||||
software->pkt_timebase = context->pkt_timebase;
|
||||
software->thread_count = context->thread_count;
|
||||
software->max_pixels = context->max_pixels;
|
||||
if (result >= 0) result = avcodec_open2(software, context->codec, nullptr);
|
||||
if (result < 0) { avcodec_free_context(&software); return result; }
|
||||
avcodec_free_context(&context); context = software;
|
||||
return 0;
|
||||
}
|
||||
// Mapping retains and locks the CVPixelBuffer until the last frame reference
|
||||
// is released. Consumers see ordinary CPU-readable planes without a pixel copy.
|
||||
inline int refVideoFrame(AVFrame *destination, const AVFrame *source) {
|
||||
if (source->format == AV_PIX_FMT_VIDEOTOOLBOX)
|
||||
return av_hwframe_map(destination, source, AV_HWFRAME_MAP_READ);
|
||||
return av_frame_ref(destination, source);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user