PR #24725 opened by rnissl
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24725
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24725.patch

This is 6/9 in the GoPro Max review order. It applies on current master by 
itself.

Stitch the two HEVC EAC views from a GoPro Max 2 .360 file to 2:1 
equirectangular. Based on gopromax_opencl (Max 1) by Ronan LE MEILLAT and 
TADANO Tokumei.

Preferred review order, each request on its own:

1/9 avutil/log: flush stderr and match Windows warning yellow
2/9 avfilter: add yuvrgb_cuda
3/9 avfilter: add gopromax2_cuda GoPro Max 2 EAC stitch
4/9 avcodec: add mjpeg_nvjpeg NVIDIA JPEG encoder
5/9 avcodec/bsf: add jpegapp to inject JPEG APP segments
6/9 avfilter: add gopromax2 GoPro Max 2 EAC stitch
7/9 avfilter: add gopromax2_opencl GoPro Max 2 EAC stitch
8/9 avfilter: add gopromax_opencl GoPro Max 1 stitch
9/9 avcodec/mjpegenc: encode packed RGB via YUV conversion

After 1-5, mapillary_tools processes GoPro Max 2 files GPU-only in my setup: 
scale_cuda=yuv444p10, yuvrgb_cuda=rgb0, gopromax2_cuda, mjpeg_nvjpeg. jpegapp 
inserts the APP1 segment while encoding, so the JPEG ffmpeg writes needs no 
postprocessing.

After 6, the Max 2 CPU fallback works. After 7, Max 2 via OpenCL works. After 
8, original GoPro Max (Max 1) via OpenCL works.

9/9 is needed when 4/9 cannot be used, for example because no nvjpeg library is 
present, and encoding falls back to the software mjpeg encoder. That encoder 
then receives packed RGB (rgb24). Upstream mjpegenc does not accept it; this 
change converts it to YUV first.

The six patches I sent to ffmpeg-devel on 20 September are obsolete. Once the 
filter requests are merged, I will send one short documentation request that 
only adds the cross-references between these sections.


From f3d060b7a1513ddcab32b651b36dacb58e04c5f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Reinhard=20Ni=C3=9Fl?= <[email protected]>
Date: Sat, 26 Sep 2026 21:34:59 +0200
Subject: [PATCH] avfilter: add gopromax2 GoPro Max 2 EAC stitch
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Stitch the two HEVC EAC views from a GoPro Max 2 .360 file to 2:1
equirectangular. Based on gopromax_opencl (Max 1) by Ronan LE MEILLAT
and TADANO Tokumei.

Co-authored-by: Cursor <[email protected]>
Signed-off-by: Reinhard Nißl <[email protected]>
---
 Changelog                  |   5 +
 doc/filters.texi           |  29 +++
 libavfilter/Makefile       |   1 +
 libavfilter/allfilters.c   |   1 +
 libavfilter/vf_gopromax2.c | 482 +++++++++++++++++++++++++++++++++++++
 5 files changed, 518 insertions(+)
 create mode 100644 libavfilter/vf_gopromax2.c

diff --git a/Changelog b/Changelog
index 1cfa9db1b6..2348c65173 100644
--- a/Changelog
+++ b/Changelog
@@ -15,6 +15,11 @@ version <next>:
 - H.264 data partitioning support
 - DSD (dsd_msbf) encoder
 
+- jpegapp bitstream filter
+- MJPEG encoder packed RGB input
+- mjpeg_nvjpeg NVIDIA JPEG encoder
+- yuvrgb_cuda filter
+- gopromax2 GoPro Max 2 CPU stitch filter
 
 version 9.0:
 - Extend AMF Color Converter (vf_vpp_amf) HDR capabilities
diff --git a/doc/filters.texi b/doc/filters.texi
index 1a9bbeff7e..cf65cab33a 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -24979,6 +24979,35 @@ from the video stream (if available).
 Use specified codec instead of snow.
 @end table
 
