From 2e4eafc7c3b5f508083ca0855cba93c34f22bb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E5=86=89?= Date: Tue, 4 Aug 2026 00:11:27 +0800 Subject: [PATCH 1/3] feat(screen): add streaming frame pipeline foundation --- .../Scrcpy/Video/ScrcpyStreamingFrame.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Screen-Remote/Services/Scrcpy/Video/ScrcpyStreamingFrame.swift diff --git a/Screen-Remote/Services/Scrcpy/Video/ScrcpyStreamingFrame.swift b/Screen-Remote/Services/Scrcpy/Video/ScrcpyStreamingFrame.swift new file mode 100644 index 0000000..74afb14 --- /dev/null +++ b/Screen-Remote/Services/Scrcpy/Video/ScrcpyStreamingFrame.swift @@ -0,0 +1,23 @@ +import CoreVideo +import Foundation + +/// A transport object between the native scrcpy decoder and the macOS rendering layer. +/// +/// The previous rendering path converted every decoded frame directly into CGImage. +/// Keeping the decoded CVPixelBuffer alive allows future Metal-backed surfaces to consume +/// frames without forcing an intermediate image conversion. +final class ScrcpyStreamingFrame: @unchecked Sendable { + let pixelBuffer: CVPixelBuffer + let timestamp: UInt64 + let generation: UInt64 + + init( + pixelBuffer: CVPixelBuffer, + timestamp: UInt64, + generation: UInt64 + ) { + self.pixelBuffer = pixelBuffer + self.timestamp = timestamp + self.generation = generation + } +} From d333704234d1435c0ebaf4e6caccbbedacbed1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E5=86=89?= Date: Tue, 4 Aug 2026 00:13:40 +0800 Subject: [PATCH 2/3] refactor(screen): expose decoded frames for native rendering --- .../Scrcpy/Video/ScrcpyVideoSurface.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Screen-Remote/Services/Scrcpy/Video/ScrcpyVideoSurface.swift b/Screen-Remote/Services/Scrcpy/Video/ScrcpyVideoSurface.swift index 6e858e7..adb8adf 100644 --- a/Screen-Remote/Services/Scrcpy/Video/ScrcpyVideoSurface.swift +++ b/Screen-Remote/Services/Scrcpy/Video/ScrcpyVideoSurface.swift @@ -6,12 +6,28 @@ import VideoToolbox import Darwin final class ScrcpyVideoSurface: ObservableObject { + /// Compatibility image output for existing SwiftUI views. + /// New renderers should consume `frame` and keep the original pixel buffer. @Published private(set) var image: CGImage? + @Published private(set) var frame: ScrcpyStreamingFrame? @Published private(set) var pixelSize = CGSize.zero private let context = CIContext(options: [.cacheIntermediates: false]) + private var generation: UInt64 = 0 func publish(_ pixelBuffer: CVPixelBuffer) { + generation &+= 1 + let streamingFrame = ScrcpyStreamingFrame( + pixelBuffer: pixelBuffer, + generation: generation + ) + + DispatchQueue.main.async { [weak self] in + self?.frame = streamingFrame + } + + // Keep the existing CGImage path temporarily so current SwiftUI + // surfaces continue to work while Metal-backed rendering is introduced. let ciImage = CIImage(cvPixelBuffer: pixelBuffer) guard let image = context.createCGImage(ciImage, from: ciImage.extent) else { return } DispatchQueue.main.async { [weak self] in @@ -23,6 +39,7 @@ final class ScrcpyVideoSurface: ObservableObject { func clear() { DispatchQueue.main.async { [weak self] in self?.image = nil + self?.frame = nil self?.pixelSize = .zero } } From 952ac605e7662401c6aea37ca5378b112cb5028c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E5=86=89?= Date: Tue, 4 Aug 2026 00:14:08 +0800 Subject: [PATCH 3/3] feat(screen): add frame generation gate for session rendering --- .../Video/ScrcpyFrameGenerationGate.swift | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Screen-Remote/Services/Scrcpy/Video/ScrcpyFrameGenerationGate.swift diff --git a/Screen-Remote/Services/Scrcpy/Video/ScrcpyFrameGenerationGate.swift b/Screen-Remote/Services/Scrcpy/Video/ScrcpyFrameGenerationGate.swift new file mode 100644 index 0000000..11c4b9f --- /dev/null +++ b/Screen-Remote/Services/Scrcpy/Video/ScrcpyFrameGenerationGate.swift @@ -0,0 +1,33 @@ +import Foundation + +/// Prevents frames from a previous scrcpy session from being rendered after +/// reconnects or parameter-driven session replacement. +final class ScrcpyFrameGenerationGate { + private let lock = NSLock() + private var generation: UInt64 = 0 + + func beginSession() -> UInt64 { + lock.lock() + defer { lock.unlock() } + generation &+= 1 + return generation + } + + func invalidate() { + lock.lock() + generation &+= 1 + lock.unlock() + } + + func accepts(_ frameGeneration: UInt64) -> Bool { + lock.lock() + defer { lock.unlock() } + return frameGeneration == generation + } + + var currentGeneration: UInt64 { + lock.lock() + defer { lock.unlock() } + return generation + } +}