Native video: readable tile labels (red mute, name chip) + flip camera

Label legibility: the name + mute overlays were both white/invisible.
- Mute badge is now a RED (#dc2626) circle with a white mic-slash, matching
  the web tile's .meet-mute, moved to the top-left.
- Name is now white on a dark translucent pill (PaddingLabel) so it stays
  legible over any video, bottom-left.

Front/back camera switch: new NativeCall.switchCamera() flips the local
CameraCapturer front<->back (switchCameraPosition, verified in 2.15.3). New
"Flip camera" button on the meeting bar (switchCamera icon), shown only
while a native call's camera is on (updateFlipBtn). Mirroring auto-corrects
(VideoView mirrorMode .auto only mirrors the front camera).

Needs a Codemagic build (plugin change). Web deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 15:58:12 +05:30
parent 34bdd8ea16
commit a5e2ed8a9d
3 changed files with 49 additions and 13 deletions
@@ -28,6 +28,7 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
CAPPluginMethod(name: "reportIncomingCall", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setMuted", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setCamera", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "switchCamera", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "syncVideoTiles", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "endCall", returnType: CAPPluginReturnPromise)
]
@@ -163,33 +164,38 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
v.layer.cornerRadius = 8
host.addSubview(v)
let label = UILabel()
// Name chip (bottom-left): white text on a dark translucent pill so it's legible over ANY video
// (plain white text vanished on bright footage).
let label = PaddingLabel()
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.backgroundColor = UIColor.black.withAlphaComponent(0.5)
label.layer.cornerRadius = 7
label.clipsToBounds = true
label.translatesAutoresizingMaskIntoConstraints = false
v.addSubview(label)
let badge = UIImageView(image: UIImage(systemName: "mic.slash.fill"))
// Mute badge (top-left): a RED circle with a white mic-slash matches the web tile's .meet-mute (#dc2626).
let badge = UIImageView(image: UIImage(systemName: "mic.slash.fill")?.withConfiguration(
UIImage.SymbolConfiguration(pointSize: 11, weight: .semibold)))
badge.tag = NativeCallPlugin.muteTag
badge.tintColor = .white
badge.contentMode = .center
badge.backgroundColor = UIColor.black.withAlphaComponent(0.55)
badge.layer.cornerRadius = 9
badge.backgroundColor = UIColor(red: 0.863, green: 0.149, blue: 0.149, alpha: 1) // #dc2626
badge.layer.cornerRadius = 10
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),
label.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -6),
label.trailingAnchor.constraint(lessThanOrEqualTo: v.trailingAnchor, constant: -6),
badge.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 6),
badge.topAnchor.constraint(equalTo: v.topAnchor, constant: 6),
badge.widthAnchor.constraint(equalToConstant: 20),
badge.heightAnchor.constraint(equalToConstant: 20),
])
tileViews[key] = v
return v
@@ -301,6 +307,19 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
}
}
// Flip the local camera between front and back.
@objc func switchCamera(_ call: CAPPluginCall) {
guard let r = room else { call.reject("no active call"); return }
let pub = r.localParticipant.videoTracks.first(where: { $0.source == .camera })
guard let track = pub?.track as? LocalVideoTrack, let cam = track.capturer as? CameraCapturer else {
call.reject("camera not active"); return
}
Task {
do { _ = try await cam.switchCameraPosition(); call.resolve() }
catch { call.reject("switch failed: \(String(describing: error))") }
}
}
// Position native video views to match the web meeting tiles. `tiles` = [{uid, local, x, y, w, h}] in
// CSS px (== points; getBoundingClientRect coords). We create/move a VideoView for each participant that
// has a live camera track, and remove views for tiles that are gone or whose camera is off (so the web
@@ -483,3 +502,13 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
notifyListeners("audioDeactivated", data: ["ok": 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)
override func drawText(in rect: CGRect) { super.drawText(in: rect.inset(by: insets)) }
override var intrinsicContentSize: CGSize {
let s = super.intrinsicContentSize
return CGSize(width: s.width + insets.left + insets.right, height: s.height + insets.top + insets.bottom)
}
}