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}