+@anchor{gopromax2}
+@section gopromax2
+
+Convert the two GoPro Max 2 .360 EAC tracks to equirectangular projection
+on the CPU.
+
+Each input track is @code{side} + @code{center} + @code{side} (GoPro Max 2:
+5952x1920 = 2016 + 1920 + 2016). The duplicated overlap lives in the side
+faces and is blended over @code{blend} pixels (default 48). Typical mapping
+is front @code{0:0} and rear @code{0:4}. Default output size is 7680x3840.
+
+@table @option
+@item w
+@item h
+Output equirectangular size. Must be 2:1.
+@item side
+Width of each side face in luma pixels. @code{0} is @code{(iw-center)/2}.
+@item center
+Width of the center face. @code{0} is the input height.
+@item blend
+Overlap blend width in luma pixels. Default 48.
+@item yaw
+Yaw rotation of the output equirect in degrees. Default 0.
+@end table
+
+@example
+ffmpeg -i INPUT.360 -filter_complex "[0:0][0:4]gopromax2=yaw=-90" -frames:v 1 
out.jpg
+@end example
+
 @section v360
 
 Convert 360 videos between various formats.
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index ceea328e67..ece097706e 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -336,6 +336,7 @@ OBJS-$(CONFIG_FSYNC_FILTER)                  += vf_fsync.o
 OBJS-$(CONFIG_GBLUR_FILTER)                  += vf_gblur.o
 OBJS-$(CONFIG_GBLUR_VULKAN_FILTER)           += vf_gblur_vulkan.o vulkan.o 
vulkan_filter.o
 OBJS-$(CONFIG_GEQ_FILTER)                    += vf_geq.o
+OBJS-$(CONFIG_GOPROMAX2_FILTER)              += vf_gopromax2.o framesync.o
 OBJS-$(CONFIG_GRADFUN_FILTER)                += vf_gradfun.o
 OBJS-$(CONFIG_GRAPHMONITOR_FILTER)           += f_graphmonitor.o
 OBJS-$(CONFIG_GRAYWORLD_FILTER)              += vf_grayworld.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 3b93fe12f6..5ecddd86cd 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -310,6 +310,7 @@ extern const FFFilter ff_vf_fsync;
 extern const FFFilter ff_vf_gblur;
 extern const FFFilter ff_vf_gblur_vulkan;
 extern const FFFilter ff_vf_geq;
+extern const FFFilter ff_vf_gopromax2;
 extern const FFFilter ff_vf_gradfun;
 extern const FFFilter ff_vf_graphmonitor;
 extern const FFFilter ff_vf_grayworld;
