Native video: name + mute labels on the native video tiles
The native video covers the web tile's name/mute badges, so redraw those natively. syncVideoTiles now also carries each tile's name + muted state; each tile VideoView gets a bottom-left name label (with shadow for legibility) and a bottom-right mic-slash badge shown when that participant is muted. Kept above the video renderer via bringSubviewToFront. Web sends name/muted from meetNames/meetMuted (and ME.name/!meetMic for __local). Front/back camera switch + screen-share rendering still deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import PushKit
|
||||
import CallKit
|
||||
@@ -148,6 +149,62 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
}
|
||||
}
|
||||
|
||||
// Tags for the overlay subviews we add to each tile's VideoView (name label + mute badge).
|
||||
private static let nameTag = 9001
|
||||
private static let muteTag = 9002
|
||||
|
||||
// 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()
|
||||
v.layoutMode = .fill
|
||||
v.backgroundColor = .black
|
||||
v.clipsToBounds = true
|
||||
v.layer.cornerRadius = 8
|
||||
host.addSubview(v)
|
||||
|
||||
let label = UILabel()
|
||||
label.tag = NativeCallPlugin.nameTag
|
||||
label.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||
label.textColor = .white
|
||||
label.shadowColor = UIColor.black.withAlphaComponent(0.9) // legible over any video
|
||||
label.shadowOffset = CGSize(width: 0, height: 1)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
v.addSubview(label)
|
||||
|
||||
let badge = UIImageView(image: UIImage(systemName: "mic.slash.fill"))
|
||||
badge.tag = NativeCallPlugin.muteTag
|
||||
badge.tintColor = .white
|
||||
badge.contentMode = .center
|
||||
badge.backgroundColor = UIColor.black.withAlphaComponent(0.55)
|
||||
badge.layer.cornerRadius = 9
|
||||
badge.clipsToBounds = true
|
||||
badge.translatesAutoresizingMaskIntoConstraints = false
|
||||
v.addSubview(badge)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
label.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 6),
|
||||
label.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -5),
|
||||
label.trailingAnchor.constraint(lessThanOrEqualTo: badge.leadingAnchor, constant: -4),
|
||||
badge.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -6),
|
||||
badge.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -5),
|
||||
badge.widthAnchor.constraint(equalToConstant: 18),
|
||||
badge.heightAnchor.constraint(equalToConstant: 18),
|
||||
])
|
||||
tileViews[key] = v
|
||||
return v
|
||||
}
|
||||
|
||||
private func updateTileOverlay(_ vv: VideoView, name: String, muted: Bool) {
|
||||
// Keep the overlays above the video renderer (VideoView adds its renderer when the track is set).
|
||||
if let label = vv.viewWithTag(NativeCallPlugin.nameTag) as? UILabel {
|
||||
label.text = name; label.isHidden = name.isEmpty; vv.bringSubviewToFront(label)
|
||||
}
|
||||
if let badge = vv.viewWithTag(NativeCallPlugin.muteTag) {
|
||||
badge.isHidden = !muted; vv.bringSubviewToFront(badge)
|
||||
}
|
||||
}
|
||||
|
||||
// Route call audio to the LOUDSPEAKER when we'd otherwise be on the quiet earpiece. Guarded so a connected
|
||||
// wired/Bluetooth headset (any non-receiver route) still wins. Re-asserted on mute toggles because enabling
|
||||
// the mic can flip the route back to the earpiece.
|
||||
@@ -263,19 +320,12 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
||||
let key = self.tileKey(uid: uid, isLocal: isLocal)
|
||||
guard let track = self.cameraTrack(forUid: uid, isLocal: isLocal) else { continue } // camera off → leave the web avatar
|
||||
wanted.insert(key)
|
||||
let vv = self.tileViews[key] ?? {
|
||||
let v = VideoView()
|
||||
v.layoutMode = .fill
|
||||
v.backgroundColor = .black
|
||||
v.clipsToBounds = true
|
||||
v.layer.cornerRadius = 8
|
||||
host.addSubview(v)
|
||||
self.tileViews[key] = v
|
||||
return v
|
||||
}()
|
||||
let vv = self.tileViews[key] ?? self.makeTileView(host: host, key: key)
|
||||
if vv.superview !== host { host.addSubview(vv) }
|
||||
if vv.track !== track { vv.track = track }
|
||||
vv.frame = CGRect(x: x, y: y, width: w, height: h)
|
||||
// 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)
|
||||
}
|
||||
// Drop views for participants no longer present / camera turned off.
|
||||
for (key, vv) in self.tileViews where !wanted.contains(key) {
|
||||
|
||||
@@ -5376,10 +5376,11 @@ function bzNativeSyncTiles(){
|
||||
document.querySelectorAll('#meetGrid .meet-tile').forEach(el=>{
|
||||
const id=el.id ? el.id.replace('meet-tile-','') : ''; if(!id || id==='__waiting') return;
|
||||
const r=el.getBoundingClientRect(); if(r.width<2||r.height<2) return;
|
||||
let uid, local=false;
|
||||
if(id==='__local'){ uid=(ME&&ME.id)||''; local=true; } else { uid=meetPeerUids.get(id)||''; }
|
||||
let uid, local=false, name='', muted=false;
|
||||
if(id==='__local'){ uid=(ME&&ME.id)||''; local=true; name=(ME&&ME.name)||'You'; muted=!meetMic; }
|
||||
else { uid=meetPeerUids.get(id)||''; name=meetNames.get(id)||'Guest'; muted=!!meetMuted.get(id); }
|
||||
if(!uid) return;
|
||||
tiles.push({ uid, local, x:r.left, y:r.top, w:r.width, h:r.height });
|
||||
tiles.push({ uid, local, name, muted, x:r.left, y:r.top, w:r.width, h:r.height });
|
||||
});
|
||||
try{ NC.syncVideoTiles({ tiles }); }catch(_){}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user