Skip to content
Merged
2 changes: 1 addition & 1 deletion Sources/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"WebRTC_player.experimental.disclaimer" = "Note: Native WebRTC video player is currently an experimental feature, audio may not work and microphone permission and usage may be requested even though not in use. Please use the web player interface for advanced options and reliable playback.";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change will be reverted by Lokalise. But at this moment it's fine, the current copy is better for the moment in time.

"WebRTC_player.experimental.disclaimer" = "Note: Native WebRTC video player is currently an experimental feature. Please use the web player interface for advanced options and reliable playback.";
"about.acknowledgements.title" = "Acknowledgements";
"about.beta.title" = "Join Beta";
"about.chat.title" = "Chat";
Expand Down
58 changes: 53 additions & 5 deletions Sources/App/WebView/WebRTC/WebRTCClient.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AVFoundation
import Foundation
import Shared
import WebRTC
Expand Down Expand Up @@ -34,6 +35,7 @@ final class WebRTCClient: NSObject {
]
private var videoCapturer: RTCVideoCapturer?
private var remoteVideoTrack: RTCVideoTrack?
private var remoteAudioTrack: RTCAudioTrack?
private var remoteDataChannel: RTCDataChannel?

@available(*, unavailable)
Expand Down Expand Up @@ -70,10 +72,7 @@ final class WebRTCClient: NSObject {
self.peerConnection = peerConnection
super.init()
createMediaTracks()

// This is currently disable since the library does not offer a way to disable just the microphone usage.
// TODO: Find a workaround so audio can be receveid without using microphone in parallel
RTCAudioSession.sharedInstance().useManualAudio = true
configureAudioSession()

self.peerConnection.delegate = self
}
Expand Down Expand Up @@ -117,7 +116,14 @@ final class WebRTCClient: NSObject {
}

func set(remoteSdp: RTCSessionDescription, completion: @escaping (Error?) -> Void) {
peerConnection.setRemoteDescription(remoteSdp, completionHandler: completion)
peerConnection.setRemoteDescription(remoteSdp) { [weak self] error in
if let error {
Current.Log.error("Failed to set remote description: \(error.localizedDescription)")
} else {
self?.setRemoteAudioTrack()
}
completion(error)
}
Comment thread
bgoncal marked this conversation as resolved.
}

