diff --git a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift index 770011b..c8a6117 100644 --- a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift +++ b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift @@ -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) {