diff --git a/libavfilter/vf_gopromax2.c b/libavfilter/vf_gopromax2.c
new file mode 100644
index 0000000000..806d55a737
--- /dev/null
+++ b/libavfilter/vf_gopromax2.c
@@ -0,0 +1,482 @@
+/*
+ * GoPro Max 2 .360 to equirectangular (CPU).
+ * Dual-EAC side/center/side stitch to 2:1 equirectangular.
+ *
+ * Copyright (c) 2021 Ronan LE MEILLAT
+ * Copyright (c) 2024 TADANO Tokumei
+ * Copyright (c) 2026 Reinhard Nißl
+ *
+ * Based on gopromax_opencl (Max 1) by Ronan LE MEILLAT and TADANO Tokumei.
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg 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.
+ *
+ * FFmpeg 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 FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <math.h>
+
+#include "libavutil/common.h"
+#include "libavutil/opt.h"
+#include "libavutil/pixdesc.h"
+
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+
+#include "avfilter.h"
+#include "filters.h"
+#include "framesync.h"
+#include "video.h"
+
+enum {
+    FACE_LEFT,
+    FACE_RIGHT,
+    FACE_FRONT,
+    FACE_BACK,
+    FACE_TOP,
+    FACE_DOWN
+};
+
+typedef struct GoProMax2Context {
+    const AVClass *class;
+
+    FFFrameSync fs;
+
+    int width, height;
+    int side, center, blend;
+    float yaw;
+
+    int in_w, in_h;
+    int out_w, out_h;
+    int layout_side, layout_center, layout_blend;
+    int nb_planes;
+    int depth;
+    int maxval;
+    int log2_chroma_w, log2_chroma_h;
+} GoProMax2Context;
+
+typedef struct ThreadData {
+    AVFrame *front, *rear, *dst;
+    float yaw;
+} ThreadData;
+
+static const enum AVPixelFormat pix_fmts[] = {
+    AV_PIX_FMT_YUV420P,  AV_PIX_FMT_YUV422P,  AV_PIX_FMT_YUV444P,
+    AV_PIX_FMT_YUVJ420P, AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUVJ444P,
+    AV_PIX_FMT_YUV420P10, AV_PIX_FMT_YUV422P10, AV_PIX_FMT_YUV444P10,
+    AV_PIX_FMT_YUV420P12, AV_PIX_FMT_YUV422P12, AV_PIX_FMT_YUV444P12,
+    AV_PIX_FMT_YUV420P16, AV_PIX_FMT_YUV422P16, AV_PIX_FMT_YUV444P16,
+    AV_PIX_FMT_GBRP, AV_PIX_FMT_GBRP10, AV_PIX_FMT_GBRP12, AV_PIX_FMT_GBRP16,
+    AV_PIX_FMT_NONE
+};
+
+static av_always_inline float sample_u8(const uint8_t *data, int linesize,
+                                        int w, int h, float x, float y)
+{
+    int x0, y0, x1, y1;
+    float fx, fy, a, b;
+
+    x = av_clipf(x, 0.f, (float)(w - 1));
+    y = av_clipf(y, 0.f, (float)(h - 1));
+    x0 = (int)x;
+    y0 = (int)y;
+    x1 = FFMIN(x0 + 1, w - 1);
+    y1 = FFMIN(y0 + 1, h - 1);
+    fx = x - x0;
+    fy = y - y0;
+    a = data[y0 * linesize + x0] * (1.f - fx) + data[y0 * linesize + x1] * fx;
+    b = data[y1 * linesize + x0] * (1.f - fx) + data[y1 * linesize + x1] * fx;
+    return a * (1.f - fy) + b * fy;
+}
+
+static av_always_inline float sample_u16(const uint8_t *data, int linesize,
+                                         int w, int h, float x, float y)
+{
+    const uint16_t *row0, *row1;
+    int x0, y0, x1, y1;
+    float fx, fy, a, b;
+
+    x = av_clipf(x, 0.f, (float)(w - 1));
+    y = av_clipf(y, 0.f, (float)(h - 1));
+    x0 = (int)x;
+    y0 = (int)y;
+    x1 = FFMIN(x0 + 1, w - 1);
+    y1 = FFMIN(y0 + 1, h - 1);
+    fx = x - x0;
+    fy = y - y0;
+    row0 = (const uint16_t *)(data + (size_t)y0 * linesize);
+    row1 = (const uint16_t *)(data + (size_t)y1 * linesize);
+    a = row0[x0] * (1.f - fx) + row0[x1] * fx;
+    b = row1[x0] * (1.f - fx) + row1[x1] * fx;
+    return a * (1.f - fy) + b * fy;
+}
+
+static int find_face(float lon, float lat, float *u, float *v)
+{
+    float cl = cosf(lat);
+    float px = cl * sinf(lon);
+    float py = cl * cosf(lon);
+    float pz = sinf(lat);
+    float ax = fabsf(px), ay = fabsf(py), az = fabsf(pz);
+    float dom, k, qx, qy, qz;
+    int face = FACE_FRONT;
+
+    if (ax >= ay && ax >= az)
+        face = px < 0.f ? FACE_LEFT : FACE_RIGHT;
+    else if (ay > ax && ay >= az)
+        face = py >= 0.f ? FACE_FRONT : FACE_BACK;
+    else if (az > ax && az > ay)
+        face = pz >= 0.f ? FACE_TOP : FACE_DOWN;
+
+    dom = ax;
+    if (face == FACE_FRONT || face == FACE_BACK)
+        dom = ay;
+    else if (face == FACE_TOP || face == FACE_DOWN)
+        dom = az;
+    if (dom < 1e-12f)
+        dom = 1e-12f;
+
+    k = 4.f / (float)M_PI;
+    qx = atanf(px / dom) * k;
+    qy = atanf(py / dom) * k;
+    qz = atanf(pz / dom) * k;
+
+    switch (face) {
+    case FACE_LEFT:  *u = (qy + 1.f) * 0.5f; *v = (qz + 1.f) * 0.5f; break;
+    case FACE_RIGHT: *u = (1.f - qy) * 0.5f; *v = (qz + 1.f) * 0.5f; break;
+    case FACE_FRONT: *u = (qx + 1.f) * 0.5f; *v = (qz + 1.f) * 0.5f; break;
+    case FACE_BACK:  *u = (1.f - qx) * 0.5f; *v = (qz + 1.f) * 0.5f; break;
+    case FACE_TOP:   *u = (1.f - qx) * 0.5f; *v = (qy + 1.f) * 0.5f; break;
+    default:         *u = (1.f - qx) * 0.5f; *v = (1.f - qy) * 0.5f; break;
+    }
+    *u = av_clipf(*u, 0.f, 1.f - 1e-6f);
+    *v = av_clipf(*v, 0.f, 1.f - 1e-6f);
+    return face;
+}
+
+static float get_colour(int face, float u, float v,
+                        const uint8_t *front, int front_ls,
+                        const uint8_t *rear, int rear_ls,
+                        int src_w, int src_h,
+                        int side, int center, int blendw, int hi)
+{
+    const uint8_t *src;
+    int use_front, x0, w, src_ls;
+    float duv, uL, uR, y, c1, c2;
+
+    if (face == FACE_DOWN || face == FACE_BACK || face == FACE_TOP) {
+        float nu = v;
+        float nv = 1.f - 1e-6f - u;
+        u = nu;
+        v = nv;
+    }
+    v = 1.f - 1e-6f - v;
+    u = av_clipf(u, 0.f, 1.f - 1e-6f);
+    v = av_clipf(v, 0.f, 1.f - 1e-6f);
+
+    use_front = (face == FACE_FRONT || face == FACE_LEFT || face == 
FACE_RIGHT);
+    src = use_front ? front : rear;
+    src_ls = use_front ? front_ls : rear_ls;
+
+    if (face == FACE_FRONT || face == FACE_BACK) {
+        x0 = side;
+        w = center;
+        if (hi)
+            return sample_u16(src, src_ls, src_w, src_h,
+                              x0 + u * (float)w, v * (float)src_h);
+        return sample_u8(src, src_ls, src_w, src_h,
+                         x0 + u * (float)w, v * (float)src_h);
+    }
+
+    if (face == FACE_LEFT || face == FACE_DOWN) {
+        x0 = 0;
+        w = side;
+    } else {
+        x0 = side + center;
+        w = side;
+    }
+
+    duv = (float)blendw / (float)w;
+    uL = 2.f * (0.5f - duv) * u;
+    uR = 2.f * (0.5f - duv) * (u - 0.5f) + 0.5f + duv;
+    y = v * (float)src_h;
+    if (uL <= 0.5f - 2.f * duv) {
+        if (hi)
+            return sample_u16(src, src_ls, src_w, src_h, x0 + uL * (float)w, 
y);
+        return sample_u8(src, src_ls, src_w, src_h, x0 + uL * (float)w, y);
+    }
+    if (uR >= 0.5f + 2.f * duv) {
+        if (hi)
+            return sample_u16(src, src_ls, src_w, src_h, x0 + uR * (float)w, 
y);
+        return sample_u8(src, src_ls, src_w, src_h, x0 + uR * (float)w, y);
+    }
+    if (hi) {
+        c1 = sample_u16(src, src_ls, src_w, src_h, x0 + uL * (float)w, y);
+        c2 = sample_u16(src, src_ls, src_w, src_h, x0 + uR * (float)w, y);
+    } else {
+        c1 = sample_u8(src, src_ls, src_w, src_h, x0 + uL * (float)w, y);
+        c2 = sample_u8(src, src_ls, src_w, src_h, x0 + uR * (float)w, y);
+    }
+    return c1 + (c2 - c1) * ((uL - 0.5f + 2.f * duv) / (2.f * duv));
+}
+
+static void plane_layout(const GoProMax2Context *s, int plane,
+                         int *pw, int *ph, int *side, int *center, int *blend)
+{
+    *pw = s->in_w;
+    *ph = s->in_h;
+    if (plane == 1 || plane == 2) {
+        *pw = AV_CEIL_RSHIFT(s->in_w, s->log2_chroma_w);
+        *ph = AV_CEIL_RSHIFT(s->in_h, s->log2_chroma_h);
+    }
+    *side   = s->layout_side   * *pw / s->in_w;
+    *center = s->layout_center * *ph / s->in_h;
+    *blend  = FFMAX(1, s->layout_blend * *pw / s->in_w);
+}
+
+static int filter_slice(AVFilterContext *ctx, void *arg, int jobnr, int 
nb_jobs)
+{
+    GoProMax2Context *s = ctx->priv;
+    ThreadData *td = arg;
+    int plane, hi = s->depth > 8;
+
+    for (plane = 0; plane < s->nb_planes; plane++) {
+        int src_w, src_h, side, center, blend;
+        int dst_w, dst_h, y0, y1, x, y;
+        uint8_t *dst;
+
+        plane_layout(s, plane, &src_w, &src_h, &side, &center, &blend);
+        dst_w = plane == 1 || plane == 2 ?
+            AV_CEIL_RSHIFT(s->out_w, s->log2_chroma_w) : s->out_w;
+        dst_h = plane == 1 || plane == 2 ?
+            AV_CEIL_RSHIFT(s->out_h, s->log2_chroma_h) : s->out_h;
+
+        y0 = (dst_h * jobnr) / nb_jobs;
+        y1 = (dst_h * (jobnr + 1)) / nb_jobs;
+        dst = td->dst->data[plane];
+
+        for (y = y0; y < y1; y++) {
+            for (x = 0; x < dst_w; x++) {
+                float lat = (float)M_PI * 0.5f - (y + 0.5f) / (float)dst_h * 
(float)M_PI;
+                float lon = (x + 0.5f) / (float)dst_w * 2.f * (float)M_PI - 
(float)M_PI + td->yaw;
+                float u, v, val;
+                int face;
+
+                if (lon > (float)M_PI)
+                    lon -= 2.f * (float)M_PI;
+                if (lon < -(float)M_PI)
+                    lon += 2.f * (float)M_PI;
+
+                face = find_face(lon, lat, &u, &v);
+                val = get_colour(face, u, v,
+                                 td->front->data[plane], 
td->front->linesize[plane],
+                                 td->rear->data[plane], 
td->rear->linesize[plane],
+                                 src_w, src_h, side, center, blend, hi);
+                if (hi) {
+                    uint16_t *row = (uint16_t *)(dst + (size_t)y * 
td->dst->linesize[plane]);
+                    row[x] = av_clip(lrintf(val), 0, s->maxval);
+                } else {
+                    dst[y * td->dst->linesize[plane] + x] =
+                        av_clip_uint8(lrintf(val));
+                }
+            }
+        }
+    }
+    return 0;
+}
+
+static int gopromax2_filter(FFFrameSync *fs)
+{
+    AVFilterContext *avctx = fs->parent;
+    AVFilterLink *outlink = avctx->outputs[0];
+    GoProMax2Context *s = avctx->priv;
+    AVFrame *front, *rear, *out;
+    ThreadData td;
+    int ret;
+
+    ret = ff_framesync_get_frame(fs, 0, &front, 0);
+    if (ret < 0)
+        return ret;
+    ret = ff_framesync_get_frame(fs, 1, &rear, 0);
+    if (ret < 0)
+        return ret;
+
+    out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
+    if (!out)
+        return AVERROR(ENOMEM);
+
+    ret = av_frame_copy_props(out, front);
+    if (ret < 0) {
+        av_frame_free(&out);
+        return ret;
+    }
+
+    td.front = front;
+    td.rear  = rear;
+    td.dst   = out;
+    td.yaw   = s->yaw * (float)M_PI / 180.f;
+
+    ff_filter_execute(avctx, filter_slice, &td, NULL,
+                      FFMIN(s->out_h, ff_filter_get_nb_threads(avctx)));
+
+    return ff_filter_frame(outlink, out);
+}
+
+static int config_output(AVFilterLink *outlink)
+{
+    AVFilterContext *avctx = outlink->src;
+    GoProMax2Context *s = avctx->priv;
+    AVFilterLink *frontlink = avctx->inputs[0];
+    AVFilterLink *rearlink  = avctx->inputs[1];
+    FilterLink *inl = ff_filter_link(frontlink);
+    FilterLink *outl = ff_filter_link(outlink);
+    const AVPixFmtDescriptor *desc;
+    int ret;
+
+    if (frontlink->w != rearlink->w || frontlink->h != rearlink->h) {
+        av_log(avctx, AV_LOG_ERROR, "Front and rear must be the same size.\n");
+        return AVERROR(EINVAL);
+    }
+    if (frontlink->format != rearlink->format) {
+        av_log(avctx, AV_LOG_ERROR, "Front and rear must be the same pixel 
format.\n");
+        return AVERROR(EINVAL);
+    }
+
+    s->in_w = frontlink->w;
+    s->in_h = frontlink->h;
+    s->layout_center = s->center > 0 ? s->center : s->in_h;
+    s->layout_side   = s->side   > 0 ? s->side   : (s->in_w - 
s->layout_center) / 2;
+    s->layout_blend  = s->blend  > 0 ? s->blend  : 48;
+
+    if (s->layout_side <= 0 || s->layout_center <= 0 ||
+        2 * s->layout_side + s->layout_center != s->in_w) {
+        av_log(avctx, AV_LOG_ERROR,
+               "Max 2 layout mismatch: input %dx%d, side %d center %d "
+               "(need 2*side+center = width).\n",
+               s->in_w, s->in_h, s->layout_side, s->layout_center);
+        return AVERROR(EINVAL);
+    }
+
+    if (s->width > 0 && s->height > 0) {
+        if (s->width != s->height * 2) {
+            av_log(avctx, AV_LOG_ERROR,
+                   "Specified size (%dx%d) is not 2:1 equirect.\n",
+                   s->width, s->height);
+            return AVERROR(EINVAL);
+        }
+        s->out_w = s->width;
+        s->out_h = s->height;
+    } else if (s->width > 0 || s->height > 0) {
+        av_log(avctx, AV_LOG_ERROR,
+               "Both width and height values should be specified.\n");
+        return AVERROR(EINVAL);
+    } else {
+        s->out_w = 4 * s->in_h;
+        s->out_h = 2 * s->in_h;
+    }
+
+    desc = av_pix_fmt_desc_get(frontlink->format);
+    s->nb_planes = av_pix_fmt_count_planes(frontlink->format);
+    s->depth = desc->comp[0].depth;
+    s->maxval = (1 << s->depth) - 1;
+    s->log2_chroma_w = desc->log2_chroma_w;
+    s->log2_chroma_h = desc->log2_chroma_h;
+
+    outlink->w = s->out_w;
+    outlink->h = s->out_h;
+    outlink->sample_aspect_ratio = (AVRational){ 1, 1 };
+    outlink->time_base = frontlink->time_base;
+    outl->frame_rate = inl->frame_rate;
+
+    av_log(avctx, AV_LOG_INFO,
+           "gopromax2 in %dx%d side %d center %d blend %d yaw %.1f out 
%dx%d\n",
+           s->in_w, s->in_h, s->layout_side, s->layout_center, s->layout_blend,
+           s->yaw, s->out_w, s->out_h);
+
+    ret = ff_framesync_init_dualinput(&s->fs, avctx);
+    if (ret < 0)
+        return ret;
+
+    ret = ff_framesync_configure(&s->fs);
+    if (ret < 0)
+        return ret;
+
+    outlink->time_base = s->fs.time_base;
+    return 0;
+}
+
+static av_cold int gopromax2_init(AVFilterContext *avctx)
+{
+    GoProMax2Context *s = avctx->priv;
+    s->fs.on_event = &gopromax2_filter;
+    return 0;
+}
+
+static int gopromax2_activate(AVFilterContext *avctx)
+{
+    GoProMax2Context *s = avctx->priv;
+    return ff_framesync_activate(&s->fs);
+}
+
+static av_cold void gopromax2_uninit(AVFilterContext *avctx)
+{
+    GoProMax2Context *s = avctx->priv;
+    ff_framesync_uninit(&s->fs);
+}
+
+#define OFFSET(x) offsetof(GoProMax2Context, x)
+#define FLAGS (AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM)
+static const AVOption gopromax2_options[] = {
+    { "w",      "output width",  OFFSET(width),  AV_OPT_TYPE_INT,   {.i64=0},  
0, INT16_MAX, FLAGS },
+    { "h",      "output height", OFFSET(height), AV_OPT_TYPE_INT,   {.i64=0},  
0, INT16_MAX, FLAGS },
+    { "side",   "side face width in luma pixels (0 = (in_w-center)/2)",
+                                 OFFSET(side),   AV_OPT_TYPE_INT,   {.i64=0},  
0, INT16_MAX, FLAGS },
+    { "center", "center face width in luma pixels (0 = in_h)",
+                                 OFFSET(center), AV_OPT_TYPE_INT,   {.i64=0},  
0, INT16_MAX, FLAGS },
+    { "blend",  "overlap blend width in luma pixels",
+                                 OFFSET(blend),  AV_OPT_TYPE_INT,   {.i64=48}, 
1,      1024, FLAGS },
+    { "yaw",    "yaw rotation of the equirect in degrees",
+                                 OFFSET(yaw),    AV_OPT_TYPE_FLOAT, {.dbl=0}, 
-360,     360, FLAGS },
+    { NULL },
+};
+
+AVFILTER_DEFINE_CLASS(gopromax2);
+
+static const AVFilterPad gopromax2_inputs[] = {
+    { .name = "front", .type = AVMEDIA_TYPE_VIDEO },
+    { .name = "rear",  .type = AVMEDIA_TYPE_VIDEO },
+};
+
+static const AVFilterPad gopromax2_outputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = &config_output,
+    },
+};
+
+const FFFilter ff_vf_gopromax2 = {
+    .p.name          = "gopromax2",
+    .p.description   = NULL_IF_CONFIG_SMALL("GoPro Max 2 .360 to 
equirectangular projection"),
+    .p.priv_class    = &gopromax2_class,
+    .p.flags         = AVFILTER_FLAG_SLICE_THREADS,
+    .priv_size       = sizeof(GoProMax2Context),
+    .init            = &gopromax2_init,
+    .uninit          = &gopromax2_uninit,
+    .activate        = &gopromax2_activate,
+    FILTER_INPUTS(gopromax2_inputs),
+    FILTER_OUTPUTS(gopromax2_outputs),
+    FILTER_PIXFMTS_ARRAY(pix_fmts),
+};
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to