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 };