neverlag-mpv-changes.patch — Neverlag's changes to mpv 0.41.0
====================================================================
Applies to the unmodified mpv 0.41.0 release source tree
(https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz):
tar xzf mpv-0.41.0.tar.gz && cd mpv-0.41.0
patch -p1 < neverlag-mpv-changes.patch
Contents
1. meson.build: compile osdep/utils-mac.c on Darwin even when Cocoa is
disabled. ao_coreaudio and ao_audiounit reference its CFString helpers,
which upstream only builds together with the Cocoa/Swift front end.
Used on macOS, iOS and tvOS.
2. meson.build: let the videotoolbox-gl hwdec build against plain OpenGL
(features['gl']) instead of requiring gl-cocoa or ios-gl. The hwdec
needs IOSurface, CoreVideo and CGL only. Used on macOS; the iOS and
tvOS builds disable videotoolbox-gl.
3. New libmpv render backend "cb" (MPV_RENDER_API_TYPE_CB): declared in
include/mpv/render_cb.h, implemented in video/out/libmpv_cb.c, wired up
in video/out/vo_libmpv.c, video/out/libmpv.h, include/mpv/render.h and
the build option -Dlibmpv-cb. It hands the decoded frame (a
CVPixelBuffer for VideoToolbox output) to the API user instead of
rendering it, so the app can display it without a copy. Enabled in the
tvOS build (-Dlibmpv-cb=enabled).
Build options used with this tree are listed at
https://neverlag.app/open-source/.
License: all changes are licensed like the files they modify — GNU Lesser
General Public License, version 2.1 or (at your option) any later version.
mpv itself is built with -Dgpl=false.
Generated by Tools/open-source/make-mpv-patch.sh in the Neverlag repository
from the release tarball above.
diff -ruN a/include/mpv/render.h b/include/mpv/render.h
--- a/include/mpv/render.h
+++ b/include/mpv/render.h
@@ -422,6 +422,17 @@
* See MPV_RENDER_PARAM_SW_STRIDE for alignment requirements.
*/
MPV_RENDER_PARAM_SW_POINTER = 20,
+ /**
+ * Callback backend (MPV_RENDER_API_TYPE_CB, see render_cb.h): pointer to a
+ * mpv_cb_frame that mpv_render_context_render() fills with the raw decoded
+ * frame. Required for that backend, ignored by all others.
+ *
+ * Type: mpv_cb_frame*
+ *
+ * Deliberately numbered far above the upstream values so that future
+ * upstream additions do not collide with it.
+ */
+ MPV_RENDER_PARAM_CB_FRAME = 100,
} mpv_render_param_type;
/**
@@ -468,6 +479,7 @@
#define MPV_RENDER_API_TYPE_OPENGL "opengl"
// See section "Software renderer"
#define MPV_RENDER_API_TYPE_SW "sw"
+// See render_cb.h (raw decoded frames, Apple platforms only)
/**
* Flags used in mpv_render_frame_info.flags. Each value represents a bit in it.
diff -ruN a/include/mpv/render_cb.h b/include/mpv/render_cb.h
--- a/include/mpv/render_cb.h
+++ b/include/mpv/render_cb.h
@@ -0,0 +1,141 @@
+/* Copyright (C) 2026 Neverlag
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see .
+ */
+
+#ifndef MPV_CLIENT_API_RENDER_CB_H_
+#define MPV_CLIENT_API_RENDER_CB_H_
+
+#include
+#include
+
+#include "render.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Callback render backend ("cb").
+ *
+ * Unlike the OpenGL and software backends, this backend does not render
+ * anything. mpv_render_context_render() hands the raw decoded frame that mpv
+ * wants displayed *now* to the API user, who is responsible for colour
+ * conversion, scaling and presentation. mpv keeps demuxing, decoding, caching
+ * and frame timing (which frame, when).
+ *
+ * Use mpv_render_context_create() with MPV_RENDER_PARAM_API_TYPE set to
+ * MPV_RENDER_API_TYPE_CB. No further creation parameters are required.
+ *
+ * The backend registers a VideoToolbox hwdec device, so --hwdec=videotoolbox
+ * (zero-copy) becomes available: the frame arrives as a retained
+ * CVPixelBufferRef that keeps its IOSurface. Software frames (any other hwdec,
+ * or the decoder's software fallback) arrive as plane pointers that stay valid
+ * until the next mpv_render_context_render() call.
+ *
+ * Rendering: call mpv_render_context_render() with a MPV_RENDER_PARAM_CB_FRAME
+ * parameter pointing to a mpv_cb_frame that the backend fills. Pass
+ * MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME = 0 to return immediately and present
+ * at target_time_ns yourself, or leave it at the default (1) to have the call
+ * return at the target time, in which case present immediately.
+ *
+ * Only available on Apple platforms (HAVE_LIBMPV_CB).
+ */
+#define MPV_RENDER_API_TYPE_CB "cb"
+
+/**
+ * One frame handed out by the callback backend. Filled by
+ * mpv_render_context_render() when MPV_RENDER_PARAM_CB_FRAME is passed.
+ */
+typedef struct mpv_cb_frame {
+ /**
+ * Realtime at which the frame should be visible, in mpv_get_time_ns()
+ * units. 0 means "present immediately".
+ */
+ int64_t target_time_ns;
+ /**
+ * Approximate distance to the next frame in nanoseconds; 0 if unknown.
+ */
+ int64_t duration_ns;
+ /**
+ * 1 if this is the same frame as the previous call (redraw request while
+ * paused, resize, etc.), 0 for a new frame. Redraws can be ignored by
+ * presenters that keep the last frame on screen.
+ */
+ int redraw;
+ /**
+ * 1 if a frame is present (format != NULL). 0 if mpv has no frame yet
+ * (e.g. before the first decoded image); all other fields are then 0.
+ */
+ int has_frame;
+ /**
+ * Visible image size in luma pixels.
+ */
+ int w, h;
+ /**
+ * Display size after pixel-aspect correction (what an aspect-preserving
+ * presenter should scale the image to before fitting it on screen).
+ */
+ int display_w, display_h;
+ /**
+ * Clockwise rotation in degrees (0, 90, 180, 270).
+ */
+ int rotate;
+ /**
+ * mpv image format name: "videotoolbox" for hardware frames, otherwise
+ * the software format ("nv12", "p010", "yuv420p", "yuv420p10", ...).
+ * Points to memory owned by the render context; valid until the next
+ * mpv_render_context_render() call.
+ */
+ const char *format;
+ /**
+ * For hardware frames: the underlying pixel format of the CVPixelBuffer
+ * ("nv12", "p010", ...). NULL for software frames.
+ */
+ const char *sw_format;
+ /**
+ * Hardware frame: a CVPixelBufferRef with +1 retain count. The API user
+ * must release it with CVPixelBufferRelease(). NULL for software frames.
+ */
+ void *cvpixelbuffer;
+ /**
+ * Software frame: plane pointers and strides (bytes per row). Valid until
+ * the next mpv_render_context_render() call or until the render context
+ * is freed. NULL/0 for hardware frames.
+ */
+ uint8_t *planes[4];
+ ptrdiff_t stride[4];
+ /**
+ * Colour description, named as in the "video-out-params" property
+ * ("bt.709", "bt.2020", "hlg", "pq", "bt.2020-ncl", "limited", ...).
+ * Static strings, always valid. NULL if unknown.
+ */
+ const char *primaries;
+ const char *transfer;
+ const char *matrix;
+ const char *levels;
+ const char *chroma_location;
+ /**
+ * Nominal peak luminance in cd/m^2 from HDR metadata, 0 if unknown.
+ */
+ double max_luma;
+} mpv_cb_frame;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff -ruN a/meson.build b/meson.build
--- a/meson.build
+++ b/meson.build
@@ -406,6 +406,7 @@
if darwin
path_source = files('osdep/path-darwin.c')
timer_source = files('osdep/timer-darwin.c')
+ sources += files('osdep/utils-mac.c')
endif
cocoa = dependency('appleframeworks', modules: ['Cocoa', 'IOKit', 'QuartzCore'],
@@ -1500,7 +1501,7 @@
endif
videotoolbox_gl = get_option('videotoolbox-gl').require(
- features['gl-cocoa'] or features['ios-gl'],
+ features['gl'],
error_message: 'gl-cocoa nor ios-gl could be found!',
)
features += {'videotoolbox-gl': videotoolbox_gl.allowed()}
@@ -1522,7 +1523,17 @@
sources += files('video/out/hwdec/hwdec_vt_pl.m')
endif
+# Callback render backend (render_cb.h): raw decoded frames for the API user,
+# VideoToolbox frames without a copy. Apple platforms only.
+libmpv_cb = get_option('libmpv-cb').require(darwin,
+ error_message: 'libmpv-cb needs an Apple platform!')
+features += {'libmpv-cb': libmpv_cb.allowed()}
+if features['libmpv-cb']
+ dependencies += dependency('appleframeworks', modules: ['CoreVideo'])
+ sources += files('video/out/libmpv_cb.c')
+endif
+
# macOS features
macos_sdk_version_py = ''
if darwin
@@ -1796,7 +1807,8 @@
description: 'mpv media player client library')
headers = ['include/mpv/client.h', 'include/mpv/render.h',
- 'include/mpv/render_gl.h', 'include/mpv/stream_cb.h']
+ 'include/mpv/render_gl.h', 'include/mpv/render_cb.h',
+ 'include/mpv/stream_cb.h']
install_headers(headers, subdir: 'mpv')
# Allow projects to build with libmpv by cloning into ./subprojects/mpv
diff -ruN a/meson.options b/meson.options
--- a/meson.options
+++ b/meson.options
@@ -103,6 +103,7 @@
option('ios-gl', type: 'feature', value: 'auto', description: 'iOS OpenGL ES interop support')
option('videotoolbox-gl', type: 'feature', value: 'auto', description: 'Videotoolbox with OpenGL')
option('videotoolbox-pl', type: 'feature', value: 'auto', description: 'Videotoolbox with libplacebo')
+option('libmpv-cb', type: 'feature', value: 'auto', description: 'libmpv callback render backend (raw decoded frames, Apple platforms)')
# macOS features
option('macos-10-15-4-features', type: 'feature', value: 'auto', description: 'macOS 10.15.4 SDK Features')
diff -ruN a/video/out/libmpv.h b/video/out/libmpv.h
--- a/video/out/libmpv.h
+++ b/video/out/libmpv.h
@@ -81,3 +81,4 @@
extern const struct render_backend_fns render_backend_gpu;
extern const struct render_backend_fns render_backend_sw;
+extern const struct render_backend_fns render_backend_cb;
diff -ruN a/video/out/libmpv_cb.c b/video/out/libmpv_cb.c
--- a/video/out/libmpv_cb.c
+++ b/video/out/libmpv_cb.c
@@ -0,0 +1,235 @@
+/*
+ * Callback render backend: hands the raw decoded frame to the libmpv API user
+ * instead of rendering it. See include/mpv/render_cb.h for the contract.
+ *
+ * Structure follows libmpv_sw.c. Apple platforms only: the point of the
+ * backend is to pass VideoToolbox frames (CVPixelBufferRef) to the app without
+ * a copy, so the app can display them via the system video path or Metal.
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see .
+ */
+
+#include
+
+#include
+#include
+
+#include "mpv/render_cb.h"
+#include "libmpv.h"
+#include "options/m_option.h"
+#include "video/csputils.h"
+#include "video/hwdec.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+
+struct priv {
+ struct mp_image_params src_params;
+ struct mp_hwdec_ctx vt_ctx;
+ bool has_vt;
+ // Stable storage for the format names handed out in mpv_cb_frame.
+ char format_name[32];
+ char sw_format_name[32];
+};
+
+static const int software_formats[] = {
+ IMGFMT_NV12,
+ IMGFMT_P010,
+ IMGFMT_420P,
+ 0,
+};
+
+static int init(struct render_backend *ctx, mpv_render_param *params)
+{
+ ctx->priv = talloc_zero(NULL, struct priv);
+ struct priv *p = ctx->priv;
+
+ char *api = get_mpv_render_param(params, MPV_RENDER_PARAM_API_TYPE, NULL);
+ if (!api)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ if (strcmp(api, MPV_RENDER_API_TYPE_CB) != 0)
+ return MPV_ERROR_NOT_IMPLEMENTED;
+
+ // Register a VideoToolbox device so vd_lavc can pick the zero-copy
+ // "videotoolbox" hwdec (hwdec_vt.c does the same for the GPU backends).
+ // Failure is not fatal: the decoder then falls back to videotoolbox-copy
+ // or software, and frames arrive as plane pointers.
+ ctx->hwdec_devs = hwdec_devices_create();
+ p->vt_ctx = (struct mp_hwdec_ctx){
+ .driver_name = "videotoolbox",
+ .hw_imgfmt = IMGFMT_VIDEOTOOLBOX,
+ };
+ int ret = av_hwdevice_ctx_create(&p->vt_ctx.av_device_ref,
+ AV_HWDEVICE_TYPE_VIDEOTOOLBOX, NULL, NULL, 0);
+ if (ret == 0) {
+ hwdec_devices_add(ctx->hwdec_devs, &p->vt_ctx);
+ p->has_vt = true;
+ MP_VERBOSE(ctx, "cb: VideoToolbox hwdec device registered\n");
+ } else {
+ MP_WARN(ctx, "cb: no VideoToolbox hwdec device (%d), software frames only\n", ret);
+ }
+
+ return 0;
+}
+
+static bool check_format(struct render_backend *ctx, int imgfmt)
+{
+ struct priv *p = ctx->priv;
+
+ if (imgfmt == IMGFMT_VIDEOTOOLBOX)
+ return p->has_vt;
+ for (int n = 0; software_formats[n]; n++) {
+ if (software_formats[n] == imgfmt)
+ return true;
+ }
+ // 10-bit planar software fallback of the HEVC decoder.
+ return imgfmt == mp_imgfmt_from_name(bstr0("yuv420p10"));
+}
+
+static int set_parameter(struct render_backend *ctx, mpv_render_param param)
+{
+ return MPV_ERROR_NOT_IMPLEMENTED;
+}
+
+static void reconfig(struct render_backend *ctx, struct mp_image_params *params)
+{
+ struct priv *p = ctx->priv;
+
+ p->src_params = *params;
+}
+
+static void reset(struct render_backend *ctx)
+{
+ // stateless
+}
+
+static void update_external(struct render_backend *ctx, struct vo *vo)
+{
+ // No OSD: the app draws nothing on top of the frame.
+}
+
+static void resize(struct render_backend *ctx, struct mp_rect *src,
+ struct mp_rect *dst, struct mp_osd_res *osd)
+{
+ // The app scales; mpv's src/dst rectangles are not used.
+}
+
+static int get_target_size(struct render_backend *ctx, mpv_render_param *params,
+ int *out_w, int *out_h)
+{
+ struct priv *p = ctx->priv;
+
+ // No target surface. Report the source size so vo_libmpv's resize logic
+ // sees a stable, non-zero viewport.
+ *out_w = p->src_params.w > 0 ? p->src_params.w : 1;
+ *out_h = p->src_params.h > 0 ? p->src_params.h : 1;
+ return 0;
+}
+
+static int render(struct render_backend *ctx, mpv_render_param *params,
+ struct vo_frame *frame)
+{
+ struct priv *p = ctx->priv;
+
+ mpv_cb_frame *out = get_mpv_render_param(params, MPV_RENDER_PARAM_CB_FRAME, NULL);
+ if (!out)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ *out = (mpv_cb_frame){0};
+
+ struct mp_image *img = frame->current;
+ if (!img)
+ return 0;
+
+ out->has_frame = 1;
+ out->target_time_ns = frame->pts;
+ out->duration_ns = frame->duration > 0 ? (int64_t)frame->duration : 0;
+ out->redraw = frame->redraw ? 1 : 0;
+ out->w = img->w;
+ out->h = img->h;
+ out->rotate = img->params.rotate;
+
+ // Display size after pixel-aspect correction, like video-out-params/dw|dh.
+ int dw = img->w, dh = img->h;
+ if (img->params.p_w > 0 && img->params.p_h > 0) {
+ if (img->params.p_w > img->params.p_h) {
+ dw = (int)((int64_t)img->w * img->params.p_w / img->params.p_h);
+ } else if (img->params.p_h > img->params.p_w) {
+ dh = (int)((int64_t)img->h * img->params.p_h / img->params.p_w);
+ }
+ }
+ out->display_w = dw;
+ out->display_h = dh;
+
+ mp_imgfmt_to_name_buf(p->format_name, sizeof(p->format_name), img->imgfmt);
+ out->format = p->format_name;
+
+ if (img->imgfmt == IMGFMT_VIDEOTOOLBOX) {
+ // mpv convention (hwdec_vt_pl.m): the CVPixelBufferRef travels in
+ // planes[3], like AVFrame.data[3] in FFmpeg.
+ CVPixelBufferRef pb = (CVPixelBufferRef)img->planes[3];
+ if (pb)
+ CVPixelBufferRetain(pb);
+ out->cvpixelbuffer = pb;
+ if (img->params.hw_subfmt) {
+ mp_imgfmt_to_name_buf(p->sw_format_name, sizeof(p->sw_format_name),
+ img->params.hw_subfmt);
+ out->sw_format = p->sw_format_name;
+ }
+ } else {
+ for (int n = 0; n < MP_MAX_PLANES && n < 4; n++) {
+ out->planes[n] = img->planes[n];
+ out->stride[n] = img->stride[n];
+ }
+ }
+
+ out->primaries = m_opt_choice_str(pl_csp_prim_names, img->params.color.primaries);
+ out->transfer = m_opt_choice_str(pl_csp_trc_names, img->params.color.transfer);
+ out->matrix = m_opt_choice_str(pl_csp_names, img->params.repr.sys);
+ out->levels = m_opt_choice_str(pl_csp_levels_names, img->params.repr.levels);
+ out->chroma_location = m_opt_choice_str(pl_chroma_names, img->params.chroma_location);
+ out->max_luma = img->params.color.hdr.max_luma;
+
+ return 0;
+}
+
+static void destroy(struct render_backend *ctx)
+{
+ struct priv *p = ctx->priv;
+
+ if (!p)
+ return;
+ if (ctx->hwdec_devs) {
+ if (p->has_vt)
+ hwdec_devices_remove(ctx->hwdec_devs, &p->vt_ctx);
+ hwdec_devices_destroy(ctx->hwdec_devs);
+ ctx->hwdec_devs = NULL;
+ }
+ av_buffer_unref(&p->vt_ctx.av_device_ref);
+}
+
+const struct render_backend_fns render_backend_cb = {
+ .init = init,
+ .check_format = check_format,
+ .set_parameter = set_parameter,
+ .reconfig = reconfig,
+ .reset = reset,
+ .update_external = update_external,
+ .resize = resize,
+ .get_target_size = get_target_size,
+ .render = render,
+ .destroy = destroy,
+};
diff -ruN a/video/out/vo_libmpv.c b/video/out/vo_libmpv.c
--- a/video/out/vo_libmpv.c
+++ b/video/out/vo_libmpv.c
@@ -114,6 +114,9 @@
const struct render_backend_fns *render_backends[] = {
&render_backend_gpu,
&render_backend_sw,
+#if HAVE_LIBMPV_CB
+ &render_backend_cb,
+#endif
NULL
};