Hole-punch attempt 3: set WebView transparent at startup + scrollView.isOpaque
Previous hole-punch builds showed no video because WKWebView can IGNORE isOpaque=false when it's set AFTER the page has already rendered (during the call). Now: - makeWebViewTransparent() runs once at plugin load (startup): isOpaque=false, backgroundColor=.clear, and crucially scrollView.isOpaque=false + scrollView.backgroundColor=.clear (an opaque scrollView occludes content behind the WebView). - setHolePunch now just toggles a BLACK backing on the WebView's parent during the call (transparency is already applied); restored after. - Tile frames use host.convert(rect, to: superview) instead of a manual origin offset — robust to any WebView inset. Plugin-only — needs a Codemagic build. If this still shows no video, WKWebView hole-punch is a dead end here and we revert to native-video-on-top. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -55,10 +55,7 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
// Hole-punch: draw the native video BEHIND a transparent WebView so all web UI (bar, menus, panels) floats
|
||||
// on top. Saved so we can restore the WebView when the call ends.
|
||||
private var holePunchOn = false
|
||||
private var savedWebOpaque = true
|
||||
private var savedWebBg: UIColor?
|
||||
private var savedScrollBg: UIColor?
|
||||
private var savedVCBg: UIColor?
|
||||
private var savedVCBg: UIColor? // the WebView parent's original background, restored when the call ends
|
||||
|
||||
override public func load() {
|
||||
let config = CXProviderConfiguration(localizedName: "Biz Connect")
|
||||
@@ -91,6 +88,20 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
// Screen sharing (ReplayKit broadcast extension). LiveKit tells us when a broadcast starts/stops and
|
||||
// (with shouldPublishTrack=true, the default) auto-publishes/unpublishes the screen-share track.
|
||||
BroadcastManager.shared.delegate = self
|
||||
|
||||
// Make the WebView transparent at STARTUP. WKWebView can IGNORE isOpaque=false when it's flipped after
|
||||
// the page has already rendered — the likely reason the hole-punch showed no video. Doing it once, up
|
||||
// front, makes the transparent-meeting areas actually reveal the native video behind the WebView. The
|
||||
// web body is opaque, so the app looks normal outside a call.
|
||||
DispatchQueue.main.async { [weak self] in self?.makeWebViewTransparent() }
|
||||
}
|
||||
|
||||
private func makeWebViewTransparent() {
|
||||
guard let web = bridge?.webView else { return }
|
||||
web.isOpaque = false
|
||||
web.backgroundColor = .clear
|
||||
web.scrollView.isOpaque = false // the scrollView being opaque can occlude native content behind the WebView
|
||||
web.scrollView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
@objc private func audioRouteChanged(_ note: Notification) {
|
||||
@@ -188,23 +199,18 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
return v
|
||||
}
|
||||
|
||||
// Turn hole-punch on/off. On: make the WebView transparent (its black layer bg shows where the web page is
|
||||
// transparent, i.e., over the meeting tiles) so the native video BEHIND it shows through. Off: restore.
|
||||
// Turn hole-punch on/off. Transparency is already set at startup (makeWebViewTransparent); here we just add
|
||||
// a BLACK backing to the WebView's parent (the layer directly behind the video tiles) during the call, and
|
||||
// remove it + the tiles afterwards.
|
||||
@objc func setHolePunch(_ call: CAPPluginCall) {
|
||||
let on = call.getBool("on") ?? false
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self, let web = self.bridge?.webView else { call.resolve(); return }
|
||||
self.makeWebViewTransparent() // belt-and-braces
|
||||
let parent = web.superview
|
||||
if on {
|
||||
if !self.holePunchOn {
|
||||
self.savedWebOpaque = web.isOpaque
|
||||
self.savedWebBg = web.backgroundColor
|
||||
self.savedScrollBg = web.scrollView.backgroundColor
|
||||
self.savedVCBg = self.bridge?.viewController?.view.backgroundColor
|
||||
}
|
||||
web.isOpaque = false
|
||||
web.backgroundColor = .clear // fully transparent so the video BEHIND the WebView shows through
|
||||
web.scrollView.backgroundColor = .clear
|
||||
self.bridge?.viewController?.view.backgroundColor = .black // the black backing (behind the video) for gaps/letterbox
|
||||
if !self.holePunchOn { self.savedVCBg = parent?.backgroundColor }
|
||||
parent?.backgroundColor = .black
|
||||
self.holePunchOn = true
|
||||
} else {
|
||||
self.applyHolePunchRestore(web)
|
||||
@@ -215,13 +221,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the WebView to opaque (must run on main).
|
||||
// Remove the black backing (leave the WebView transparent — the web body is opaque, so it looks normal).
|
||||
private func applyHolePunchRestore(_ web: WKWebView) {
|
||||
guard holePunchOn else { return }
|
||||
web.isOpaque = savedWebOpaque
|
||||
web.backgroundColor = savedWebBg
|
||||
web.scrollView.backgroundColor = savedScrollBg
|
||||
bridge?.viewController?.view.backgroundColor = savedVCBg
|
||||
web.superview?.backgroundColor = savedVCBg
|
||||
holePunchOn = false
|
||||
}
|
||||
|
||||
@@ -392,10 +395,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
if vv.superview !== host.superview, let sup = host.superview { sup.insertSubview(vv, belowSubview: host) } // keep BEHIND the transparent WebView
|
||||
vv.layoutMode = wantScreen ? .fit : .fill
|
||||
if vv.track !== track { vv.track = track }
|
||||
// The container tracks the tile rect (getBoundingClientRect is in the WebView's viewport, so map
|
||||
// it into the superview by adding the WebView's origin). The zoom transform (web-forwarded via
|
||||
// setTileZoom) lives on the INNER video, so this never fights an active zoom.
|
||||
vv.frame = CGRect(x: x + host.frame.minX, y: y + host.frame.minY, width: w, height: h)
|
||||
// The container tracks the tile rect. getBoundingClientRect is in the WebView's coordinate space;
|
||||
// convert it into the superview (where the tiles live) — robust to any WebView offset/inset. The
|
||||
// zoom transform (web-forwarded via setTileZoom) lives on the INNER video, so this never fights it.
|
||||
vv.frame = host.convert(CGRect(x: x, y: y, width: w, height: h), to: host.superview)
|
||||
}
|
||||
// Drop views for participants no longer present / camera turned off.
|
||||
for (key, vv) in self.tileViews where !wanted.contains(key) {
|
||||
|
||||
Reference in New Issue
Block a user