C.W.K.
Stream
Lesson 03 of 05 · published

Render Into a View You Own

~14 min · embed-engine, appkit, nsview, rendering

Level 0Reel Novice
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Render into an AppKit NSView using libmpv's render/client API."

Two Ways to Show the Video

An embedded engine has to draw somewhere. The lazy way is to let the engine open its own window and drive it — quick to start, and a dead end. That window isn't part of your app; your keyboard shortcuts don't reach it, your full-screen transitions fight it, your on-video overlays have nowhere to live. You end up with a remote control taped to a stranger's TV.

The right way is the one libmpv is designed for: its render API lets the engine draw into a surface you provide. Ashen Reel gives it an AppKit NSView, and the video renders inside the app's own window — inside the responder chain, under the overlays, part of the composition you control.

Why 'Inside Your Window' Changes Everything

Once the video lives in an NSView you own, a pile of hard features become nearly free. The seek overlay draws on top because it's a sibling view. Full screen and Spaces behave because it's a normal window. The keyboard chords reach it because it's in the responder chain. Double-click-to-fullscreen and scroll-to-change-volume work because the view receives the events. None of that is possible when the engine holds its own detached window.

Embed the engine into a surface you own; never let it own the window. The window is where your app's identity lives — its chrome, its input, its overlays. Rent the decoder, but keep the glass.

The render API works on a callback: libmpv signals from its own thread that a new frame is ready, you hop to the main thread and mark the view for redraw, and AppKit asks you to present. Here's the shape, simplified:

libmpv's update callback fires on libmpv's thread, not yours. Touching AppKit off the main thread is a crash waiting to happen. The one job of the callback is to hop to the main thread; everything UI happens there. This threading seam is exactly the kind of thing you want isolated in a small, well-understood view class.

The payoff is a player that feels native because it is native — the borrowed engine paints pixels into Ashen Reel's own glass, and every window, key, and overlay behaves like the AppKit app it actually is.

Code

Hosting libmpv's render output in an NSView (simplified)·swift
import AppKit
import CLibMPV

/// The AppKit view libmpv renders into. Video lives INSIDE our window, so
/// overlays, full screen, and the responder chain all just work.
final class PlayerView: NSView {
    private var render: OpaquePointer?   // mpv_render_context (params elided)

    func attach(_ mpv: OpaquePointer) {
        mpv_render_context_create(&render, mpv, /* render params */ nil)

        // libmpv calls this from ITS thread when a frame is ready.
        // Our only job here: hop to main and ask AppKit to redraw.
        mpv_render_context_set_update_callback(render, { ctx in
            let view = Unmanaged<PlayerView>.fromOpaque(ctx!).takeUnretainedValue()
            DispatchQueue.main.async { view.needsDisplay = true }
        }, Unmanaged.passUnretained(self).toOpaque())
    }

    // On redraw, present the next frame into this view's backing surface.
    // (mpv_render_context_render call elided for brevity.)
}

External links

Exercise

Think about any component you've embedded that came with its own UI surface — a web view, a charting widget, a video element, a game engine canvas. Did it draw into a container you controlled, or did it hold its own surface? List one feature that would be easy if it rendered into your container and hard if it didn't. That feature is usually the tell for which embedding style you should have chosen.
Hint
The question is always 'who owns the glass?' If the embedded thing owns its own window/surface, your app's input, overlays, and layout have to fight it. If it draws into a surface you provide, all of that comes for free from your normal UI framework.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.