Fix hole-punch: put native video BEHIND the WebView, not under its scrollView

Diagnosis: no native video rendered at all (bg went black, but camera AND
screen tiles were empty). WKWebView does NOT composite native subviews placed
under its scrollView through transparent web content — only the webView's own
layer background shows through. So the video was effectively invisible.

Fix (the standard hole-punch): insert the video tiles into the WebView's
SUPERVIEW, BEHIND the (fully transparent) WebView, so the web content paints
on top and the video shows through. Changes:
- makeTileView / syncVideoTiles: insert belowSubview: host (the WebView) in
  host.superview, and offset frames by the WebView's origin (getBoundingClientRect
  is viewport-relative).
- webView.backgroundColor = .clear (was .black — a black webView bg would have
  occluded the video behind it); the VC view's black background is the backing.

Plugin-only change — needs a Codemagic build. Web (transparent chain, zoom,
bz-hasvid) already correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 12:29:01 +05:30
parent c79d78485b
commit d9ea104b73
@@ -178,11 +178,12 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
}
}
// Create a tile view and place it BEHIND the web content (hole-punch), so the web tile's own name/mute/
// border/avatar render on top. No native overlays needed the WebView draws them.
// Create a tile view BEHIND the whole WebView (in its superview). WKWebView does NOT composite native
// subviews placed under its scrollView through transparent web content, so the video must sit behind the
// (transparent) WebView itself; the web UI then paints on top. No native overlays the web tile draws them.
private func makeTileView(host: WKWebView, key: String) -> TileVideoView {
let v = TileVideoView(frame: .zero)
host.insertSubview(v, belowSubview: host.scrollView)
if let sup = host.superview { sup.insertSubview(v, belowSubview: host) } else { host.addSubview(v) }
tileViews[key] = v
return v
}
@@ -201,9 +202,9 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
self.savedVCBg = self.bridge?.viewController?.view.backgroundColor
}
web.isOpaque = false
web.backgroundColor = .black // shows through the transparent meeting; also the letterbox bg
web.backgroundColor = .clear // fully transparent so the video BEHIND the WebView shows through
web.scrollView.backgroundColor = .clear
self.bridge?.viewController?.view.backgroundColor = .black // backing so the transparent meeting reads black, not white
self.bridge?.viewController?.view.backgroundColor = .black // the black backing (behind the video) for gaps/letterbox
self.holePunchOn = true
} else {
self.applyHolePunchRestore(web)
@@ -388,12 +389,13 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
: self.cameraTrack(forUid: uid, isLocal: isLocal) else { continue }
wanted.insert(key)
let vv = self.tileViews[key] ?? self.makeTileView(host: host, key: key)
if vv.superview !== host { host.insertSubview(vv, belowSubview: host.scrollView) } // keep BEHIND web UI
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; the zoom transform (web-forwarded via setTileZoom) lives on
// the INNER video, so this never fights an active zoom.
vv.frame = CGRect(x: x, y: y, width: w, height: h)
// 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)
}
// Drop views for participants no longer present / camera turned off.
for (key, vv) in self.tileViews where !wanted.contains(key) {