func set(remoteCandidate: RTCIceCandidate, completion: @escaping (Error?) -> Void) {
Expand All @@ -128,6 +134,35 @@ final class WebRTCClient: NSObject {
remoteVideoTrack?.add(renderer)
}

func muteAudio() {
remoteAudioTrack?.isEnabled = false
}

func unmuteAudio() {
remoteAudioTrack?.isEnabled = true
}

func isAudioMuted() -> Bool {
guard let remoteAudioTrack else { return true }
return !remoteAudioTrack.isEnabled
}

private func configureAudioSession() {
let audioSession = RTCAudioSession.sharedInstance()
audioSession.lockForConfiguration()
defer {
audioSession.unlockForConfiguration()
}
do {
// Configure for playback only (receive audio without microphone)
try audioSession.setCategory(.playback)
try audioSession.setMode(.spokenAudio)
try audioSession.setActive(true)
} catch {
Current.Log.error("Failed to configure audio session: \(error.localizedDescription)")
}
}

private func createMediaTracks() {
let streamId = "stream"
let videoTrack = createVideoTrack()
Expand All @@ -148,6 +183,19 @@ final class WebRTCClient: NSObject {
let videoTrack = WebRTCClient.factory.videoTrack(with: videoSource, trackId: "video0")
return videoTrack
}

private func setRemoteAudioTrack() {
guard let audioTransceiver = peerConnection.transceivers.first(where: { $0.mediaType == .audio }) else {
Current.Log.warning("No audio transceiver found")
return
}
guard let audioTrack = audioTransceiver.receiver.track as? RTCAudioTrack else {
Current.Log.warning("Remote track is not an RTCAudioTrack")
return
}
remoteAudioTrack = audioTrack
Current.Log.info("Remote audio track set successfully")
}
}

// MARK: - RTCPeerConnectionDelegate
Expand Down
8 changes: 5 additions & 3 deletions Sources/App/WebView/WebRTC/WebRTCVideoPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,11 @@ struct WebRTCVideoPlayerView: View {
}

private var controls: some View {
WebRTCVideoPlayerViewControls {
dismiss()
}
WebRTCVideoPlayerViewControls(
close: { dismiss() },
isMuted: viewModel.isMuted,
toggleMute: { viewModel.toggleMute() }
)
.transition(.opacity)
.animation(.easeInOut, value: viewModel.controlsVisible)
.opacity(viewModel.controlsVisible || !isVideoPlaying ? 1.0 : 0.0)
Expand Down
38 changes: 32 additions & 6 deletions Sources/App/WebView/WebRTC/WebRTCVideoPlayerViewControls.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import SFSafeSymbols
import Shared
import SwiftUI

struct WebRTCVideoPlayerViewControls: View {
let close: () -> Void
let isMuted: Bool
let toggleMute: () -> Void

// TODO: Include more player controls
var body: some View {
ZStack {
VStack {
HStack {
Spacer()
ModalCloseButton(tint: .white) {
close()
}
.padding(16)
topButtons
}
Spacer()
Text(L10n.WebRTCPlayer.Experimental.disclaimer)
Expand All @@ -33,10 +32,37 @@ struct WebRTCVideoPlayerViewControls: View {
.opacity(0.5)
)
}

@ViewBuilder
private var topButtons: some View {
Button(action: toggleMute) {
Image(systemSymbol: isMuted ? .speakerSlashFill : .speakerWave3)
.resizable()
.frame(width: 16, height: 16)
.foregroundStyle(.white)
.padding(DesignSystem.Spaces.oneAndHalf)
.modify { view in
if #available(iOS 26.0, *) {
view.glassEffect(.clear.interactive(), in: .circle)
} else {
view
.background(Color.black.opacity(0.5))
.clipShape(Circle())
}
}
}
.buttonStyle(.plain)
Comment thread
bgoncal marked this conversation as resolved.
ModalCloseButton(tint: .white) {
close()
}
.padding(16)
Comment thread
bgoncal marked this conversation as resolved.
}
}

#Preview {
WebRTCVideoPlayerViewControls(
close: {}
close: {},
isMuted: false,
toggleMute: {}
)
}
12 changes: 12 additions & 0 deletions Sources/App/WebView/WebRTC/WebRTCViewPlayerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ final class WebRTCViewPlayerViewModel: ObservableObject {
@Published var failureReason: String?
@Published var showLoader: Bool = true
@Published var controlsVisible: Bool = true
@Published var isMuted: Bool = true

var hideControlsWorkItem: DispatchWorkItem?

Expand All @@ -49,6 +50,17 @@ final class WebRTCViewPlayerViewModel: ObservableObject {
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: workItem)
}

func toggleMute() {
guard let webRTCClient else { return }
if webRTCClient.isAudioMuted() {
webRTCClient.unmuteAudio()
} else {
webRTCClient.muteAudio()
}
// Always get the final state from the client to ensure consistency
isMuted = webRTCClient.isAudioMuted()
}

// MARK: - WebRTC

func start() {
Expand Down
9 changes: 9 additions & 0 deletions Sources/Shared/DesignSystem/Components/ModalCloseButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ public struct ModalCloseButton: View {
Image(systemSymbol: .xmark)
.resizable()
.frame(width: 16, height: 16)
.modify { view in
if #available(iOS 26.0, *) {
view
.padding(DesignSystem.Spaces.oneAndHalf)
.glassEffect(.clear.interactive(), in: .circle)
} else {
view
}
}
})
.buttonStyle(.plain)
.foregroundStyle(tint)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Shared/Resources/Swiftgen/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public enum L10n {

public enum WebRTCPlayer {
public enum Experimental {
/// Note: Native WebRTC video player is currently an experimental feature, audio may not work and microphone permission and usage may be requested even though not in use. Please use the web player interface for advanced options and reliable playback.
/// Note: Native WebRTC video player is currently an experimental feature. Please use the web player interface for advanced options and reliable playback.
public static var disclaimer: String { return L10n.tr("Localizable", "WebRTC_player.experimental.disclaimer") }
}
}
Expand Down
Loading