From 312424f1e13d8f8161d68bf6cd361b2ea8fc1d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:19:36 +0100 Subject: [PATCH 1/3] FrameProcessor protocol --- Sources/LiveKit/Agent/SessionOptions.swift | 11 +++++- .../LiveKit/Core/Room+FrameProcessor.swift | 29 +++++++++++++++ .../AudioCustomProcessingDelegate.swift | 2 ++ .../LiveKit/Protocols/FrameProcessor.swift | 35 +++++++++++++++++++ .../LiveKit/Protocols/VideoProcessor.swift | 2 ++ .../BackgroundBlurVideoProcessor.swift | 4 ++- 6 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 Sources/LiveKit/Core/Room+FrameProcessor.swift create mode 100644 Sources/LiveKit/Protocols/FrameProcessor.swift diff --git a/Sources/LiveKit/Agent/SessionOptions.swift b/Sources/LiveKit/Agent/SessionOptions.swift index 96bddf01c..14e1a4b4f 100644 --- a/Sources/LiveKit/Agent/SessionOptions.swift +++ b/Sources/LiveKit/Agent/SessionOptions.swift @@ -32,10 +32,19 @@ public struct SessionOptions: Sendable { public init( room: Room = .init(), preConnectAudio: Bool = true, - agentConnectTimeout: TimeInterval = 20 + agentConnectTimeout: TimeInterval = 20, + audioProcessor: AudioFrameProcessor? = nil, + videoProcessor: VideoFrameProcessor? = nil ) { self.room = room self.preConnectAudio = preConnectAudio self.agentConnectTimeout = agentConnectTimeout + + if let audioProcessor { + room.add(audioFrameProcessor: audioProcessor) + } + if let videoProcessor { + room.add(videoFrameProcessor: videoProcessor) + } } } diff --git a/Sources/LiveKit/Core/Room+FrameProcessor.swift b/Sources/LiveKit/Core/Room+FrameProcessor.swift new file mode 100644 index 000000000..971443c6b --- /dev/null +++ b/Sources/LiveKit/Core/Room+FrameProcessor.swift @@ -0,0 +1,29 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +public extension Room { + func add(audioFrameProcessor: AudioFrameProcessor) { + add(delegate: audioFrameProcessor) + AudioManager.shared.capturePostProcessingDelegate = audioFrameProcessor + } + + func add(videoFrameProcessor: VideoFrameProcessor) { + add(delegate: videoFrameProcessor) + (localParticipant.firstCameraVideoTrack as? LocalVideoTrack)?.capturer.processor = videoFrameProcessor + } +} diff --git a/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift b/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift index 14284dfb1..8f66597d3 100644 --- a/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift +++ b/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift @@ -21,6 +21,8 @@ internal import LiveKitWebRTC public let kLiveKitKrispAudioProcessorName = "livekit_krisp_noise_cancellation" +public typealias AudioFrameProcessor = AudioCustomProcessingDelegate & FrameProcessor + /// Used to modify audio buffers before they are sent to the network or played to the user @objc public protocol AudioCustomProcessingDelegate: Sendable { diff --git a/Sources/LiveKit/Protocols/FrameProcessor.swift b/Sources/LiveKit/Protocols/FrameProcessor.swift new file mode 100644 index 000000000..25314b24e --- /dev/null +++ b/Sources/LiveKit/Protocols/FrameProcessor.swift @@ -0,0 +1,35 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +@objc +public protocol FrameProcessor: RoomDelegate, Sendable { + @objc + var isEnabled: Bool { get set } + + /// Contextual information about the track. + @objc optional + func update(roomName: String, participantIdentity: String, publicationSid: String) + + /// Credentials information. + @objc optional + func update(token: String, url: String) + + /// Called when the processor is no longer needed. + @objc optional + func close() +} diff --git a/Sources/LiveKit/Protocols/VideoProcessor.swift b/Sources/LiveKit/Protocols/VideoProcessor.swift index 8acc68b95..419a1ac92 100644 --- a/Sources/LiveKit/Protocols/VideoProcessor.swift +++ b/Sources/LiveKit/Protocols/VideoProcessor.swift @@ -16,6 +16,8 @@ import Foundation +public typealias VideoFrameProcessor = FrameProcessor & VideoProcessor + @objc public protocol VideoProcessor { func process(frame: VideoFrame) -> VideoFrame? diff --git a/Sources/LiveKit/VideoProcessors/BackgroundBlurVideoProcessor.swift b/Sources/LiveKit/VideoProcessors/BackgroundBlurVideoProcessor.swift index 6907dd1f5..3834e2d97 100644 --- a/Sources/LiveKit/VideoProcessors/BackgroundBlurVideoProcessor.swift +++ b/Sources/LiveKit/VideoProcessors/BackgroundBlurVideoProcessor.swift @@ -30,7 +30,7 @@ import os.signpost /// - Important: This class is not thread safe and will be called on a dedicated serial `processingQueue`. @available(iOS 15.0, macOS 12.0, tvOS 15.0, visionOS 1.0, *) @objc -public final class BackgroundBlurVideoProcessor: NSObject, @unchecked Sendable, VideoProcessor, Loggable { +public final class BackgroundBlurVideoProcessor: NSObject, @unchecked Sendable, VideoFrameProcessor, Loggable { #if LK_SIGNPOSTS private let signpostLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "", category: "BackgroundBlur") #endif @@ -69,6 +69,8 @@ public final class BackgroundBlurVideoProcessor: NSObject, @unchecked Sendable, private var cachedPixelBuffer: CVPixelBuffer? private var cachedPixelBufferSize: CGSize? + public var isEnabled: Bool = true + // MARK: Init /// Initialize the background blur video processor. From 6d5d7a619dabe5853edde05e07d378f6e05716b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:55:47 +0100 Subject: [PATCH 2/3] Move and respect isEnabled value --- Sources/LiveKit/Core/Room+FrameProcessor.swift | 1 + .../LiveKit/Protocols/AudioCustomProcessingDelegate.swift | 8 +++++++- Sources/LiveKit/Protocols/FrameProcessor.swift | 7 ++----- Sources/LiveKit/Protocols/VideoProcessor.swift | 2 ++ Sources/LiveKit/Track/Capturers/VideoCapturer.swift | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Sources/LiveKit/Core/Room+FrameProcessor.swift b/Sources/LiveKit/Core/Room+FrameProcessor.swift index 971443c6b..6460de968 100644 --- a/Sources/LiveKit/Core/Room+FrameProcessor.swift +++ b/Sources/LiveKit/Core/Room+FrameProcessor.swift @@ -16,6 +16,7 @@ import Foundation +@objc public extension Room { func add(audioFrameProcessor: AudioFrameProcessor) { add(delegate: audioFrameProcessor) diff --git a/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift b/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift index 8f66597d3..0774958bc 100644 --- a/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift +++ b/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift @@ -26,6 +26,9 @@ public typealias AudioFrameProcessor = AudioCustomProcessingDelegate & FrameProc /// Used to modify audio buffers before they are sent to the network or played to the user @objc public protocol AudioCustomProcessingDelegate: Sendable { + @objc + var isEnabled: Bool { get set } + /// An optional identifier for the audio processor implementation. /// This can be used to identify different types of audio processing (e.g. noise cancellation). /// Generally you can leave this as the default value. @@ -114,7 +117,10 @@ class AudioCustomProcessingDelegateAdapter: MulticastDelegate, @u func audioProcessingProcess(audioBuffer: LKRTCAudioBuffer) { let lkAudioBuffer = LKAudioBuffer(audioBuffer: audioBuffer) - target?.audioProcessingProcess(audioBuffer: lkAudioBuffer) + + if let target, target.isEnabled { + target.audioProcessingProcess(audioBuffer: lkAudioBuffer) + } // Convert to pcmBuffer and notify only if an audioRenderer is added. if isDelegatesNotEmpty, let pcmBuffer = lkAudioBuffer.toAVAudioPCMBuffer() { diff --git a/Sources/LiveKit/Protocols/FrameProcessor.swift b/Sources/LiveKit/Protocols/FrameProcessor.swift index 25314b24e..401ce5877 100644 --- a/Sources/LiveKit/Protocols/FrameProcessor.swift +++ b/Sources/LiveKit/Protocols/FrameProcessor.swift @@ -18,16 +18,13 @@ import Foundation @objc public protocol FrameProcessor: RoomDelegate, Sendable { - @objc - var isEnabled: Bool { get set } - /// Contextual information about the track. @objc optional - func update(roomName: String, participantIdentity: String, publicationSid: String) + func updateStreamInfo(roomName: String, participantIdentity: String, publicationSid: String) /// Credentials information. @objc optional - func update(token: String, url: String) + func updateCredentials(token: String, url: String) /// Called when the processor is no longer needed. @objc optional diff --git a/Sources/LiveKit/Protocols/VideoProcessor.swift b/Sources/LiveKit/Protocols/VideoProcessor.swift index 419a1ac92..a8cb13a22 100644 --- a/Sources/LiveKit/Protocols/VideoProcessor.swift +++ b/Sources/LiveKit/Protocols/VideoProcessor.swift @@ -20,5 +20,7 @@ public typealias VideoFrameProcessor = FrameProcessor & VideoProcessor @objc public protocol VideoProcessor { + var isEnabled: Bool { get set } + func process(frame: VideoFrame) -> VideoFrame? } diff --git a/Sources/LiveKit/Track/Capturers/VideoCapturer.swift b/Sources/LiveKit/Track/Capturers/VideoCapturer.swift index fcd664c91..ba819c67a 100644 --- a/Sources/LiveKit/Track/Capturers/VideoCapturer.swift +++ b/Sources/LiveKit/Track/Capturers/VideoCapturer.swift @@ -313,7 +313,7 @@ extension VideoCapturer { } // Apply processing if we have a processor attached. - if let processor = _state.processor { + if let processor = _state.processor, processor.isEnabled { guard let processedFrame = processor.process(frame: lkFrame) else { log("VideoProcessor didn't return a frame, skipping frame.", .warning) return From 76c1ebec41a435e40895145862b8af4fa5524d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:32:28 +0100 Subject: [PATCH 3/3] Alternative solution with typealias --- .../AudioCustomProcessingDelegate.swift | 2 +- .../LiveKit/Protocols/FrameProcessor.swift | 32 ------------------- .../LiveKit/Protocols/VideoProcessor.swift | 2 +- 3 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 Sources/LiveKit/Protocols/FrameProcessor.swift diff --git a/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift b/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift index 0774958bc..89fb208b8 100644 --- a/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift +++ b/Sources/LiveKit/Protocols/AudioCustomProcessingDelegate.swift @@ -21,7 +21,7 @@ internal import LiveKitWebRTC public let kLiveKitKrispAudioProcessorName = "livekit_krisp_noise_cancellation" -public typealias AudioFrameProcessor = AudioCustomProcessingDelegate & FrameProcessor +public typealias AudioFrameProcessor = AudioCustomProcessingDelegate & RoomDelegate /// Used to modify audio buffers before they are sent to the network or played to the user @objc diff --git a/Sources/LiveKit/Protocols/FrameProcessor.swift b/Sources/LiveKit/Protocols/FrameProcessor.swift deleted file mode 100644 index 401ce5877..000000000 --- a/Sources/LiveKit/Protocols/FrameProcessor.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2025 LiveKit - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Foundation - -@objc -public protocol FrameProcessor: RoomDelegate, Sendable { - /// Contextual information about the track. - @objc optional - func updateStreamInfo(roomName: String, participantIdentity: String, publicationSid: String) - - /// Credentials information. - @objc optional - func updateCredentials(token: String, url: String) - - /// Called when the processor is no longer needed. - @objc optional - func close() -} diff --git a/Sources/LiveKit/Protocols/VideoProcessor.swift b/Sources/LiveKit/Protocols/VideoProcessor.swift index a8cb13a22..1487267ba 100644 --- a/Sources/LiveKit/Protocols/VideoProcessor.swift +++ b/Sources/LiveKit/Protocols/VideoProcessor.swift @@ -16,7 +16,7 @@ import Foundation -public typealias VideoFrameProcessor = FrameProcessor & VideoProcessor +public typealias VideoFrameProcessor = RoomDelegate & VideoProcessor @objc public protocol VideoProcessor {