|
1 | 1 | import Foundation |
2 | 2 |
|
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 | + // 処理実行中フラグ |
9 | 7 | private var isProcessing = false |
| 8 | + // カメラ操作のためのキャプチャラー |
10 | 9 | private var capturer: CameraVideoCapturer? |
11 | 10 |
|
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 } |
46 | 26 |
|
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 | + } |
62 | 39 |
|
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 | + } |
73 | 50 |
|
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 | + } |
79 | 57 |
|
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 | + } |
85 | 69 |
|
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: ()) } |
92 | 82 | } |
93 | 83 | } |
94 | 84 | } |
|
0 commit comments