diff --git a/CHANGES.md b/CHANGES.md index 47db832e..f498e6e6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,9 @@ ## develop +- [UPDATE] VideoHardMuteActor での映像ハードミュート解除時にカメラキャプチャ未起動なら開始するようにする + - `Configuration.initialCameraEnabled` により接続時にカメラ初期化が行われていない場合の分岐 + - @t-miya - [UPDATE] libwebrtc m144.7559.2.1 に上げる - @t-miya - [UPDATE] Statistics, StatisticsEntry をドキュメント対象として公開する @@ -19,6 +22,9 @@ - [UPDATE] Configuration.simulcastRid を非推奨にする - 移行先は `Configuration.simulcastRequestRid` - @zztkm +- [ADD] Configuration に接続確立時にカメラ初期化を行わない設定 `initialCameraEnabled` を追加する + - 接続時に映像ハードミュートを行うために利用する + - @t-miya - [ADD] MediaChannel に音声ソフトミュートを設定する `setAudioSoftMute(_:)` を追加する - 送信ストリームの AudioTrack を取得し、MediaStream.audioEnabled を切り替える - デジタルサイレンスパケットが送られる状態となり、マイクからの音声は送出されない diff --git a/Sora/Configuration.swift b/Sora/Configuration.swift index eeb530cf..6038d96c 100644 --- a/Sora/Configuration.swift +++ b/Sora/Configuration.swift @@ -135,6 +135,13 @@ public struct Configuration { /// デフォルトは `true` です。 public var audioEnabled: Bool = true + /// 接続確立時に端末カメラキャプチャを自動起動するかどうか。 + /// + /// `cameraSettings.isEnabled` が `true` の場合でも、このフラグが `false` であれば + /// 接続時点ではカメラキャプチャを起動しません。 + /// 後から `MediaChannel.setVideoHardMute(false)` で必要に応じて開始できます。 + public var initialCameraEnabled: Bool = true + /// サイマルキャストの可否。 `true` であればサイマルキャストを有効にします。 public var simulcastEnabled: Bool = false diff --git a/Sora/MediaChannel.swift b/Sora/MediaChannel.swift index fa67d572..d3fd1421 100644 --- a/Sora/MediaChannel.swift +++ b/Sora/MediaChannel.swift @@ -754,10 +754,18 @@ public final class MediaChannel { if mute { // ソフトミュートによる黒塗りフレーム送出 -> ハードミュート有効化の順になるようにします senderStream.videoEnabled = false - try await Self.videoHardMuteActor.setMute(mute: true, senderStream: senderStream) + try await Self.videoHardMuteActor.setMute( + mute: true, + senderStream: senderStream, + cameraSettings: configuration.cameraSettings + ) } else { // ハードミュート無効化 -> ソフトミュートによる黒塗りフレーム送出解除の順になるようにします - try await Self.videoHardMuteActor.setMute(mute: false, senderStream: senderStream) + try await Self.videoHardMuteActor.setMute( + mute: false, + senderStream: senderStream, + cameraSettings: configuration.cameraSettings + ) senderStream.videoEnabled = true } Logger.debug(type: .mediaChannel, message: "setVideoHardMute mute=\(mute)") diff --git a/Sora/PeerChannel.swift b/Sora/PeerChannel.swift index 9396162b..f8a479ba 100644 --- a/Sora/PeerChannel.swift +++ b/Sora/PeerChannel.swift @@ -482,7 +482,9 @@ class PeerChannel: NSObject, RTCPeerConnectionDelegate { } // カメラの初期化 - if configuration.videoEnabled, configuration.cameraSettings.isEnabled { + if configuration.videoEnabled, configuration.cameraSettings.isEnabled, + configuration.initialCameraEnabled + { initializeCameraVideoCapture(stream: stream) } diff --git a/Sora/VideoMute.swift b/Sora/VideoMute.swift index f15584db..60e484fa 100644 --- a/Sora/VideoMute.swift +++ b/Sora/VideoMute.swift @@ -5,18 +5,19 @@ import Foundation actor VideoHardMuteActor { // 処理実行中フラグ private var isProcessing = false - // カメラ操作のためのキャプチャラー - private var capturer: CameraVideoCapturer? + // ハードミュートで stop したキャプチャを解除時に restart するための保持キャプチャラー + private var storedCapturer: CameraVideoCapturer? /// ハードミュートを有効化/無効化します /// /// - Parameters: /// - mute: `true` で有効化、`false` で無効化 /// - senderStream: 送信ストリーム + /// - cameraSettings: カメラ設定 /// - Throws: - /// - 既に処理実行中、またはカメラキャプチャラーが無効な場合は `SoraError.mediaChannelError` + /// - 既に処理実行中の場合は `SoraError.mediaChannelError` /// - カメラ操作の失敗時は `SoraError.cameraError` - func setMute(mute: Bool, senderStream: MediaStream) async throws { + func setMute(mute: Bool, senderStream: MediaStream, cameraSettings: CameraSettings) async throws { guard !isProcessing else { throw SoraError.mediaChannelError(reason: "video hard mute operation is in progress") } @@ -26,13 +27,12 @@ actor VideoHardMuteActor { // ミュートを有効化します if mute { guard let currentCapturer = await currentCameraVideoCapturer() else { - // 既にハードミュート済み(再開用キャプチャラーを保持)なら、冪等として何もしません - if capturer != nil { return } - throw SoraError.mediaChannelError(reason: "CameraVideoCapturer is unavailable") + // キャプチャ未起動の場合は停止対象がないため、冪等として成功扱いにします + return } try await stopCameraVideoCapture(currentCapturer) // ミュート無効化する際にキャプチャラーを使用するため保持しておきます - capturer = currentCapturer + storedCapturer = currentCapturer return } @@ -40,11 +40,12 @@ actor VideoHardMuteActor { // 現在のキャプチャラーが取得できる場合は既に再開済みとして成功扱いにします let currentCapturer = await currentCameraVideoCapturer() if currentCapturer != nil { return } - // 前回停止時のキャプチャラーが保持できていない場合エラー - guard let stored = capturer else { - throw SoraError.mediaChannelError(reason: "CameraVideoCapturer is unavailable") + // 前回停止時のキャプチャラーが保持できていれば restart、なければ start します + if let capturerForRestart = storedCapturer { + try await restartCameraVideoCapture(capturerForRestart, senderStream: senderStream) + return } - try await restartCameraVideoCapture(stored, senderStream: senderStream) + try await startCameraVideoCapture(cameraSettings: cameraSettings, senderStream: senderStream) } // 現在のカメラキャプチャラーを取得します @@ -95,4 +96,76 @@ actor VideoHardMuteActor { } } } + + // カメラキャプチャを開始します + private func startCameraVideoCapture( + cameraSettings: CameraSettings, + senderStream: MediaStream + ) async throws { + // libwebrtc のカメラ用キュー(SoraDispatcher)を利用して実行します + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + SoraDispatcher.async(on: .camera) { + // 接続時設定の position に対応した CameraVideoCapturer を取得します。 + // `.front` / `.back` を優先して利用し、静的プロパティ経由で参照される状態と齟齬が出ないようにします。 + let capturer: CameraVideoCapturer + switch cameraSettings.position { + case .front: + guard let front = CameraVideoCapturer.front else { + continuation.resume( + throwing: SoraError.cameraError(reason: "front camera is not found")) + return + } + capturer = front + case .back: + guard let back = CameraVideoCapturer.back else { + continuation.resume(throwing: SoraError.cameraError(reason: "back camera is not found")) + return + } + capturer = back + case .unspecified: + continuation.resume( + throwing: SoraError.cameraError( + reason: "CameraSettings.position should not be .unspecified" + ) + ) + return + @unknown default: + guard let device = CameraVideoCapturer.device(for: cameraSettings.position) else { + continuation.resume( + throwing: SoraError.cameraError(reason: "camera device is not found for position") + ) + return + } + capturer = CameraVideoCapturer(device: device) + } + + guard + // 接続時設定に基づいてカメラの解像度、フレームレートを指定します + let format = CameraVideoCapturer.format( + width: cameraSettings.resolution.width, + height: cameraSettings.resolution.height, + for: capturer.device, + frameRate: cameraSettings.frameRate), + let frameRate = CameraVideoCapturer.maxFrameRate(cameraSettings.frameRate, for: format) + else { + continuation.resume( + throwing: SoraError.cameraError(reason: "failed to resolve camera settings")) + return + } + + // カメラキャプチャを開始します + // CameraVideoCapturer.start はコールバック形式です + capturer.stream = senderStream + // start 完了まで capturer を確実に生存させるためにクロージャ側でも保持します。 + // start 成功時は CameraVideoCapturer.current がセットされ、以後はそちらが保持します。 + capturer.start(format: format, frameRate: frameRate) { [capturer] error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ()) + } + } + } + } + } }