From a56185206706589326318a80573b13d3f8e9988e Mon Sep 17 00:00:00 2001 From: sravan Date: Fri, 7 Aug 2026 10:25:46 +0530 Subject: [PATCH] Fix build: TileVideoView composes VideoView (can't subclass a non-open class) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build error: `cannot inherit from non-open class 'VideoView' outside of its defining module` — VideoView is public, not open, so it can't be subclassed in the plugin module. Reworked TileVideoView from a VideoView subclass into a UIView CONTAINER that holds a VideoView: the container is frame-synced to the web tile rect, the pinch/pan zoom transform lives on the inner video (so it never fights the position poll), and the name/mute overlays sit on the container so they no longer scale with zoom. track/layoutMode are forwarded. Also silenced the extension warning: SampleHandler now restates `@unchecked Sendable`. The broadcast extension itself compiled + linked in the failed build, so the SPM-linked extension injection + App Group setup are working. Co-Authored-By: Claude Opus 4.8 --- mobile/ios-broadcast/SampleHandler.swift | 2 +- .../NativeCallPlugin/NativeCallPlugin.swift | 56 +++++++++++++------ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/mobile/ios-broadcast/SampleHandler.swift b/mobile/ios-broadcast/SampleHandler.swift index 40ac82e..194ca82 100644 --- a/mobile/ios-broadcast/SampleHandler.swift +++ b/mobile/ios-broadcast/SampleHandler.swift @@ -13,4 +13,4 @@ import LiveKit // convention, keyed off this extension's bundle id (com.bizgaze.connect.broadcast) and the default app group // group.. Both are also set explicitly on the app side (RTCScreenSharingExtension / // RTCAppGroupIdentifier in Info.plist) so there's no ambiguity. -class SampleHandler: LKSampleHandler {} +final class SampleHandler: LKSampleHandler, @unchecked Sendable {} diff --git a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift index 906ff04..f9dd0a6 100644 --- a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift +++ b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift @@ -171,15 +171,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega private static let nameTag = 9001 private static let muteTag = 9002 - // Build a tile VideoView with its native name label (bottom-left) + mute badge (bottom-right) — the video - // covers the web tile, so these redraw the essentials the web can no longer show through. + // Build a tile view (a container holding a LiveKit VideoView) with its native name label (bottom-left) + + // mute badge (top-left) — the video covers the web tile, so these redraw the essentials. private func makeTileView(host: UIView, key: String) -> TileVideoView { - let v = TileVideoView() - v.layoutMode = .fill - v.backgroundColor = .black - v.clipsToBounds = true - v.layer.cornerRadius = 8 - v.installZoomGestures() + let v = TileVideoView(frame: .zero) // sets up its inner VideoView + zoom gestures + cosmetics in init host.addSubview(v) // Name chip (bottom-left): white text on a dark translucent pill so it's legible over ANY video @@ -388,7 +383,9 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega vv.isScreen = wantScreen // enables pinch-zoom on shared screens (resets zoom if turned off) vv.layoutMode = wantScreen ? .fit : .fill if vv.track !== track { vv.track = track } - if vv.transform.isIdentity { vv.frame = CGRect(x: x, y: y, width: w, height: h) } // don't fight an active zoom + // The container always tracks the tile rect; the zoom transform lives on the INNER video, so + // this never fights an active zoom. + vv.frame = CGRect(x: x, y: y, width: w, height: h) // Native video covers the web tile, so redraw the essentials (name + muted) natively. self.updateTileOverlay(vv, name: (t["name"] as? String) ?? "", muted: (t["muted"] as? Bool) ?? false) } @@ -549,16 +546,41 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega } } -// A tile video view that supports pinch-to-zoom + pan while it's showing a shared SCREEN (so fine print is -// legible); double-tap resets. Camera tiles keep isScreen=false and never zoom. While zoomed the view carries -// a transform, and syncVideoTiles stops overwriting its frame so the zoom isn't fought each poll. -final class TileVideoView: VideoView, UIGestureRecognizerDelegate { +// A tile view = a container holding a LiveKit VideoView. We CANNOT subclass VideoView (it's `public`, not +// `open`, so subclassing outside its module is illegal), so we compose instead. Pinch-to-zoom + pan apply to +// the INNER video while it's showing a shared SCREEN (so fine print is legible); double-tap resets. The +// container itself stays frame-synced to the web tile rect, so zooming never fights the position poll, and the +// name/mute overlays (added to the container by makeTileView) don't scale with the zoom. +final class TileVideoView: UIView, UIGestureRecognizerDelegate { + let video = VideoView() var isScreen = false { didSet { if !isScreen { resetZoom() } } } - var isZoomed: Bool { !transform.isIdentity } + var isZoomed: Bool { !video.transform.isIdentity } private var zoomScale: CGFloat = 1 private var zoomOffset: CGPoint = .zero - func installZoomGestures() { + // Forward the two properties the plugin sets so call sites read like a VideoView. + var track: VideoTrack? { get { video.track } set { video.track = newValue } } + var layoutMode: VideoView.LayoutMode { get { video.layoutMode } set { video.layoutMode = newValue } } + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .black + clipsToBounds = true + layer.cornerRadius = 8 + video.layoutMode = .fill + addSubview(video) + installZoomGestures() + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + // Position via bounds+center (not frame) so it coexists with the zoom transform. + override func layoutSubviews() { + super.layoutSubviews() + video.bounds = CGRect(origin: .zero, size: bounds.size) + video.center = CGPoint(x: bounds.midX, y: bounds.midY) + } + + private func installZoomGestures() { isUserInteractionEnabled = true let pinch = UIPinchGestureRecognizer(target: self, action: #selector(onPinch(_:))); pinch.delegate = self let pan = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:))); pan.delegate = self @@ -568,11 +590,11 @@ final class TileVideoView: VideoView, UIGestureRecognizerDelegate { func resetZoom() { zoomScale = 1; zoomOffset = .zero - if !transform.isIdentity { transform = .identity } + if !video.transform.isIdentity { video.transform = .identity } } private func apply() { - transform = CGAffineTransform(translationX: zoomOffset.x, y: zoomOffset.y).scaledBy(x: zoomScale, y: zoomScale) + video.transform = CGAffineTransform(translationX: zoomOffset.x, y: zoomOffset.y).scaledBy(x: zoomScale, y: zoomScale) } @objc private func onPinch(_ g: UIPinchGestureRecognizer) {