Fix build: TileVideoView composes VideoView (can't subclass a non-open class)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -13,4 +13,4 @@ import LiveKit
|
|||||||
// convention, keyed off this extension's bundle id (com.bizgaze.connect.broadcast) and the default app group
|
// convention, keyed off this extension's bundle id (com.bizgaze.connect.broadcast) and the default app group
|
||||||
// group.<appBundleId>. Both are also set explicitly on the app side (RTCScreenSharingExtension /
|
// group.<appBundleId>. Both are also set explicitly on the app side (RTCScreenSharingExtension /
|
||||||
// RTCAppGroupIdentifier in Info.plist) so there's no ambiguity.
|
// RTCAppGroupIdentifier in Info.plist) so there's no ambiguity.
|
||||||
class SampleHandler: LKSampleHandler {}
|
final class SampleHandler: LKSampleHandler, @unchecked Sendable {}
|
||||||
|
|||||||
@@ -171,15 +171,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
|||||||
private static let nameTag = 9001
|
private static let nameTag = 9001
|
||||||
private static let muteTag = 9002
|
private static let muteTag = 9002
|
||||||
|
|
||||||
// Build a tile VideoView with its native name label (bottom-left) + mute badge (bottom-right) — the video
|
// Build a tile view (a container holding a LiveKit VideoView) with its native name label (bottom-left) +
|
||||||
// covers the web tile, so these redraw the essentials the web can no longer show through.
|
// mute badge (top-left) — the video covers the web tile, so these redraw the essentials.
|
||||||
private func makeTileView(host: UIView, key: String) -> TileVideoView {
|
private func makeTileView(host: UIView, key: String) -> TileVideoView {
|
||||||
let v = TileVideoView()
|
let v = TileVideoView(frame: .zero) // sets up its inner VideoView + zoom gestures + cosmetics in init
|
||||||
v.layoutMode = .fill
|
|
||||||
v.backgroundColor = .black
|
|
||||||
v.clipsToBounds = true
|
|
||||||
v.layer.cornerRadius = 8
|
|
||||||
v.installZoomGestures()
|
|
||||||
host.addSubview(v)
|
host.addSubview(v)
|
||||||
|
|
||||||
// Name chip (bottom-left): white text on a dark translucent pill so it's legible over ANY video
|
// 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.isScreen = wantScreen // enables pinch-zoom on shared screens (resets zoom if turned off)
|
||||||
vv.layoutMode = wantScreen ? .fit : .fill
|
vv.layoutMode = wantScreen ? .fit : .fill
|
||||||
if vv.track !== track { vv.track = track }
|
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.
|
// 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)
|
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
|
// A tile view = a container holding a LiveKit VideoView. We CANNOT subclass VideoView (it's `public`, not
|
||||||
// legible); double-tap resets. Camera tiles keep isScreen=false and never zoom. While zoomed the view carries
|
// `open`, so subclassing outside its module is illegal), so we compose instead. Pinch-to-zoom + pan apply to
|
||||||
// a transform, and syncVideoTiles stops overwriting its frame so the zoom isn't fought each poll.
|
// the INNER video while it's showing a shared SCREEN (so fine print is legible); double-tap resets. The
|
||||||
final class TileVideoView: VideoView, UIGestureRecognizerDelegate {
|
// 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 isScreen = false { didSet { if !isScreen { resetZoom() } } }
|
||||||
var isZoomed: Bool { !transform.isIdentity }
|
var isZoomed: Bool { !video.transform.isIdentity }
|
||||||
private var zoomScale: CGFloat = 1
|
private var zoomScale: CGFloat = 1
|
||||||
private var zoomOffset: CGPoint = .zero
|
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
|
isUserInteractionEnabled = true
|
||||||
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(onPinch(_:))); pinch.delegate = self
|
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(onPinch(_:))); pinch.delegate = self
|
||||||
let pan = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:))); pan.delegate = self
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:))); pan.delegate = self
|
||||||
@@ -568,11 +590,11 @@ final class TileVideoView: VideoView, UIGestureRecognizerDelegate {
|
|||||||
|
|
||||||
func resetZoom() {
|
func resetZoom() {
|
||||||
zoomScale = 1; zoomOffset = .zero
|
zoomScale = 1; zoomOffset = .zero
|
||||||
if !transform.isIdentity { transform = .identity }
|
if !video.transform.isIdentity { video.transform = .identity }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func apply() {
|
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) {
|
@objc private func onPinch(_ g: UIPinchGestureRecognizer) {
|
||||||
|
|||||||
Reference in New Issue
Block a user