diff --git a/configure.ac b/configure.ac index ec506723ad1..4a4519295db 100644 --- a/configure.ac +++ b/configure.ac @@ -2934,7 +2934,9 @@ if test "${with_mtl}" = yes; then AC_LANG_POP([Objective C]) if test "${HAVE_MTL}" = yes; then MTL_OBJC_OBJ="mtlterm.o mtlfns.o" - MTL_LIBS="-framework Metal -framework QuartzCore -framework CoreText" + dnl AVFoundation/CoreMedia/CoreVideo: inline video (AVPlayerItemVideoOutput + dnl decoded straight into Metal textures via CVMetalTextureCache). + MTL_LIBS="-framework Metal -framework QuartzCore -framework CoreText -framework AVFoundation -framework CoreMedia -framework CoreVideo" dnl Metal piggybacks on NS infrastructure; keep window_system=nextstep dnl so that term_header=nsterm.h and all NS macros remain available. fi @@ -7492,7 +7494,10 @@ case "$opsys" in fi fi if test "$HAVE_MTL" = "yes"; then - libs_nsgui="$libs_nsgui -framework Metal -framework CoreText" + dnl AVFoundation/CoreMedia/CoreVideo: inline video (Fase H2). + libs_nsgui="$libs_nsgui -framework Metal -framework CoreText \ + -framework AVFoundation -framework CoreMedia \ + -framework CoreVideo" fi else libs_nsgui= diff --git a/lisp/mtl.el b/lisp/mtl.el index b43c66ba56d..1c34f3b35bb 100644 --- a/lisp/mtl.el +++ b/lisp/mtl.el @@ -64,7 +64,7 @@ ;; Helper functions (must be defined before defcustom :set functions use them) (defun mtl--cursor-mode-number (mode) - "Convert cursor MODE symbol to integer for mtl-cursor-mode." + "Convert cursor MODE symbol to integer for `mtl-cursor-mode'." (pcase mode ('block 0) ('spring 1) @@ -77,7 +77,7 @@ (_ 1))) (defun mtl--scroll-easing-number (easing) - "Convert EASING symbol to integer for mtl-scroll-effect." + "Convert EASING symbol to integer for `mtl-scroll-effect'." (pcase easing ('none 0) ('linear 1) @@ -171,7 +171,7 @@ The NS backend still handles events, menus, and scrollbars." (error "mtl-enable: Metal is not available on this system")) (let ((f (or frame (selected-frame)))) (unless (framep f) - (error "mtl-enable: argument is not a frame")) + (error "Mtl-enable: argument is not a frame")) (mtl-enable-for-frame f) ;; Apply current configuration (mtl-cursor-mode (mtl--cursor-mode-number mtl-cursor-animation)) @@ -223,6 +223,62 @@ The NS backend still handles events, menus, and scrollbars." nil t)))) (setopt mtl-scroll-easing easing)) +;; --------------------------------------------------------------------------- +;; Inline video (Fase H2) + +(defvar mtl--video-state nil + "Active inline video: (MARKER WIDTH HEIGHT TIMER FRAME), or nil.") + +(defun mtl--video-sync () + "Track the video placeholder: move/clip the GPU rect and present a frame. +Runs on a 30fps timer started by `mtl-video-insert'. Follows scrolling +and window changes; hides the video while its position is off-screen." + (when mtl--video-state + (pcase-let ((`(,marker ,w ,h ,_timer ,frame) mtl--video-state)) + (if (not (and (frame-live-p frame) (marker-buffer marker))) + (mtl-video-stop) + (let* ((win (get-buffer-window (marker-buffer marker) frame)) + (vis (and win (pos-visible-in-window-p marker win t)))) + (if (not (and vis (listp vis))) + ;; Not visible: park the rect off-screen but keep decoding. + (mtl-video-move 0 -32768 w h nil frame) + (let* ((edges (window-inside-pixel-edges win)) + (x (+ (nth 0 edges) (nth 0 vis))) + (y (+ (nth 1 edges) (nth 1 vis)))) + (mtl-video-move x y w h edges frame))) + (mtl-video-tick frame)))))) + +;;;###autoload +(defun mtl-video-insert (file width height &optional loop) + "Insert a WIDTH x HEIGHT placeholder at point and play video FILE over it. +The placeholder is a space with a pixel-sized display spec; the GPU +composites the video at its position every frame, following scrolling +\(clipped to the window interior). With LOOP non-nil, restart playback +at the end. One video per frame; a previous one is replaced." + (interactive "fVideo file: \nnWidth (px): \nnHeight (px): ") + (mtl-video-stop) + (insert (propertize " " + 'display `(space :width (,width) :height (,height)) + 'mtl-video file)) + (let ((marker (copy-marker (1- (point))))) + ;; Park off-screen; the first sync tick positions it for real. + (unless (mtl-video-open file 0 -32768 width height loop) + (error "mtl-video-open failed for %s" file)) + (setq mtl--video-state + (list marker width height + (run-at-time 0 0.033 #'mtl--video-sync) + (selected-frame))))) + +;;;###autoload +(defun mtl-video-stop () + "Stop and remove the inline video, canceling its sync timer." + (interactive) + (when mtl--video-state + (pcase-let ((`(,_marker ,_w ,_h ,timer ,frame) mtl--video-state)) + (when (timerp timer) (cancel-timer timer)) + (when (frame-live-p frame) (mtl-video-close frame))) + (setq mtl--video-state nil))) + ;; --------------------------------------------------------------------------- ;; Startup integration diff --git a/src/mtlfns.m b/src/mtlfns.m index 78f7bc4381a..efa2149e1c0 100644 --- a/src/mtlfns.m +++ b/src/mtlfns.m @@ -22,6 +22,7 @@ #include "fontset.h" #include "font.h" #include "character.h" +#include "coding.h" #include "macfont.h" #include "mtlterm.h" @@ -453,6 +454,117 @@ DEFUN ("mtl-animation-status", Fmtl_animation_status, Smtl_animation_status, Initialization ----------------------------------------------------------------------- */ +/* --------------------------------------------------------------------------- + Fase H2: inline video + --------------------------------------------------------------------------- */ + +DEFUN ("mtl-video-open", Fmtl_video_open, Smtl_video_open, 5, 7, 0, + doc: /* Play video FILE over FRAME at X, Y sized WIDTH x HEIGHT pixels. +X and Y are frame-relative logical pixels (top-left origin). The video +is decoded by AVFoundation straight into Metal textures and composited +over the frame content every present; redisplay underneath continues +normally. If LOOP is non-nil, restart playback at the end. +FRAME defaults to the selected frame. One video per frame: opening a +new one replaces the previous. Returns t on success. */) + (Lisp_Object file, Lisp_Object x, Lisp_Object y, Lisp_Object width, + Lisp_Object height, Lisp_Object loop, Lisp_Object frame) +{ + if (NILP (frame)) frame = Fselected_frame (); + CHECK_LIVE_FRAME (frame); + struct frame *f = XFRAME (frame); + CHECK_STRING (file); + CHECK_FIXNUM (x); CHECK_FIXNUM (y); + CHECK_FIXNUM (width); CHECK_FIXNUM (height); + + Lisp_Object expanded = Fexpand_file_name (file, Qnil); + bool ok; + block_input (); + ok = mtl_video_open (f, SSDATA (ENCODE_FILE (expanded)), + XFIXNUM (x), XFIXNUM (y), + XFIXNUM (width), XFIXNUM (height), + !NILP (loop)); + unblock_input (); + return ok ? Qt : Qnil; +} + +DEFUN ("mtl-video-close", Fmtl_video_close, Smtl_video_close, 0, 1, 0, + doc: /* Stop and remove the inline video on FRAME. +FRAME defaults to the selected frame. Returns t if a video was open. */) + (Lisp_Object frame) +{ + if (NILP (frame)) frame = Fselected_frame (); + CHECK_LIVE_FRAME (frame); + bool ok; + block_input (); + ok = mtl_video_close (XFRAME (frame)); + unblock_input (); + return ok ? Qt : Qnil; +} + +DEFUN ("mtl-video-pause", Fmtl_video_pause, Smtl_video_pause, 1, 2, 0, + doc: /* Pause (PAUSED non-nil) or resume the inline video on FRAME. +FRAME defaults to the selected frame. Returns t if a video is open. */) + (Lisp_Object paused, Lisp_Object frame) +{ + if (NILP (frame)) frame = Fselected_frame (); + CHECK_LIVE_FRAME (frame); + bool ok; + block_input (); + ok = mtl_video_set_paused (XFRAME (frame), !NILP (paused)); + unblock_input (); + return ok ? Qt : Qnil; +} + +DEFUN ("mtl-video-move", Fmtl_video_move, Smtl_video_move, 4, 6, 0, + doc: /* Move/resize the inline video on FRAME to X, Y, WIDTH, HEIGHT. +Frame-relative logical pixels. Optional CLIP is a list (LEFT TOP RIGHT +BOTTOM), also frame-relative, that confines the video to a window's +interior; nil removes clipping. FRAME defaults to the selected frame. +Returns t if a video is open. */) + (Lisp_Object x, Lisp_Object y, Lisp_Object width, Lisp_Object height, + Lisp_Object clip, Lisp_Object frame) +{ + if (NILP (frame)) frame = Fselected_frame (); + CHECK_LIVE_FRAME (frame); + CHECK_FIXNUM (x); CHECK_FIXNUM (y); + CHECK_FIXNUM (width); CHECK_FIXNUM (height); + bool ok; + block_input (); + ok = mtl_video_set_rect (XFRAME (frame), XFIXNUM (x), XFIXNUM (y), + XFIXNUM (width), XFIXNUM (height)); + if (ok) + { + if (CONSP (clip)) + { + int cl = XFIXNUM (Fnth (make_fixnum (0), clip)); + int ct = XFIXNUM (Fnth (make_fixnum (1), clip)); + int cr = XFIXNUM (Fnth (make_fixnum (2), clip)); + int cb = XFIXNUM (Fnth (make_fixnum (3), clip)); + mtl_video_set_clip (XFRAME (frame), cl, ct, cr - cl, cb - ct); + } + else + mtl_video_set_clip (XFRAME (frame), 0, 0, 0, 0); + } + unblock_input (); + return ok ? Qt : Qnil; +} + +DEFUN ("mtl-video-tick", Fmtl_video_tick, Smtl_video_tick, 0, 1, 0, + doc: /* Present a fresh frame of the inline video on FRAME. +Driven by a Lisp timer in mtl.el (Emacs's event loop starves the +CADisplayLink while idle). Returns t while a video is open, nil +otherwise (letting the timer cancel itself). */) + (Lisp_Object frame) +{ + if (NILP (frame)) frame = Fselected_frame (); + if (!FRAME_LIVE_P (XFRAME (frame))) return Qnil; + bool ok; + block_input (); + ok = mtl_video_tick (XFRAME (frame)); + unblock_input (); + return ok ? Qt : Qnil; +} + void syms_of_mtlfns (void) { @@ -472,6 +584,12 @@ syms_of_mtlfns (void) defsubr (&Smtl_animations); defsubr (&Smtl_capture_frame); defsubr (&Smtl_draw_stats); + /* Fase H2: inline video */ + defsubr (&Smtl_video_open); + defsubr (&Smtl_video_close); + defsubr (&Smtl_video_pause); + defsubr (&Smtl_video_move); + defsubr (&Smtl_video_tick); } #endif /* HAVE_MTL */ diff --git a/src/mtlterm.h b/src/mtlterm.h index 41432623a1d..46c4c4cbeb4 100644 --- a/src/mtlterm.h +++ b/src/mtlterm.h @@ -10,6 +10,8 @@ #import #import #import +#import +#import #include "dispextern.h" #include "frame.h" @@ -140,6 +142,38 @@ typedef struct mtl_spring { @end +/* ----------------------------------------------------------------------- + MtlVideoPlayer — inline video playback (Fase H2). + AVPlayer decodes; AVPlayerItemVideoOutput hands BGRA pixel buffers that + CVMetalTextureCache wraps as Metal textures with zero copies; the + compositor draws the current frame as a textured quad over the static + texture each present, and the animator's CADisplayLink keeps presents + flowing while playback is active. + ----------------------------------------------------------------------- */ + +@interface MtlVideoPlayer : NSObject +@property (nonatomic, strong) AVPlayer *player; +@property (nonatomic, strong) AVPlayerItemVideoOutput *output; +@property (nonatomic, assign) CVMetalTextureCacheRef textureCache; +/* Keep the CoreVideo wrapper alive while its MTLTexture is in use. */ +@property (nonatomic, assign) CVMetalTextureRef currentCVTexture; +@property (nonatomic, strong) id currentTexture; +@property (nonatomic, assign) NSRect rect; /* logical px */ +/* Window-interior clip (logical px); NSZeroRect = no clipping. Keeps a + half-scrolled video from bleeding over the mode line. */ +@property (nonatomic, assign) NSRect clipRect; +@property (nonatomic, assign) BOOL loop; +/* NSNotificationCenter block token for loop mode (retained; this file is + compiled without ARC, so raw ivar assignments would not retain). */ +@property (nonatomic, strong) id endObserver; + +- (instancetype)initWithURL:(NSURL *)url rect:(NSRect)rect loop:(BOOL)loop; +/* Latest decoded frame as a Metal texture (nil before the first frame). */ +- (id)textureForNow; +- (BOOL)isPlaying; +- (void)shutdown; +@end + /* ----------------------------------------------------------------------- MtlFrameData — per-frame Metal rendering state. Stored as ObjC associated object on EmacsView. @@ -187,6 +221,10 @@ typedef struct mtl_spring { @property (nonatomic, assign) BOOL cycleSawClear; @property (nonatomic, assign) BOOL cycleSawDraw; +/* Fase H2: active inline video (one per frame for now), drawn by + compositeToScreen over the static texture. */ +@property (nonatomic, strong) MtlVideoPlayer *videoPlayer; + /* Main Emacs render cycle (renders to staticTexture) */ - (void)beginFrame; - (void)endFrame; @@ -279,6 +317,15 @@ MtlGlyphCacheEntry *mtl_cache_glyph (CTFontRef font, uint32_t codepoint); /* Pre-rasterize printable ASCII for FRAME's default face (atlas warm-up). */ extern void mtl_warm_glyph_cache (struct frame *f); +/* Fase H2: inline video (one player per frame). */ +extern bool mtl_video_open (struct frame *f, const char *path, + int x, int y, int w, int h, bool loop); +extern bool mtl_video_close (struct frame *f); +extern bool mtl_video_set_paused (struct frame *f, bool paused); +extern bool mtl_video_set_rect (struct frame *f, int x, int y, int w, int h); +extern bool mtl_video_set_clip (struct frame *f, int x, int y, int w, int h); +extern bool mtl_video_tick (struct frame *f); + extern bool mtl_render_offscreen_png (const char *path, int w, int h, void (^draw)(id)); extern bool mtl_render_text_png (const char *path); diff --git a/src/mtlterm.m b/src/mtlterm.m index 4f2fc2b1557..d606047c498 100644 --- a/src/mtlterm.m +++ b/src/mtlterm.m @@ -850,6 +850,20 @@ easing_apply (MtlScrollEasing mode, float t) } } +/* Sequence tracing for present-flow debugging (MTL_LOG_SEQ=1). */ +static BOOL +mtl_log_seq_p (void) +{ + static int on = -1; + if (on < 0) on = getenv ("MTL_LOG_SEQ") != NULL; + return on > 0; +} + +#define MTL_SEQ(fmt, ...) \ + do { if (mtl_log_seq_p ()) \ + fprintf (stderr, "[mtlseq %.3f] " fmt "\n", \ + CACurrentMediaTime (), ##__VA_ARGS__); } while (0) + /* ----------------------------------------------------------------------- @implementation MtlAnimator ----------------------------------------------------------------------- */ @@ -1025,6 +1039,11 @@ easing_apply (MtlScrollEasing mode, float t) needsComposite = YES; } + /* Fase H2: while a video plays, every tick presents so the compositor + samples the freshest decoded frame. */ + if (fd.videoPlayer && [fd.videoPlayer isPlaying]) + needsComposite = YES; + if (needsComposite || self.cursorDirty) { self.cursorDirty = NO; @@ -1034,6 +1053,124 @@ easing_apply (MtlScrollEasing mode, float t) @end +/* ----------------------------------------------------------------------- + @implementation MtlVideoPlayer (Fase H2: inline video) + ----------------------------------------------------------------------- */ + +@implementation MtlVideoPlayer + +/* NOTE: this file is compiled without ARC; always go through the property + setters (retain semantics), never raw ivar assignment, or the AVPlayer + graph gets autoreleased under us. */ +- (instancetype)initWithURL:(NSURL *)url rect:(NSRect)rect loop:(BOOL)loop +{ + self = [super init]; + if (!self) return nil; + + AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url]; + NSDictionary *attrs = @{ + (id) kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), + (id) kCVPixelBufferMetalCompatibilityKey : @YES, + }; + self.output = [[[AVPlayerItemVideoOutput alloc] + initWithPixelBufferAttributes:attrs] autorelease]; + [item addOutput:self.output]; + + self.player = [AVPlayer playerWithPlayerItem:item]; + self.player.actionAtItemEnd = loop ? AVPlayerActionAtItemEndNone + : AVPlayerActionAtItemEndPause; + if (loop) + self.endObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:AVPlayerItemDidPlayToEndTimeNotification + object:item + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *note) { + (void) note; + [item seekToTime:kCMTimeZero completionHandler:nil]; + }]; + + CVMetalTextureCacheRef cache = NULL; + CVMetalTextureCacheCreate (NULL, NULL, g_device, NULL, &cache); + self.textureCache = cache; + self.rect = rect; + self.clipRect = NSZeroRect; + self.loop = loop; + [self.player play]; + return self; +} + +- (BOOL)isPlaying +{ + return self.player != nil && self.player.rate != 0.0f; +} + +/* Wrap the newest decoded pixel buffer as a Metal texture (zero copy via + CVMetalTextureCache). Falls back to the previous frame's texture when the + output has nothing new, so redraws between video frames keep the picture. */ +- (id)textureForNow +{ + if (!self.output || !self.textureCache) return self.currentTexture; + + CMTime t = [self.output itemTimeForHostTime:CACurrentMediaTime ()]; + if ([self.output hasNewPixelBufferForItemTime:t]) + { + CVPixelBufferRef pb = [self.output copyPixelBufferForItemTime:t + itemTimeForDisplay:NULL]; + if (pb) + { + size_t w = CVPixelBufferGetWidth (pb); + size_t h = CVPixelBufferGetHeight (pb); + CVMetalTextureRef cvtex = NULL; + if (CVMetalTextureCacheCreateTextureFromImage ( + NULL, self.textureCache, pb, NULL, + MTLPixelFormatBGRA8Unorm, w, h, 0, &cvtex) + == kCVReturnSuccess && cvtex) + { + /* The MTLTexture is only valid while its CV wrapper lives; + release the previous wrapper now that it is replaced. */ + if (self.currentCVTexture) + CFRelease (self.currentCVTexture); + self.currentCVTexture = cvtex; + self.currentTexture = CVMetalTextureGetTexture (cvtex); + } + CVPixelBufferRelease (pb); + } + } + return self.currentTexture; +} + +- (void)shutdown +{ + [self.player pause]; + if (self.endObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:self.endObserver]; + self.endObserver = nil; + } + self.player = nil; + self.output = nil; + self.currentTexture = nil; + if (self.currentCVTexture) + { + CFRelease (self.currentCVTexture); + self.currentCVTexture = NULL; + } + if (self.textureCache) + { + CVMetalTextureCacheFlush (self.textureCache, 0); + CFRelease (self.textureCache); + self.textureCache = NULL; + } +} + +- (void)dealloc +{ + [self shutdown]; + [super dealloc]; +} + +@end + /* ----------------------------------------------------------------------- @implementation MtlFrameData ----------------------------------------------------------------------- */ @@ -1400,20 +1537,6 @@ easing_apply (MtlScrollEasing mode, float t) self.needsPresent = YES; } -/* Sequence tracing for present-flow debugging (MTL_LOG_SEQ=1). */ -static BOOL -mtl_log_seq_p (void) -{ - static int on = -1; - if (on < 0) on = getenv ("MTL_LOG_SEQ") != NULL; - return on > 0; -} - -#define MTL_SEQ(fmt, ...) \ - do { if (mtl_log_seq_p ()) \ - fprintf (stderr, "[mtlseq %.3f] " fmt "\n", \ - CACurrentMediaTime (), ##__VA_ARGS__); } while (0) - - (void)compositeToScreen { if (!self.staticTexture || !g_blit_pipeline) return; @@ -1421,7 +1544,12 @@ mtl_log_seq_p (void) id drawable = [self.metalLayer nextDrawable]; if (!drawable) return; - MTL_SEQ ("PRESENT"); + MTL_SEQ ("PRESENT layer=%.0fx%.0f drawable=%lux%lu static=%lux%lu", + self.metalLayer.frame.size.width, self.metalLayer.frame.size.height, + (unsigned long) drawable.texture.width, + (unsigned long) drawable.texture.height, + (unsigned long) self.staticTexture.width, + (unsigned long) self.staticTexture.height); self.needsPresent = NO; /* about to present whatever is in the static texture */ @@ -1458,6 +1586,65 @@ mtl_log_seq_p (void) [enc drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6]; } + /* Fase H2: inline video overlay. Drawn over the static texture so the + redisplay engine can keep treating the placeholder area as ordinary + buffer background. */ + MtlVideoPlayer *vp = self.videoPlayer; + if (vp) + { + id vtex = [vp textureForNow]; + if (vtex && g_image_pipeline) + { + /* Clip to the window interior so a half-scrolled video does not + bleed over the mode line or a neighboring window. */ + NSRect cr = vp.clipRect; + BOOL clipped = !NSIsEmptyRect (cr); + if (clipped) + { + CGSize dsz = self.metalLayer.drawableSize; + double scx = sz.width > 0 ? dsz.width / sz.width : 1.0; + double scy = sz.height > 0 ? dsz.height / sz.height : 1.0; + long tw = (long) drawable.texture.width; + long th = (long) drawable.texture.height; + long cx0 = lround (NSMinX (cr) * scx); + long cy0 = lround (NSMinY (cr) * scy); + long cx1 = lround (NSMaxX (cr) * scx); + long cy1 = lround (NSMaxY (cr) * scy); + cx0 = MAX (0, MIN (cx0, tw)); cy0 = MAX (0, MIN (cy0, th)); + cx1 = MAX (cx0, MIN (cx1, tw)); cy1 = MAX (cy0, MIN (cy1, th)); + MTLScissorRect sc = { (NSUInteger) cx0, (NSUInteger) cy0, + (NSUInteger) (cx1 - cx0), + (NSUInteger) (cy1 - cy0) }; + if (sc.width == 0 || sc.height == 0) + sc = (MTLScissorRect) {0, 0, 1, 1}; + [enc setScissorRect:sc]; + } + + typedef struct { float x, y, u, v, a; } ImgVert; + NSRect vr = vp.rect; + float x0 = NSMinX (vr), y0 = NSMinY (vr); + float x1 = NSMaxX (vr), y1 = NSMaxY (vr); + ImgVert verts[6] = { + {x0,y0, 0,0,1}, {x1,y0, 1,0,1}, {x0,y1, 0,1,1}, + {x1,y0, 1,0,1}, {x1,y1, 1,1,1}, {x0,y1, 0,1,1}, + }; + [enc setRenderPipelineState:g_image_pipeline]; + [enc setVertexBytes:verts length:sizeof (verts) atIndex:0]; + [enc setVertexBuffer:self.uniformBuffer offset:0 atIndex:1]; + [enc setFragmentTexture:vtex atIndex:0]; + [enc setFragmentSamplerState:g_sampler atIndex:0]; + [enc drawPrimitives:MTLPrimitiveTypeTriangle + vertexStart:0 vertexCount:6]; + + if (clipped) + { + MTLScissorRect full = { 0, 0, drawable.texture.width, + drawable.texture.height }; + [enc setScissorRect:full]; + } + } + } + /* Animation overlay (cursor effects, trail, particles) is opt-in. When off, the cursor lives in the static texture (drawn by mtl_draw_window_cursor), so the compositor only blits and presents. This is what kills the stray @@ -2857,6 +3044,95 @@ mtl_flush_display (struct frame *f) [fd compositeToScreen]; } +/* --------------------------------------------------------------------------- + Fase H2: inline video API (called from mtlfns.m). + --------------------------------------------------------------------------- */ + +bool +mtl_video_open (struct frame *f, const char *path, int x, int y, + int w, int h, bool loop) +{ + MtlFrameData *fd = mtl_get_frame_data (f); + if (!fd) return false; + + if (fd.videoPlayer) + { + [fd.videoPlayer shutdown]; + fd.videoPlayer = nil; + } + + NSString *ns_path = [NSString stringWithUTF8String:path]; + if (!ns_path || ![[NSFileManager defaultManager] fileExistsAtPath:ns_path]) + return false; + + MtlVideoPlayer *vp = + [[MtlVideoPlayer alloc] initWithURL:[NSURL fileURLWithPath:ns_path] + rect:NSMakeRect (x, y, w, h) + loop:loop]; + if (!vp) return false; + fd.videoPlayer = vp; + + /* The animator's CADisplayLink drives presents during playback. */ + [fd.animator startAnimating]; + return true; +} + +bool +mtl_video_close (struct frame *f) +{ + MtlFrameData *fd = mtl_get_frame_data (f); + if (!fd || !fd.videoPlayer) return false; + [fd.videoPlayer shutdown]; + fd.videoPlayer = nil; + if (!g_mtl_animations_enabled) + [fd.animator stopAnimating]; + [fd compositeToScreen]; /* repaint without the overlay */ + return true; +} + +bool +mtl_video_set_paused (struct frame *f, bool paused) +{ + MtlFrameData *fd = mtl_get_frame_data (f); + if (!fd || !fd.videoPlayer) return false; + if (paused) + [fd.videoPlayer.player pause]; + else + [fd.videoPlayer.player play]; + return true; +} + +bool +mtl_video_set_rect (struct frame *f, int x, int y, int w, int h) +{ + MtlFrameData *fd = mtl_get_frame_data (f); + if (!fd || !fd.videoPlayer) return false; + fd.videoPlayer.rect = NSMakeRect (x, y, w, h); + return true; +} + +bool +mtl_video_set_clip (struct frame *f, int x, int y, int w, int h) +{ + MtlFrameData *fd = mtl_get_frame_data (f); + if (!fd || !fd.videoPlayer) return false; + fd.videoPlayer.clipRect = NSMakeRect (x, y, w, h); + return true; +} + +/* Present a fresh composite if a video is active. Called from a Lisp-level + timer (mtl.el): Emacs's event loop starves the CADisplayLink while idle, + so Lisp timers are what reliably drives playback presents. */ +bool +mtl_video_tick (struct frame *f) +{ + MtlFrameData *fd = mtl_get_frame_data (f); + if (!fd || !fd.videoPlayer) return false; + if ([fd.videoPlayer isPlaying] && !fd.encoder) + [fd compositeToScreen]; + return true; +} + static void mtl_update_begin (struct frame *f) {