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