From aed3d22675dbfe440c9f0b4b82828dcc0a180314 Mon Sep 17 00:00:00 2001 From: sravan Date: Thu, 6 Aug 2026 20:56:27 +0530 Subject: [PATCH] Native screen-share: maximize the shared screen + pinch-to-zoom Better visibility: on a native call, a shared screen now fills the whole meeting area and hides the small participant tiles (new .scr-full grid class, toggled when meetNative && sharing). The plugin renders the screen over that full-area stage rect. Zoom: the tile video view is now TileVideoView with pinch-to-zoom + pan (and double-tap to reset) enabled only while it's showing a screen. While zoomed the view holds a transform and syncVideoTiles stops overwriting its frame; name/mute overlays hide during zoom. Needs a Codemagic build (plugin change). Web deployed. Co-Authored-By: Claude Opus 4.8 --- .../NativeCallPlugin/NativeCallPlugin.swift | 74 ++++++++++++++++--- server/public/home.html | 6 ++ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift index 38ae723..cddb6fd 100644 --- a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift +++ b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift @@ -43,10 +43,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega private var endedCalls = Set() private var room: Room? private var activeUUID: UUID? - // Native video: one native VideoView per visible participant (key "__local" or the remote user id), - // drawn over the WebView and positioned to match the web meeting tiles (see syncVideoTiles). VideoView is - // a UIKit view — only ever touched on the main thread. - private var tileViews: [String: VideoView] = [:] + // Native video: one native video view per visible participant (key "__local" or the remote user id), + // drawn over the WebView and positioned to match the web meeting tiles (see syncVideoTiles). UIKit views — + // only ever touched on the main thread. TileVideoView adds pinch-zoom/pan for shared screens. + private var tileViews: [String: TileVideoView] = [:] override public func load() { let config = CXProviderConfiguration(localizedName: "Biz Connect") @@ -164,12 +164,13 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega // Build a tile VideoView with its native name label (bottom-left) + mute badge (bottom-right) — the video // covers the web tile, so these redraw the essentials the web can no longer show through. - private func makeTileView(host: UIView, key: String) -> VideoView { - let v = VideoView() + private func makeTileView(host: UIView, key: String) -> TileVideoView { + let v = TileVideoView() v.layoutMode = .fill v.backgroundColor = .black v.clipsToBounds = true v.layer.cornerRadius = 8 + v.installZoomGestures() host.addSubview(v) // Name chip (bottom-left): white text on a dark translucent pill so it's legible over ANY video @@ -209,13 +210,15 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega return v } - private func updateTileOverlay(_ vv: VideoView, name: String, muted: Bool) { + private func updateTileOverlay(_ vv: TileVideoView, name: String, muted: Bool) { // Keep the overlays above the video renderer (VideoView adds its renderer when the track is set). + // Hide them while zoomed into a screen — a scaled name chip over content just gets in the way. + let zoomed = vv.isZoomed if let label = vv.viewWithTag(NativeCallPlugin.nameTag) as? UILabel { - label.text = name; label.isHidden = name.isEmpty; vv.bringSubviewToFront(label) + label.text = name; label.isHidden = zoomed || name.isEmpty; vv.bringSubviewToFront(label) } if let badge = vv.viewWithTag(NativeCallPlugin.muteTag) { - badge.isHidden = !muted; vv.bringSubviewToFront(badge) + badge.isHidden = zoomed || !muted; vv.bringSubviewToFront(badge) } } @@ -353,9 +356,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega wanted.insert(key) let vv = self.tileViews[key] ?? self.makeTileView(host: host, key: key) if vv.superview !== host { host.addSubview(vv) } + vv.isScreen = wantScreen // enables pinch-zoom on shared screens (resets zoom if turned off) vv.layoutMode = wantScreen ? .fit : .fill if vv.track !== track { vv.track = track } - vv.frame = CGRect(x: x, y: y, width: w, height: h) + if vv.transform.isIdentity { vv.frame = CGRect(x: x, y: y, width: w, height: h) } // don't fight an active zoom // 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) } @@ -516,6 +520,56 @@ 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 +// legible); double-tap resets. Camera tiles keep isScreen=false and never zoom. While zoomed the view carries +// a transform, and syncVideoTiles stops overwriting its frame so the zoom isn't fought each poll. +final class TileVideoView: VideoView, UIGestureRecognizerDelegate { + var isScreen = false { didSet { if !isScreen { resetZoom() } } } + var isZoomed: Bool { !transform.isIdentity } + private var zoomScale: CGFloat = 1 + private var zoomOffset: CGPoint = .zero + + func installZoomGestures() { + isUserInteractionEnabled = true + let pinch = UIPinchGestureRecognizer(target: self, action: #selector(onPinch(_:))); pinch.delegate = self + let pan = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:))); pan.delegate = self + let dtap = UITapGestureRecognizer(target: self, action: #selector(onDoubleTap)); dtap.numberOfTapsRequired = 2 + addGestureRecognizer(pinch); addGestureRecognizer(pan); addGestureRecognizer(dtap) + } + + func resetZoom() { + zoomScale = 1; zoomOffset = .zero + if !transform.isIdentity { transform = .identity } + } + + private func apply() { + transform = CGAffineTransform(translationX: zoomOffset.x, y: zoomOffset.y).scaledBy(x: zoomScale, y: zoomScale) + } + + @objc private func onPinch(_ g: UIPinchGestureRecognizer) { + guard isScreen else { return } + if g.state == .changed { + zoomScale = min(5, max(1, zoomScale * g.scale)); g.scale = 1 + if zoomScale <= 1 { zoomOffset = .zero } + apply() + } else if (g.state == .ended || g.state == .cancelled), zoomScale <= 1.01 { + resetZoom() + } + } + + @objc private func onPan(_ g: UIPanGestureRecognizer) { + guard isScreen, zoomScale > 1 else { return } + let t = g.translation(in: self); g.setTranslation(.zero, in: self) + zoomOffset.x += t.x; zoomOffset.y += t.y + apply() + } + + @objc private func onDoubleTap() { guard isScreen else { return }; resetZoom() } + + // Let pinch + pan run together for a natural zoom/pan. + func gestureRecognizer(_ g: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool { true } +} + // A UILabel with padding, so the name chip has breathing room around its text. final class PaddingLabel: UILabel { var insets = UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6) diff --git a/server/public/home.html b/server/public/home.html index 6a8119b..8a5c664 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -410,6 +410,11 @@ .meet-grid.sharing-mode .meet-tile:not(.stage){width:160px;height:94px;flex:0 0 auto;} .meet-grid.sharing-mode .meet-tile.sharing:not(.stage){cursor:pointer;} .meet-grid.sharing-mode .meet-tile.sharing:not(.stage)::after{content:"Click to view";position:absolute;left:0;right:0;bottom:0;font-size:.58rem;text-align:center;background:rgba(37,99,235,.88);color:#fff;padding:1px 0;} + /* Native call: a shared screen should DOMINATE — fill the whole meeting area and hide the small participant + tiles (the plugin renders the screen natively over this stage rect; pinch-to-zoom happens there). */ + .meet-grid.sharing-mode.scr-full{height:100%;} + .meet-grid.sharing-mode.scr-full .meet-tile.stage{width:100%;height:100%;min-height:0;} + .meet-grid.sharing-mode.scr-full .meet-tile:not(.stage){display:none;} @media (max-width:760px){ .meet-grid.sharing-mode{flex-flow:row wrap;height:auto;} .meet-grid.sharing-mode .meet-tile.stage{width:100%;height:auto;flex:1 1 100%;min-height:40vh;} @@ -5270,6 +5275,7 @@ function getSharerIds(){ const a=[]; meetSharers.forEach(id=>a.push(id)); return function updateShareMode(){ const g=document.getElementById('meetGrid'); if(!g) return; const sharers=getSharerIds(); const on=sharers.length>0; g.classList.toggle('sharing-mode', on); + g.classList.toggle('scr-full', on && meetNative); // native: maximize the shared screen, hide participant tiles if(on){ if(!meetStageId || sharers.indexOf(meetStageId)<0) meetStageId=sharers[0]; } else meetStageId=null; applyStage(); }