From c24d5d828a636a5cadc52c9e5b23f9df44869879 Mon Sep 17 00:00:00 2001 From: sravan Date: Tue, 11 Aug 2026 14:13:22 +0530 Subject: [PATCH] Hole-punch attempt 3: set WebView transparent at startup + scrollView.isOpaque MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../NativeCallPlugin/NativeCallPlugin.swift | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift index c8a6117..72b65c2 100644 --- a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift +++ b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift @@ -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) {