From 1fe7caa6bc6047db2cf3d54dec6174883bbc9104 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Wed, 21 Oct 2026 22:01:37 +0200 Subject: [PATCH] Keep animated video resizes smooth on iOS Every layout pass with a new size recreated the framebuffer and rendered synchronously on the render queue, blocking the main thread behind the in-flight render. Animated layout changes (corner layout panel toggles, rotation) dropped to a few frames per second. Size changes arriving in quick succession now let the layer stretch the last frame and recreate the framebuffer once the size settles; one-off resizes still recreate immediately. --- .../Services/Player/MPV/MPVRenderView.swift | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Yattee/Services/Player/MPV/MPVRenderView.swift b/Yattee/Services/Player/MPV/MPVRenderView.swift index 8550bcd5..007bbda4 100644 --- a/Yattee/Services/Player/MPV/MPVRenderView.swift +++ b/Yattee/Services/Player/MPV/MPVRenderView.swift @@ -107,6 +107,14 @@ final class MPVRenderView: UIView { /// Pending resize work item (for debouncing) private var pendingResizeWorkItem: DispatchWorkItem? + /// When the view size last changed away from the framebuffer size. + private var lastSizeChangeTime: CFTimeInterval = 0 + + /// Size changes closer together than this are one continuous resize + /// (animated layout change, rotation) and recreate the framebuffer only + /// once it settles. + private static let continuousResizeWindow: CFTimeInterval = 0.15 + /// Dedicated queue for OpenGL rendering (off main thread). private let renderQueue = DispatchQueue(label: "stream.yattee.mpv.render", qos: .userInteractive) @@ -501,8 +509,28 @@ final class MPVRenderView: UIView { // Cancel any pending resize pendingResizeWorkItem?.cancel() - // Always recreate framebuffer immediately to ensure video is properly sized - // This is important during rotation animations + // A continuous resize (corner layout panel toggles, rotation) changes + // the size every animation frame. Recreating + rendering per step + // blocks main on renderQueue.sync (waiting out the in-flight render, + // then rendering again) and drops the animation to a few fps. Let the + // layer stretch the last frame meanwhile — the view keeps the video's + // aspect ratio — and recreate once the size settles. A one-off resize + // (e.g. handing the view to the mini player) still recreates now. + let now = CACurrentMediaTime() + let isContinuousResize = now - lastSizeChangeTime < Self.continuousResizeWindow + lastSizeChangeTime = now + if isContinuousResize { + MPVLogging.log("layoutSubviews: deferring framebuffer recreation (continuous resize)") + let workItem = DispatchWorkItem { [weak self] in + // Settled: the next layout pass recreates immediately + self?.lastSizeChangeTime = 0 + self?.setNeedsLayout() + } + pendingResizeWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + Self.continuousResizeWindow, execute: workItem) + return + } + renderQueue.sync { destroyFramebuffer() createFramebuffer()