]> git.sesse.net Git - nageru/commitdiff
Allow dynamic frame sizes for FFmpeg inputs.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 13 May 2024 21:45:46 +0000 (23:45 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 13 May 2024 21:45:46 +0000 (23:45 +0200)
If we need a larger frame size than FRAME_SIZE for a frame,
simply reallocate it; we're allowed to sleep in this path.

This finally allows us to accept 2160p streams (assuming your
GPU is fast enough, of course), at the cost of having to make
an OpenGL context for each decoder thread (because allocating
frames in GPU memory requires making a PBO and stuff).
Unfortunately, we still cannot decode 2160p DeckLink frames
because there's no OpenGL context there, but it's a good start.

nageru/decklink_capture.cpp
nageru/defs.h
nageru/ffmpeg_capture.cpp
nageru/ffmpeg_capture.h
nageru/kaeru.cpp
nageru/mixer.cpp
nageru/pbo_frame_allocator.cpp
nageru/theme.cpp
nageru/theme.h

index e239ec964f6f70d17fc13c24494ead77e7d726b4..90d86b15dd580f0fe73d96148fa4f728ff280c9f 100644 (file)
@@ -256,7 +256,14 @@ HRESULT STDMETHODCALLTYPE DeckLinkCapture::VideoInputFrameArrived(
                        assert(stride == width * 2);
                }
 
-               current_video_frame = video_frame_allocator->create_frame(width, height, stride);
+               if (width * stride > FRAME_SIZE) {
+                       // TODO: If we had an OpenGL context here, calling create_frame()
+                       // would be completely fine.
+                       fprintf(stderr, "Card %u: Captured frame %d x %d (stride %d) would be larger than supported frame size (%d > %d), skipping.\n",
+                               card_index, width, height, stride, width * stride, FRAME_SIZE);
+               } else {
+                       current_video_frame = video_frame_allocator->create_frame(width, height, stride);
+               }
                if (current_video_frame.data != nullptr) {
                        const uint8_t *src;
                        video_frame->GetBytes((void **)&src);
index 09743a5abe5e72a9a8c12487c28d4fd756daeb5f..369c9b8ba504845a6930738846289bc491a0e52c 100644 (file)
@@ -6,7 +6,18 @@
 // #define MAX_VIDEO_CARDS 16  // defined in shared_defs.h.
 #define MAX_ALSA_CARDS 16
 #define MAX_BUSES 256  // Audio buses.
-#define FRAME_SIZE (8 << 20)  // 8 MB. (FIXME: Not enough for a 2160p frame!)
+
+// FRAME_SIZE is the default frame size, in bytes. FFmpeg inputs (video files and SRT streams)
+// can allocate larger frames as needed; USB and DeckLink outputs always use FRAME_SIZE.
+// We should eventually add support for at least DeckLink outputs, allowing us to capture
+// 2160p frames. Also, it would allow us to lower the default frame size to the maximum
+// bmusb supports (2 MB just about covers 1080i 4:2:2, then add some for 10-bit?) to waste
+// less memory.
+//
+// As a general sanity check, we also have a MAX_FRAME_SIZE that even dynamic allocation
+// will not go past.
+#define FRAME_SIZE (8 << 20)  // 8 MB (just enough for 1080p RGBA).
+#define MAX_FRAME_SIZE (140 << 20)  // 140 MB; enough for 8192*4320 RGBA and then some.
 
 // For deinterlacing. See also comments on InputState.
 #define FRAME_HISTORY_LENGTH 5
index 38ac591c5d59c7364629693586e6fe18b5390230..b00cab614c2315681309a6963110f182ba485117 100644 (file)
@@ -4,6 +4,7 @@
 
 #include <assert.h>
 #include <errno.h>
+#include <epoxy/egl.h>
 #include <limits>
 #include <map>
 #include <memory>
@@ -20,6 +21,7 @@
 #include <string>
 #include <sys/stat.h>
 #include <thread>
+#include <QSurface>
 
 extern "C" {
 #include <libavcodec/avcodec.h>
@@ -56,6 +58,7 @@ extern "C" {
 #include <movit/colorspace_conversion_effect.h>
 
 #include "bmusb/bmusb.h"
+#include "shared/context.h"
 #include "shared/ffmpeg_raii.h"
 #include "ffmpeg_util.h"
 #include "flags.h"
@@ -283,8 +286,8 @@ RGBTriplet get_neutral_color(AVDictionary *metadata)
 
 }  // namespace
 
-FFmpegCapture::FFmpegCapture(const string &filename, unsigned width, unsigned height)
-       : filename(filename), width(width), height(height), video_timebase{1, 1}
+FFmpegCapture::FFmpegCapture(const string &filename, unsigned width, unsigned height, QSurface *surface)
+       : filename(filename), width(width), height(height), video_timebase{1, 1}, surface(surface)
 {
        description = "Video: " + filename;
 
@@ -294,12 +297,13 @@ FFmpegCapture::FFmpegCapture(const string &filename, unsigned width, unsigned he
 }
 
 #ifdef HAVE_SRT
-FFmpegCapture::FFmpegCapture(int srt_sock, const string &stream_id)
+FFmpegCapture::FFmpegCapture(int srt_sock, const string &stream_id, QSurface *surface)
        : srt_sock(srt_sock),
          width(0),  // Don't resize; SRT streams typically have stable resolution, and should behave much like regular cards in general.
          height(0),
          pixel_format(bmusb::PixelFormat_8BitYCbCrPlanar),
-         video_timebase{1, 1}
+         video_timebase{1, 1},
+         surface(surface)
 {
        if (stream_id.empty()) {
                description = "SRT stream";
@@ -323,6 +327,7 @@ FFmpegCapture::~FFmpegCapture()
                srt_close(srt_sock);
        }
 #endif
+       delete surface;
 }
 
 void FFmpegCapture::configure_card()
@@ -383,6 +388,21 @@ void FFmpegCapture::producer_thread_func()
        snprintf(thread_name, sizeof(thread_name), "FFmpeg_C_%d", card_index);
        pthread_setname_np(pthread_self(), thread_name);
 
+       // We need a context in case create_frame() needs to reallocate something.
+       // (If none is given, we are probably in Kaeru, which uses MallocFrameAllocator
+       // anyway, which doesn't reallocate currently and definitely doesn't need
+       // an active OpenGL context to do so.)
+       QOpenGLContext *context = nullptr;
+       if (surface != nullptr) {
+               context = create_context(this->surface);
+               eglBindAPI(EGL_OPENGL_API);
+               if (!make_current(context, this->surface)) {
+                       printf("display=%p surface=%p context=%p curr=%p err=%d\n", eglGetCurrentDisplay(), this->surface, context, eglGetCurrentContext(),
+                               eglGetError());
+                       abort();
+               }
+       }
+
        while (!producer_thread_should_quit.should_quit()) {
                string filename_copy;
                {
@@ -429,6 +449,8 @@ void FFmpegCapture::producer_thread_func()
                dequeue_cleanup_callback();
                has_dequeue_callbacks = false;
        }
+
+       delete_context(context);
 }
 
 void FFmpegCapture::send_disconnected_frame()
@@ -1125,7 +1147,7 @@ UniqueFrame FFmpegCapture::make_video_frame(const AVFrame *frame, const string &
 {
        *error = false;
 
-       UniqueFrame video_frame(video_frame_allocator->alloc_frame());
+       UniqueFrame video_frame(video_frame_allocator->create_frame(frame->width, frame->height, frame->width));
        if (video_frame->data == nullptr) {
                return video_frame;
        }
@@ -1190,8 +1212,8 @@ UniqueFrame FFmpegCapture::make_video_frame(const AVFrame *frame, const string &
        // FIXME: Currently, if the video is too high-res for one of the allocated
        // frames, we simply refuse to scale it here to avoid crashes. It would be better
        // if we could somehow signal getting larger frames, especially as 4K is a thing now.
-       if (video_frame->len > FRAME_SIZE) {
-               fprintf(stderr, "%s: Decoded frame would be larger than supported FRAME_SIZE (%zu > %u), not decoding.\n", pathname.c_str(), video_frame->len, FRAME_SIZE);
+       if (video_frame->len > video_frame->size) {
+               fprintf(stderr, "%s: Decoded frame would be larger than supported frame size (%zu > %zu), not decoding.\n", pathname.c_str(), video_frame->len, video_frame->size);
                *error = true;
                return video_frame;
        }
index 122bf86726ba4cb6d73a70a0ca81166d6e7d7431..b974583a10ef22d3f339859513d74c4797bcb673 100644 (file)
@@ -60,14 +60,15 @@ struct AVFormatContext;
 struct AVFrame;
 struct AVRational;
 struct AVPacket;
+class QSurface;
 
 class FFmpegCapture : public bmusb::CaptureInterface
 {
 public:
-       FFmpegCapture(const std::string &filename, unsigned width, unsigned height);
+       FFmpegCapture(const std::string &filename, unsigned width, unsigned height, QSurface *surface);
 #ifdef HAVE_SRT
        // Takes ownership of the SRT client socket.
-       FFmpegCapture(int srt_sock, const std::string &stream_id);
+       FFmpegCapture(int srt_sock, const std::string &stream_id, QSurface *surface);
 #endif
        ~FFmpegCapture();
 
@@ -351,6 +352,7 @@ private:
        // -1 is strictly speaking outside the range of the enum, but hopefully, it will be alright.
        AVColorSpace last_colorspace = static_cast<AVColorSpace>(-1);
        AVChromaLocation last_chroma_location = static_cast<AVChromaLocation>(-1);
+       QSurface *const surface;
 };
 
 #endif  // !defined(_FFMPEG_CAPTURE_H)
index 2bdc6861f2dffa5a17a0d81922e38adcbaf0b95b..caa71c6527fc3bd0cdde79dc88cc9154bae34e5b 100644 (file)
@@ -267,7 +267,7 @@ int main(int argc, char *argv[])
        }
        global_x264_encoder = x264_encoder.get();
 
-       FFmpegCapture video(argv[optind], global_flags.width, global_flags.height);
+       FFmpegCapture video(argv[optind], global_flags.width, global_flags.height, /*surface=*/nullptr);
        video.set_pixel_format(FFmpegCapture::PixelFormat_NV12);
        if (global_flags.transcode_video) {
                video.set_frame_callback(bind(video_frame_callback, &video, x264_encoder.get(), audio_encoder.get(), _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11));
index 78f9ebfcb497d52486db9a7c4430515ce653642c..e766d8864d89023d2d451bc75107ef1d5630c340 100644 (file)
@@ -409,7 +409,7 @@ Mixer::Mixer(const QSurfaceFormat &format)
        }
 
        // Must be instantiated after VideoEncoder has initialized global_flags.use_zerocopy.
-       theme.reset(new Theme(global_flags.theme_filename, global_flags.theme_dirs, resource_pool.get()));
+       theme.reset(new Theme(global_flags.theme_filename, global_flags.theme_dirs, resource_pool.get(), create_surface(format)));
 
        // Must be instantiated after the theme, as the theme decides the number of FFmpeg inputs.
        std::vector<FFmpegCapture *> video_inputs = theme->get_video_inputs();
@@ -1610,7 +1610,7 @@ void Mixer::handle_hotplugged_cards()
                                fprintf(stderr, "New SRT stream connected (%s), choosing slot %d.\n", stream_id.c_str(), free_card_index);
                        }
                        CaptureCard *card = &cards[free_card_index];
-                       FFmpegCapture *capture = new FFmpegCapture(sock, stream_id);
+                       FFmpegCapture *capture = new FFmpegCapture(sock, stream_id, create_surface_with_same_format(mixer_surface));
                        capture->set_card_index(free_card_index);
                        configure_card(free_card_index, capture, CardType::FFMPEG_INPUT, /*output=*/nullptr, /*is_srt_card=*/true);
                        card->srt_metrics.update_srt_stats(sock);  // Initial zero stats.
index 5910a0237fb6ce725072336666591a3c76f7799d..a649232edb91ff935f0f62cc737243bdbef374a4 100644 (file)
@@ -13,6 +13,7 @@
 #include <va/va.h>
 
 #include "mjpeg_encoder.h"
+#include "defs.h"
 #include "shared/va_resource_pool.h"
 #include "v210_converter.h"
 #include "shared/va_display.h"
@@ -310,6 +311,11 @@ bmusb::FrameAllocator::Frame PBOFrameAllocator::create_frame(size_t width, size_
 {
         Frame vf;
 
+       size_t desired_frame_bytes = width * stride;
+       if (stride > 8192 * 4 || height > 8192 || desired_frame_bytes > MAX_FRAME_SIZE) {
+               return vf;
+       }
+
        {
                lock_guard<mutex> lock(freelist_mutex);
                if (freelist.empty()) {
@@ -322,10 +328,22 @@ bmusb::FrameAllocator::Frame PBOFrameAllocator::create_frame(size_t width, size_
                        freelist.pop();
                }
        }
-       vf.len = 0;
-       vf.overflow = 0;
 
        Userdata *userdata = (Userdata *)vf.userdata;
+       assert(generation == userdata->generation);
+       if (vf.size < desired_frame_bytes || (vf.size > FRAME_SIZE && vf.size > desired_frame_bytes * 2)) {
+               // Frame is either too small or way too large, so reallocate it.
+               // Note that width and height now automatically becomes the right size
+               // (the one we just asked for, instead of the default for the allocator,
+               // which is generally the global resolution); it doesn't matter
+               // for correctness, since we'll recreate the texture on upload if needed,
+               // but it is nice to save that step.
+               destroy_frame(&vf);
+               init_frame(vf, userdata, this, pixel_format, std::max<size_t>(desired_frame_bytes, FRAME_SIZE), width, height, permissions, map_bits, buffer, generation);
+       };
+
+       vf.len = 0;
+       vf.overflow = 0;
 
        if (mjpeg_encoder != nullptr &&
            mjpeg_encoder->should_encode_mjpeg_for_card(card_index)) {
index 06d147f6686bb07739d7cbb5ee6bafa1ed1f66d5..afef822fe3666015b49450477e0a1b3ac68c2db2 100644 (file)
@@ -54,6 +54,7 @@
 #include "mainwindow.h"
 #include "pbo_frame_allocator.h"
 #include "scene.h"
+#include "shared/context.h"
 
 class Mixer;
 
@@ -450,6 +451,9 @@ int ImageInput_new(lua_State* L)
        return wrap_lua_object_nonowned<ImageInput>(L, "ImageInput", filename);
 }
 
+}  // namespace
+
+// Must be non-namespaced due to friend declaration.
 int VideoInput_new(lua_State* L)
 {
        assert(lua_gettop(L) == 2);
@@ -460,17 +464,19 @@ int VideoInput_new(lua_State* L)
                print_warning(L, "Invalid enum %d used for video format, choosing Y'CbCr.\n", pixel_format);
                pixel_format = bmusb::PixelFormat_8BitYCbCrPlanar;
        }
-       int ret = wrap_lua_object_nonowned<FFmpegCapture>(L, "VideoInput", filename, global_flags.width, global_flags.height);
+       Theme *theme = get_theme_updata(L);
+       int ret = wrap_lua_object_nonowned<FFmpegCapture>(L, "VideoInput", filename, global_flags.width, global_flags.height, create_surface_with_same_format(theme->surface));
        if (ret == 1) {
                FFmpegCapture **capture = (FFmpegCapture **)lua_touserdata(L, -1);
                (*capture)->set_pixel_format(bmusb::PixelFormat(pixel_format));
 
-               Theme *theme = get_theme_updata(L);
                theme->register_video_input(*capture);
        }
        return ret;
 }
 
+namespace {
+
 int VideoInput_rewind(lua_State* L)
 {
        assert(lua_gettop(L) == 1);
@@ -1513,8 +1519,8 @@ int Nageru_set_audio_bus_eq_level_db(lua_State *L)
        return 0;
 }
 
-Theme::Theme(const string &filename, const vector<string> &search_dirs, ResourcePool *resource_pool)
-       : resource_pool(resource_pool), signal_to_card_mapping(global_flags.default_stream_mapping)
+Theme::Theme(const string &filename, const vector<string> &search_dirs, ResourcePool *resource_pool, QSurface *surface)
+       : resource_pool(resource_pool), signal_to_card_mapping(global_flags.default_stream_mapping), surface(surface)
 {
        // Defaults.
        channel_names[0] = "Live";
@@ -1631,6 +1637,7 @@ Theme::~Theme()
 {
        theme_menu.reset();
        lua_close(L);
+       // Leak the surface.
 }
 
 void Theme::register_globals()
index 7fe0e8585a2b0993a359d362d6db95b08951d5ba..2c6021e4e103be0fddbebeb4145c24ddca37f3b4 100644 (file)
@@ -24,6 +24,7 @@ class Scene;
 class CEFCapture;
 class FFmpegCapture;
 class LiveInputWrapper;
+class QSurface;
 struct InputState;
 
 namespace movit {
@@ -89,7 +90,7 @@ struct InputStateInfo {
 
 class Theme {
 public:
-       Theme(const std::string &filename, const std::vector<std::string> &search_dirs, movit::ResourcePool *resource_pool);
+       Theme(const std::string &filename, const std::vector<std::string> &search_dirs, movit::ResourcePool *resource_pool, QSurface *surface);
        ~Theme();
 
        struct Chain {
@@ -244,6 +245,11 @@ private:
        std::map<unsigned, int> channel_signals;  // Set using Nageru.set_channel_signal(). Protected by <m>.
        std::map<unsigned, bool> channel_supports_wb;  // Set using Nageru.set_supports_wb(). Protected by <m>.
 
+       // Used to construct OpenGL contexts for VideoInputs. Needs to be available
+       // during the entire lifetime of Theme, since they may be created basically
+       // at any time.
+       const QSurface *surface;
+
        friend class LiveInputWrapper;
        friend class Scene;
        friend int ThemeMenu_set(lua_State *L);
@@ -251,6 +257,7 @@ private:
        friend int Nageru_set_num_channels(lua_State *L);
        friend int Nageru_set_channel_signal(lua_State *L);
        friend int Nageru_set_supports_wb(lua_State *L);
+       friend int VideoInput_new(lua_State* L);
 };
 
 // LiveInputWrapper is a facade on top of an YCbCrInput, exposed to