Skip to content

Commit f54bc19

Browse files
committed
VideoHardMuteSerialQueue を VideoHardMuteActor に変更
1 parent 6b73143 commit f54bc19

File tree

3 files changed

+90
-91
lines changed

3 files changed

+90
-91
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
- @t-miya
3131
- [ADD] MediaChannel に映像ハードミュートを設定する `setVideoHardMute(_:)` を追加する
3232
- CameraVideoCapturer の `stop``restart` のラッパー
33-
- ハードミュートの複数同時実行を防ぐためのシリアルキュークラス `VideoHardMuteSerialQueue` を追加する
33+
- ハードミュートの複数同時実行を防ぐための Actor `VideoHardMuteActor` を追加する
3434
- 映像ソフトミュートも併用し、黒塗りフレームの状態で停止させる
3535
- @t-miya
3636
- [ADD] 音声のハードミュート有効化/無効化機能を追加する

Sora/MediaChannel.swift

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,10 @@ public final class MediaChannel {
233233

234234
private let manager: Sora
235235

236-
// 映像ハードミュートの同時呼び出しを防ぐためのキューです
237-
// 同時に呼び出された場合は `SoraError.mediaChannelError` となります
238-
private let videoHardMuteSerialQueue = VideoHardMuteSerialQueue()
236+
// 映像ハードミュートの同時呼び出しを防ぐための Actor です
237+
// CameraVideoCapturer.current を操作するため、 MediaChannel 間でも排他実行します
238+
// また、CameraVideoCapturer.current はグローバルのため static にしています
239+
private static let videoHardMuteActor = VideoHardMuteActor()
239240

240241
// MARK: - インスタンスの生成
241242

@@ -750,13 +751,15 @@ public final class MediaChannel {
750751

751752
/// MediaChannel の接続中に映像をハードミュート有効化 / 無効化します
752753
///
753-
/// 内部でシリアルキューにより、操作を排他実行します。
754-
/// 同時に呼び出された場合はキュー側で `SoraError.mediaChannelError` がスローされます
754+
/// 内部で Actor により、操作を排他実行します。
755+
/// 同時に呼び出された場合は Actor 側で `SoraError.mediaChannelError` がスローされます
755756
///
756-
/// 映像ハードミュートは、黒塗りフレーム状態で停止させるため映像ソフトミュート用処理を併用します
757+
/// 映像ハードミュートは、黒塗りフレーム状態で停止させるためローカルトラックの停止を含みます
758+
/// 事前に映像ソフトミュートを利用していた場合は状態が上書きされます
759+
/// ハードミュート解除時に直前のソフトミュートの状態を復元するようなことはしません
757760
///
758761
/// - Parameter mute: `true` で有効化、`false` で無効化
759-
/// - Throws: エラー時は `SoraError.mediaChannelError` がスローされます
762+
/// - Throws: エラー時は `SoraError.cameraError` または `SoraError.mediaChannelError` がスローされます
760763
public func setVideoHardMute(_ mute: Bool) async throws {
761764
// 接続中か
762765
guard state == .connected else {
@@ -773,6 +776,12 @@ public final class MediaChannel {
773776
throw SoraError.mediaChannelError(reason: "role is not sender")
774777
}
775778

779+
// 接続設定でカメラ利用が有効になっているか
780+
// 端末カメラではなく別ソース(外部入力や別キャプチャ経路)の場合は false になることがあり、機能としては未対応
781+
guard configuration.cameraSettings.isEnabled else {
782+
throw SoraError.mediaChannelError(reason: "cameraSettings.isEnabled is false")
783+
}
784+
776785
// 送信ストリームが有効か
777786
guard let senderStream else {
778787
throw SoraError.mediaChannelError(reason: "senderStream is unavailable")
@@ -786,10 +795,10 @@ public final class MediaChannel {
786795
if mute {
787796
// 黒塗りフレーム送出 -> ハードミュート有効化の順になるようにします
788797
senderStream.videoEnabled = false
789-
try await videoHardMuteSerialQueue.set(mute: true, senderStream: senderStream)
798+
try await Self.videoHardMuteActor.setMute(mute: true, senderStream: senderStream)
790799
} else {
791800
// ハードミュート無効化 -> 黒塗りフレーム送出解除の順になるようにします
792-
try await videoHardMuteSerialQueue.set(mute: false, senderStream: senderStream)
801+
try await Self.videoHardMuteActor.setMute(mute: false, senderStream: senderStream)
793802
senderStream.videoEnabled = true
794803
}
795804
Logger.debug(type: .mediaChannel, message: "setVideoHardMute mute=\(mute)")

Sora/VideoMute.swift

Lines changed: 71 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,84 @@
11
import Foundation
22

3-
// 映像ハードミュートの同時呼び出しを防ぐためのシリアルキュークラスです
4-
// MediaChannel.setVideoHardMute(_:) 内での使用を想定しています
5-
final class VideoHardMuteSerialQueue {
6-
private let queue = DispatchQueue(label: "jp.shiguredo.sora.video.hardmute")
7-
8-
// 同時実行を防ぐための処理実行中フラグ
3+
// 映像ハードミュートの同時呼び出しによるレースコンディション防止を目的とした Actor です
4+
// MediaChannel.setVideoHardMute(_:) での使用を想定しています
5+
actor VideoHardMuteActor {
6+
// 処理実行中フラグ
97
private var isProcessing = false
8+
// カメラ操作のためのキャプチャラー
109
private var capturer: CameraVideoCapturer?
1110

12-
// queue 上で同時実行を防ぐ排他処理を行い、
13-
// libwebrtc のカメラ用キュー(SoraDispatcher)でカメラ操作を行います
14-
//
15-
// 既に処理中の状態で実行された、またはキャプチャラーが無効な場合は SoraError.mediaChannelError がスローされます
16-
func set(mute: Bool, senderStream: MediaStream) async throws {
17-
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
18-
queue.async { [self] in
19-
guard !isProcessing else {
20-
continuation.resume(
21-
throwing: SoraError.mediaChannelError(
22-
reason: "video hard mute operation is in progress"))
23-
return
24-
}
25-
26-
// 同時実行を防ぐための処理中フラグです
27-
isProcessing = true
28-
// CameraVideoCapturer.current は stop 実行時に nil になるため
29-
// restart 用にキャプチャラーを退避します
30-
let storedCapturer = capturer
31-
32-
// キューの完了処理です。処理中フラグの無効化と continuation を完了します
33-
// 状態更新を同一キューで直列化するため queue.async で実行します
34-
// continuation.resume も同一キュー上で呼ぶようにしています
35-
func finish(_ error: Error?, update: ((VideoHardMuteSerialQueue) -> Void)? = nil) {
36-
queue.async { [self] in
37-
update?(self)
38-
isProcessing = false
39-
if let error {
40-
continuation.resume(throwing: error)
41-
} else {
42-
continuation.resume(returning: ())
43-
}
44-
}
45-
}
11+
/// ハードミュートを有効化/無効化します
12+
/// カメラキャプチャラーの操作には libwebrtc のカメラ用キュー(SoraDispatcher)を利用して呼ぶようにします
13+
///
14+
/// - Parameters:
15+
/// - mute: `true` で有効化、`false` で無効化
16+
/// - senderStream: 送信ストリーム
17+
/// - Throws:
18+
/// - 既に処理実行中、またはカメラキャプチャラーが無効な場合は `SoraError.mediaChannelError`
19+
/// - カメラ操作の失敗時は `SoraError.cameraError`
20+
func setMute(mute: Bool, senderStream: MediaStream) async throws {
21+
guard !isProcessing else {
22+
throw SoraError.mediaChannelError(reason: "video hard mute operation is in progress")
23+
}
24+
isProcessing = true
25+
defer { isProcessing = false }
4626

47-
// libwebrtc のカメラ用キューでカメラ操作を非同期実行します
48-
SoraDispatcher.async(on: .camera) {
49-
if mute {
50-
// ミュート有効化
51-
// 起動中のカメラがあれば停止します
52-
guard let current = CameraVideoCapturer.current else {
53-
// 前回のハードミュートでキャプチャラーを退避している場合は冪等として成功扱いにします
54-
if storedCapturer != nil {
55-
finish(nil)
56-
} else {
57-
// カメラが起動しておらず再開用キャプチャラーも無い場合は失敗にします
58-
finish(SoraError.mediaChannelError(reason: "CameraVideoCapturer is unavailable"))
59-
}
60-
return
61-
}
27+
// ミュートを有効化します
28+
if mute {
29+
guard let currentCapturer = await currentCameraVideoCapturer() else {
30+
// 前回のハードミュートでキャプチャラーを保持している場合は冪等として成功扱いにします
31+
if capturer != nil { return }
32+
throw SoraError.mediaChannelError(reason: "CameraVideoCapturer is unavailable")
33+
}
34+
try await stopCameraVideoCapture(currentCapturer)
35+
// ミュート無効化する際にキャプチャラーを使用するため保持しておきます
36+
capturer = currentCapturer
37+
return
38+
}
6239

63-
// CameraVideoCapturer.stop() により映像キャプチャを停止します
64-
// CameraVideoCapturer.stop() は実行結果を Error? コールバックで返します
65-
current.stop { error in
66-
finish(error) { serialQueue in
67-
// キャプチャラーは再開用として退避します
68-
serialQueue.capturer = current
69-
}
70-
}
71-
return
72-
}
40+
// ミュートを無効化します
41+
// 現在のキャプチャラーが取得できる場合は既に再開済みとして成功扱いにします
42+
let currentCapturer = await currentCameraVideoCapturer()
43+
if currentCapturer != nil { return }
44+
// 前回停止時のキャプチャラーが保持できていない場合エラー
45+
guard let stored = capturer else {
46+
throw SoraError.mediaChannelError(reason: "CameraVideoCapturer is unavailable")
47+
}
48+
try await restartCameraVideoCapture(stored, senderStream: senderStream)
49+
}
7350

74-
// 既にカメラが起動中であれば何もしません
75-
if CameraVideoCapturer.current != nil {
76-
finish(nil)
77-
return
78-
}
51+
// 現在のカメラキャプチャラーを取得します
52+
private func currentCameraVideoCapturer() async -> CameraVideoCapturer? {
53+
await withCheckedContinuation { cont in
54+
SoraDispatcher.async(on: .camera) { cont.resume(returning: CameraVideoCapturer.current) }
55+
}
56+
}
7957

80-
// 退避済みのキャプチャラーの存在チェック
81-
guard let storedCapturer else {
82-
finish(SoraError.mediaChannelError(reason: "CameraVideoCapturer is unavailable"))
83-
return
84-
}
58+
// カメラキャプチャを停止します
59+
private func stopCameraVideoCapture(_ capturer: CameraVideoCapturer) async throws {
60+
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
61+
SoraDispatcher.async(on: .camera) {
62+
// CameraVideoCapturer.stop はコールバック形式です
63+
capturer.stop { error in
64+
if let error { cont.resume(throwing: error) } else { cont.resume(returning: ()) }
65+
}
66+
}
67+
}
68+
}
8569

86-
// マルチストリームの場合、停止時と現在の送信ストリームが異なることがあるので再設定します
87-
storedCapturer.stream = senderStream
88-
// CameraVideoCapturer.restart() により映像キャプチャを再開
89-
storedCapturer.restart { error in
90-
finish(error)
91-
}
70+
// カメラキャプチャを再開します
71+
private func restartCameraVideoCapture(
72+
_ capturer: CameraVideoCapturer,
73+
senderStream: MediaStream
74+
) async throws {
75+
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
76+
SoraDispatcher.async(on: .camera) {
77+
// マルチストリームの場合、停止時と現在の送信ストリームが異なることがあるので再設定します
78+
capturer.stream = senderStream
79+
// CameraVideoCapturer.restart はコールバック形式です
80+
capturer.restart { error in
81+
if let error { cont.resume(throwing: error) } else { cont.resume(returning: ()) }
9282
}
9383
}
9484
}

0 commit comments

Comments
 (0)