Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

## develop

- [UPDATE] VideoHardMuteActor での映像ハードミュート解除時にカメラキャプチャ未起動なら開始するようにする
- `Configuration.initialCameraEnabled` により接続時にカメラ初期化が行われていない場合の分岐
- @t-miya
- [UPDATE] libwebrtc m144.7559.2.1 に上げる
- @t-miya
- [UPDATE] Statistics, StatisticsEntry をドキュメント対象として公開する
Expand All @@ -19,6 +22,9 @@
- [UPDATE] Configuration.simulcastRid を非推奨にする
- 移行先は `Configuration.simulcastRequestRid`
- @zztkm
- [ADD] Configuration に接続確立時にカメラ初期化を行わない設定 `initialCameraEnabled` を追加する
- 接続時に映像ハードミュートを行うために利用する
- @t-miya
- [ADD] MediaChannel に音声ソフトミュートを設定する `setAudioSoftMute(_:)` を追加する
- 送信ストリームの AudioTrack を取得し、MediaStream.audioEnabled を切り替える
- デジタルサイレンスパケットが送られる状態となり、マイクからの音声は送出されない
Expand Down
7 changes: 7 additions & 0 deletions Sora/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 10 additions & 2 deletions Sora/MediaChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down
4 changes: 3 additions & 1 deletion Sora/PeerChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
97 changes: 85 additions & 12 deletions Sora/VideoMute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -26,25 +27,25 @@ 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
}

// ミュートを無効化します
// 現在のキャプチャラーが取得できる場合は既に再開済みとして成功扱いにします
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)
}

// 現在のカメラキャプチャラーを取得します
Expand Down Expand Up @@ -95,4 +96,76 @@ actor VideoHardMuteActor {
}
}
}

// カメラキャプチャを開始します
private func startCameraVideoCapture(
cameraSettings: CameraSettings,
senderStream: MediaStream
) async throws {
// libwebrtc のカメラ用キュー(SoraDispatcher)を利用して実行します
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) 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: ())
}
}
}
}
}
}