GLMakie: recording a movie while interacting with a Scene produces a StackOverflow

tamasgal
Fri, 14 Nov 2025 10:30:09 +0000
Visualization

I am using GLMakie.jl in a GUI application (RainbowAlga.jl) which is an interactive event display for neutrino interactions in water-based Cherenkov detectors. The application works really nicely but I am currently stuck on the most-requested features: video recording.

What I would like to implement is toggling video recording with a keypress, e.g. pressing V to start recording and pressing V again to stop and save the video. During these two events, the user should be able to interact with the display.

OK, long story short, the following MWE resembles the structure of my application, creates a window, a basegrid and rotates the camera in the renderloop. There are two keyboard events registered:

P to save a screenshot (works fine, just like in the original app)

V to start/top video recording → immediately crashing when creating VideoStream(scene) and after a while it shows a StackOverflow

How can I achieve this functionality? Starting/stopping live video recording while viewing the scene…

using GLMakie


"""

A non-evel global state to manage the video recording.

"""
mutable struct VideoRecordingState
    isrecording::Bool
    videostream::Union{VideoStream, Nothing}
end

const GLOBAL_VIDEO_RECORDING_STATE = Ref{Union{VideoRecordingState, Nothing}}(nothing)


function global_vrs()
    if isnothing(GLOBAL_VIDEO_RECORDING_STATE[])
        GLOBAL_VIDEO_RECORDING_STATE[] = VideoRecordingState(false, nothing)
    end
    GLOBAL_VIDEO_RECORDING_STATE[]
end


function main()
    scene = Scene(backgroundcolor=RGBf(1.0))
    cam = cam3d!(scene, rotation_center = :lookat)
    screen = display(GLMakie.Screen(start_renderloop=true, focus_on_show=true), scene)

    basegrid!(scene)  # against whiteout

    register_events(scene)

    on(screen.render_tick) do tick
        rotate_cam!(scene, Vec3f(0, 0.001, 0))

        if global_vrs().isrecording
            # can't even reach this block due to the stackoverflow caused by VideoStream(scene)
            recordframe!(scene)
        end
    end

    wait(screen)
end


"""

Registers callbacks for user inputs.

"""
function register_events(scene)
    on(events(scene).keyboardbutton) do event
        if ispressed(scene, Makie.Keyboard.p)
            fname = "screenshot.png"
            save(fname, scene)
            println("Screenshot saved as '$(fname)'")
            return Consume()
        end
        if ispressed(scene, Makie.Keyboard.v)
            vrs = global_vrs()
            if !vrs.isrecording
                println("Video recording started")
                vrs.isrecording = true
                vrs.videostream = VideoStream(scene)  # stackoverflow!
            else
                println("Video recording stopped")
                vrs.isrecording = false
                # not sure how to close properly, for now just GC-feeding it
                save("movie.mp4", vrs.videostream)
                vrs.videostream = nothing
            end
        end
    end
end


"""

Draws a basegrid into the scene's xy-plane.

"""
function basegrid!(scene; center=(0, 0, 0), span=(-1, 1), spacing=0.1, linewidth=1, color=(:grey, 0.3))
    min, max = span
    center = Point3f(center)
    for q ∈ range(min, max; step=spacing)
        lines!(scene, [Point3f(q, min, 0) + center, Point3f(q, max, 0) + center], color=color, linewidth=linewidth)
        lines!(scene, [Point3f(min, q, 0) + center, Point3f(max, q, 0) + center], color=color, linewidth=linewidth)
    end
    scene
end


main()

Btw. I already created scripts which programmatically handle the camera movement and export videos (see https://www.youtube.com/watch?v=5pOz4qdnS1s) but for that, I had to juggle around with the scene creation since the GUI was not required.
Instead of using screen = display(GLMakie.Screen(start_rendeloop=true, ...), I had to create a Figure with the desired dimensions, create an LScene and then replacing it’s scene with my own renderer, like

    fig = Figure(size = (3840, 2160), figure_padding = 0, backgroundcolor = bgcolor)
    lscene = LScene(fig[1, 1]; show_axis = false, scenekw = (; size = (3840, 2160)))
    scene = lscene.scene

This worked prefectly fine with a record(fig, ...) do t block:

    record(fig, "uhe_animation.mp4", time_iterator; framerate = framerate) do t
        cam_lookat =
            cam_lookat_start + frame_idx / nframes * (cam_lookat_end - cam_lookat_start)
        cam_pos = cam_pos_start + frame_idx / nframes * (cam_pos_end - cam_pos_start)
        update_cam!(scene, cam, cam_pos, cam_lookat, Vec3f(0, 0, 1))

        # some additional screen manipulation logic...

        frame_idx += 1
        next!(p)
    end

7 posts - 2 participants