LiveKitBridge.swift

  1import Foundation
  2import LiveKit
  3import WebRTC
  4import ScreenCaptureKit
  5
  6class LKRoomDelegate: RoomDelegate {
  7    var data: UnsafeRawPointer
  8    var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void
  9    var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void
 10    var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
 11    var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void
 12    var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void
 13    var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
 14    var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
 15
 16    init(
 17        data: UnsafeRawPointer,
 18        onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
 19        onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void,
 20        onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
 21        onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
 22        onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
 23        onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
 24        onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void)
 25    {
 26        self.data = data
 27        self.onDidDisconnect = onDidDisconnect
 28        self.onDidSubscribeToRemoteAudioTrack = onDidSubscribeToRemoteAudioTrack
 29        self.onDidUnsubscribeFromRemoteAudioTrack = onDidUnsubscribeFromRemoteAudioTrack
 30        self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
 31        self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack
 32        self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack
 33        self.onActiveSpeakersChanged = onActiveSpeakersChanged
 34    }
 35
 36    func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) {
 37        if connectionState.isDisconnected {
 38            self.onDidDisconnect(self.data)
 39        }
 40    }
 41
 42    func room(_ room: Room, participant: RemoteParticipant, didSubscribe publication: RemoteTrackPublication, track: Track) {
 43        if track.kind == .video {
 44            self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
 45        } else if track.kind == .audio {
 46            self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque(), Unmanaged.passUnretained(publication).toOpaque())
 47        }
 48    }
 49
 50    func room(_ room: Room, participant: Participant, didUpdate publication: TrackPublication, muted: Bool) {
 51        if publication.kind == .audio {
 52            self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted)
 53        }
 54    }
 55
 56    func room(_ room: Room, didUpdate speakers: [Participant]) {
 57        guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return }
 58        self.onActiveSpeakersChanged(self.data, speaker_ids)
 59    }
 60
 61    func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) {
 62        if track.kind == .video {
 63            self.onDidUnsubscribeFromRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString)
 64        } else if track.kind == .audio {
 65            self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString)
 66        }
 67    }
 68}
 69
 70class LKVideoRenderer: NSObject, VideoRenderer {
 71    var data: UnsafeRawPointer
 72    var onFrame: @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool
 73    var onDrop: @convention(c) (UnsafeRawPointer) -> Void
 74    var adaptiveStreamIsEnabled: Bool = false
 75    var adaptiveStreamSize: CGSize = .zero
 76    weak var track: VideoTrack?
 77
 78    init(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) {
 79        self.data = data
 80        self.onFrame = onFrame
 81        self.onDrop = onDrop
 82    }
 83
 84    deinit {
 85        self.onDrop(self.data)
 86    }
 87
 88    func setSize(_ size: CGSize) {
 89    }
 90
 91    func renderFrame(_ frame: RTCVideoFrame?) {
 92        let buffer = frame?.buffer as? RTCCVPixelBuffer
 93        if let pixelBuffer = buffer?.pixelBuffer {
 94            if !self.onFrame(self.data, pixelBuffer) {
 95                DispatchQueue.main.async {
 96                    self.track?.remove(videoRenderer: self)
 97                }
 98            }
 99        }
100    }
101}
102
103@_cdecl("LKRoomDelegateCreate")
104public func LKRoomDelegateCreate(
105    data: UnsafeRawPointer,
106    onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
107    onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void,
108    onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
109    onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
110    onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
111    onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
112    onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
113) -> UnsafeMutableRawPointer {
114    let delegate = LKRoomDelegate(
115        data: data,
116        onDidDisconnect: onDidDisconnect,
117        onDidSubscribeToRemoteAudioTrack: onDidSubscribeToRemoteAudioTrack,
118        onDidUnsubscribeFromRemoteAudioTrack: onDidUnsubscribeFromRemoteAudioTrack,
119        onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack,
120        onActiveSpeakersChanged: onActiveSpeakerChanged,
121        onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack,
122        onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack
123    )
124    return Unmanaged.passRetained(delegate).toOpaque()
125}
126
127@_cdecl("LKRoomCreate")
128public func LKRoomCreate(delegate: UnsafeRawPointer) -> UnsafeMutableRawPointer  {
129    let delegate = Unmanaged<LKRoomDelegate>.fromOpaque(delegate).takeUnretainedValue()
130    return Unmanaged.passRetained(Room(delegate: delegate)).toOpaque()
131}
132
133@_cdecl("LKRoomConnect")
134public func LKRoomConnect(room: UnsafeRawPointer, url: CFString, token: CFString, callback: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, callback_data: UnsafeRawPointer) {
135    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
136
137    room.connect(url as String, token as String).then { _ in
138        callback(callback_data, UnsafeRawPointer(nil) as! CFString?)
139    }.catch { error in
140        callback(callback_data, error.localizedDescription as CFString)
141    }
142}
143
144@_cdecl("LKRoomDisconnect")
145public func LKRoomDisconnect(room: UnsafeRawPointer) {
146    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
147    room.disconnect()
148}
149
150@_cdecl("LKRoomPublishVideoTrack")
151public func LKRoomPublishVideoTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, UnsafeMutableRawPointer?, CFString?) -> Void, callback_data: UnsafeRawPointer) {
152    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
153    let track = Unmanaged<LocalVideoTrack>.fromOpaque(track).takeUnretainedValue()
154    room.localParticipant?.publishVideoTrack(track: track).then { publication in
155        callback(callback_data, Unmanaged.passRetained(publication).toOpaque(), nil)
156    }.catch { error in
157        callback(callback_data, nil, error.localizedDescription as CFString)
158    }
159}
160
161@_cdecl("LKRoomPublishAudioTrack")
162public func LKRoomPublishAudioTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, UnsafeMutableRawPointer?, CFString?) -> Void, callback_data: UnsafeRawPointer) {
163    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
164    let track = Unmanaged<LocalAudioTrack>.fromOpaque(track).takeUnretainedValue()
165    room.localParticipant?.publishAudioTrack(track: track).then { publication in
166        callback(callback_data, Unmanaged.passRetained(publication).toOpaque(), nil)
167    }.catch { error in
168        callback(callback_data, nil, error.localizedDescription as CFString)
169    }
170}
171
172
173@_cdecl("LKRoomUnpublishTrack")
174public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawPointer) {
175    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
176    let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
177    let _ = room.localParticipant?.unpublish(publication: publication)
178}
179
180@_cdecl("LKRoomAudioTracksForRemoteParticipant")
181public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
182    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
183
184    for (_, participant) in room.remoteParticipants {
185        if participant.identity == participantId as String {
186            return participant.audioTracks.compactMap { $0.track as? RemoteAudioTrack } as CFArray?
187        }
188    }
189
190    return nil;
191}
192
193@_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant")
194public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
195    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
196
197    for (_, participant) in room.remoteParticipants {
198        if participant.identity == participantId as String {
199            return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray?
200        }
201    }
202
203    return nil;
204}
205
206@_cdecl("LKRoomVideoTracksForRemoteParticipant")
207public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
208    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
209
210    for (_, participant) in room.remoteParticipants {
211        if participant.identity == participantId as String {
212            return participant.videoTracks.compactMap { $0.track as? RemoteVideoTrack } as CFArray?
213        }
214    }
215
216    return nil;
217}
218
219@_cdecl("LKLocalAudioTrackCreateTrack")
220public func LKLocalAudioTrackCreateTrack() -> UnsafeMutableRawPointer {
221    let track = LocalAudioTrack.createTrack(options: AudioCaptureOptions(
222      echoCancellation: true,
223      noiseSuppression: true
224    ))
225
226    return Unmanaged.passRetained(track).toOpaque()
227}
228
229
230@_cdecl("LKCreateScreenShareTrackForDisplay")
231public func LKCreateScreenShareTrackForDisplay(display: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer {
232    let display = Unmanaged<MacOSDisplay>.fromOpaque(display).takeUnretainedValue()
233    let track = LocalVideoTrack.createMacOSScreenShareTrack(source: display, preferredMethod: .legacy)
234    return Unmanaged.passRetained(track).toOpaque()
235}
236
237@_cdecl("LKVideoRendererCreate")
238public func LKVideoRendererCreate(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer {
239    Unmanaged.passRetained(LKVideoRenderer(data: data, onFrame: onFrame, onDrop: onDrop)).toOpaque()
240}
241
242@_cdecl("LKVideoTrackAddRenderer")
243public func LKVideoTrackAddRenderer(track: UnsafeRawPointer, renderer: UnsafeRawPointer) {
244    let track = Unmanaged<Track>.fromOpaque(track).takeUnretainedValue() as! VideoTrack
245    let renderer = Unmanaged<LKVideoRenderer>.fromOpaque(renderer).takeRetainedValue()
246    renderer.track = track
247    track.add(videoRenderer: renderer)
248}
249
250@_cdecl("LKRemoteVideoTrackGetSid")
251public func LKRemoteVideoTrackGetSid(track: UnsafeRawPointer) -> CFString {
252    let track = Unmanaged<RemoteVideoTrack>.fromOpaque(track).takeUnretainedValue()
253    return track.sid! as CFString
254}
255
256@_cdecl("LKRemoteAudioTrackGetSid")
257public func LKRemoteAudioTrackGetSid(track: UnsafeRawPointer) -> CFString {
258    let track = Unmanaged<RemoteAudioTrack>.fromOpaque(track).takeUnretainedValue()
259    return track.sid! as CFString
260}
261
262@_cdecl("LKDisplaySources")
263public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, CFArray?, CFString?) -> Void) {
264    MacOSScreenCapturer.sources(for: .display, includeCurrentApplication: false, preferredMethod: .legacy).then { displaySources in
265        callback(data, displaySources as CFArray, nil)
266    }.catch { error in
267        callback(data, nil, error.localizedDescription as CFString)
268    }
269}
270
271@_cdecl("LKLocalTrackPublicationSetMute")
272public func LKLocalTrackPublicationSetMute(
273    publication: UnsafeRawPointer,
274    muted: Bool,
275    on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void,
276    callback_data: UnsafeRawPointer
277) {
278    let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
279
280    if muted {
281        publication.mute().then {
282            on_complete(callback_data, nil)
283        }.catch { error in
284            on_complete(callback_data, error.localizedDescription as CFString)
285        }
286    } else {
287        publication.unmute().then {
288            on_complete(callback_data, nil)
289        }.catch { error in
290            on_complete(callback_data, error.localizedDescription as CFString)
291        }
292    }
293}
294
295@_cdecl("LKRemoteTrackPublicationSetEnabled")
296public func LKRemoteTrackPublicationSetEnabled(
297    publication: UnsafeRawPointer,
298    enabled: Bool,
299    on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void,
300    callback_data: UnsafeRawPointer
301) {
302    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
303
304    publication.set(enabled: enabled).then {
305        on_complete(callback_data, nil)
306    }.catch { error in
307        on_complete(callback_data, error.localizedDescription as CFString)
308    }
309}
310
311@_cdecl("LKRemoteTrackPublicationIsMuted")
312public func LKRemoteTrackPublicationIsMuted(
313    publication: UnsafeRawPointer
314) -> Bool {
315    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
316
317    return publication.muted
318}
319
320@_cdecl("LKRemoteTrackPublicationGetSid")
321public func LKRemoteTrackPublicationGetSid(
322    publication: UnsafeRawPointer
323) -> CFString {
324    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
325
326    return publication.sid as CFString
327}