From ac93d6b5800bb605dee898a570d7ec22bc8d6792 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Thu, 23 Apr 2026 15:32:48 -0600 Subject: [PATCH 1/2] remove improper Source registration implementation. limp along changes in bridge --- .token_helpers/gen_and_set.bash | 4 +- .../set_data_track_test_tokens.bash | 4 +- bridge/README.md | 20 +- .../include/livekit_bridge/livekit_bridge.h | 35 ++- bridge/src/livekit_bridge.cpp | 16 +- bridge/tests/test_livekit_bridge.cpp | 17 +- include/livekit/room.h | 25 -- .../livekit/subscription_thread_dispatcher.h | 83 +----- include/livekit/video_source.h | 2 +- src/room.cpp | 49 +--- src/subscription_thread_dispatcher.cpp | 257 ++++++------------ src/tests/unit/test_room_callbacks.cpp | 82 ++---- .../test_subscription_thread_dispatcher.cpp | 183 ++++--------- 13 files changed, 215 insertions(+), 562 deletions(-) diff --git a/.token_helpers/gen_and_set.bash b/.token_helpers/gen_and_set.bash index b933a24f..0e855e90 100755 --- a/.token_helpers/gen_and_set.bash +++ b/.token_helpers/gen_and_set.bash @@ -16,8 +16,8 @@ # Generate a LiveKit access token via `lk` and set LIVEKIT_TOKEN (and LIVEKIT_URL) # for your current shell session. # -# source examples/tokens/gen_and_set.bash --id PARTICIPANT_ID --room ROOM_NAME [--view-token] -# eval "$(bash examples/tokens/gen_and_set.bash --id ID --room ROOM [--view-token])" +# source .token_helpers/gen_and_set.bash --id PARTICIPANT_ID --room ROOM_NAME [--view-token] +# eval "$(bash .token_helpers/gen_and_set.bash --id ID --room ROOM [--view-token])" # # Optional env: LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_VALID_FOR. diff --git a/.token_helpers/set_data_track_test_tokens.bash b/.token_helpers/set_data_track_test_tokens.bash index b9bf99b8..d3520e4f 100755 --- a/.token_helpers/set_data_track_test_tokens.bash +++ b/.token_helpers/set_data_track_test_tokens.bash @@ -16,8 +16,8 @@ # Generate two LiveKit access tokens via `lk` and set the environment variables # required by src/tests/integration/test_data_track.cpp. # -# source examples/tokens/set_data_track_test_tokens.bash -# eval "$(bash examples/tokens/set_data_track_test_tokens.bash)" +# source .token_helpers/set_data_track_test_tokens.bash +# eval "$(bash .token_helpers/set_data_track_test_tokens.bash)" # # Exports: # LK_TOKEN_TEST_A diff --git a/bridge/README.md b/bridge/README.md index a2c2acf8..44de95d1 100644 --- a/bridge/README.md +++ b/bridge/README.md @@ -38,12 +38,12 @@ mic->pushFrame(pcm_data, samples_per_channel); cam->pushFrame(rgba_data, timestamp_us); // 4. Receive frames from a remote participant -bridge.setOnAudioFrameCallback("remote-peer", livekit::TrackSource::SOURCE_MICROPHONE, +bridge.setOnAudioFrameCallback("remote-peer", "mic", [](const livekit::AudioFrame& frame) { // Called on a background reader thread }); -bridge.setOnVideoFrameCallback("remote-peer", livekit::TrackSource::SOURCE_CAMERA, +bridge.setOnVideoFrameCallback("remote-peer", "cam", [](const livekit::VideoFrame& frame, int64_t timestamp_us) { // Called on a background reader thread }); @@ -117,9 +117,9 @@ Reader threads are managed by `Room` internally. They are created when a matchin ### Callback Registration Timing -Callbacks are keyed by `(participant_identity, track_source)`. You can register them **after connecting** but before the remote participant has joined the room. `Room` stores the callback and automatically wires it up when the matching track is subscribed. +Callbacks are keyed by `(participant_identity, track_name)`. You can register them **after connecting** but before the remote participant has joined the room. `Room` stores the callback and automatically wires it up when the matching track is subscribed. -> **Note:** Only one callback may be set per `(participant_identity, track_source)` pair. Calling `setOnAudioFrameCallback` or `setOnVideoFrameCallback` again with the same identity and source will silently replace the previous callback. If you need to fan-out a single stream to multiple consumers, do so inside your callback. +> **Note:** Only one callback may be set per `(participant_identity, track_name)` pair. Calling `setOnAudioFrameCallback` or `setOnVideoFrameCallback` again with the same identity and track name will silently replace the previous callback. If you need to fan-out a single stream to multiple consumers, do so inside your callback. This means the typical pattern is: @@ -128,7 +128,7 @@ This means the typical pattern is: livekit::RoomOptions options; options.auto_subscribe = true; bridge.connect(url, token, options); -bridge.setOnAudioFrameCallback("robot-1", livekit::TrackSource::SOURCE_MICROPHONE, my_callback); +bridge.setOnAudioFrameCallback("robot-1", "mic", my_callback); // When robot-1 joins and publishes a mic track, my_callback starts firing. ``` @@ -149,10 +149,10 @@ bridge.setOnAudioFrameCallback("robot-1", livekit::TrackSource::SOURCE_MICROPHON | `isConnected()` | Returns whether the bridge is currently connected. | | `createAudioTrack(name, sample_rate, num_channels, source)` | Create and publish a local audio track with the given `TrackSource` (e.g. `SOURCE_MICROPHONE`, `SOURCE_SCREENSHARE_AUDIO`). Returns an RAII `shared_ptr`. | | `createVideoTrack(name, width, height, source)` | Create and publish a local video track with the given `TrackSource` (e.g. `SOURCE_CAMERA`, `SOURCE_SCREENSHARE`). Returns an RAII `shared_ptr`. | -| `setOnAudioFrameCallback(identity, source, callback)` | Register a callback for audio frames from a specific remote participant + track source. | -| `setOnVideoFrameCallback(identity, source, callback)` | Register a callback for video frames from a specific remote participant + track source. | -| `clearOnAudioFrameCallback(identity, source)` | Clear the audio callback for a specific remote participant + track source. Stops and joins the reader thread if active. | -| `clearOnVideoFrameCallback(identity, source)` | Clear the video callback for a specific remote participant + track source. Stops and joins the reader thread if active. | +| `setOnAudioFrameCallback(identity, track_name, callback)` | Register a callback for audio frames from a specific remote participant + track name. | +| `setOnVideoFrameCallback(identity, track_name, callback)` | Register a callback for video frames from a specific remote participant + track name. | +| `clearOnAudioFrameCallback(identity, track_name)` | Clear the audio callback for a specific remote participant + track name. Stops and joins the reader thread if active. | +| `clearOnVideoFrameCallback(identity, track_name)` | Clear the video callback for a specific remote participant + track name. Stops and joins the reader thread if active. | | `performRpc(destination_identity, method, payload, response_timeout?)` | Blocking RPC call to a remote participant. Returns the response payload. Throws `livekit::RpcError` on failure. | | `registerRpcMethod(method_name, handler)` | Register a handler for incoming RPC invocations. The handler returns an optional response payload or throws `livekit::RpcError`. | | `unregisterRpcMethod(method_name)` | Unregister a previously registered RPC handler. | @@ -263,6 +263,6 @@ The bridge is designed for simplicity and currently only supports limited audio - Simulcast tuning - Video format selection (RGBA is the default; no format option yet) - Custom `RoomOptions` or `TrackPublishOptions` -- **One callback per (participant, source):** Only a single callback can be registered for each `(participant_identity, track_source)` pair. Re-registering with the same key silently replaces the previous callback. To fan-out a stream to multiple consumers, dispatch from within your single callback. +- **One callback per (participant, track_name):** Only a single callback can be registered for each `(participant_identity, track_name)` pair. Re-registering with the same key silently replaces the previous callback. To fan-out a stream to multiple consumers, dispatch from within your single callback. For advanced use cases, use the full `client-sdk-cpp` API directly, or expand the bridge to support your use case. diff --git a/bridge/include/livekit_bridge/livekit_bridge.h b/bridge/include/livekit_bridge/livekit_bridge.h index 1f2d44a6..c2275dd9 100644 --- a/bridge/include/livekit_bridge/livekit_bridge.h +++ b/bridge/include/livekit_bridge/livekit_bridge.h @@ -37,7 +37,6 @@ namespace livekit { class Room; class AudioFrame; class VideoFrame; -enum class TrackSource; } // namespace livekit namespace livekit_bridge { @@ -88,12 +87,10 @@ using VideoFrameCallback = livekit::VideoFrameCallback; * mic->pushFrame(pcm_data, samples_per_channel); * cam->pushFrame(rgba_data, timestamp_us); * - * bridge.setOnAudioFrameCallback("remote-participant", - * livekit::TrackSource::SOURCE_MICROPHONE, + * bridge.setOnAudioFrameCallback("remote-participant", "mic", * [](const livekit::AudioFrame& f) { process(f); }); * - * bridge.setOnVideoFrameCallback("remote-participant", - * livekit::TrackSource::SOURCE_CAMERA, + * bridge.setOnVideoFrameCallback("remote-participant", "cam", * [](const livekit::VideoFrame& f, int64_t ts) { render(f); }); * * // Unpublish a single track mid-session: @@ -207,59 +204,59 @@ class LiveKitBridge { /** * Set the callback for audio frames from a specific remote participant - * and track source. + * and track name. * * Delegates to Room::setOnAudioFrameCallback. The callback fires on a * dedicated reader thread owned by Room whenever a new audio frame is * received. * - * @note Only **one** callback may be registered per (participant, source) - * pair. Calling this again with the same identity and source will + * @note Only **one** callback may be registered per (participant, track_name) + * pair. Calling this again with the same identity and track_name will * silently replace the previous callback. * * @param participant_identity Identity of the remote participant. - * @param source Track source (e.g. SOURCE_MICROPHONE). + * @param track_name Name of the remote track. * @param callback Function to invoke per audio frame. */ void setOnAudioFrameCallback(const std::string &participant_identity, - livekit::TrackSource source, + const std::string &track_name, AudioFrameCallback callback); /** * Register a callback for video frames from a specific remote participant - * and track source. + * and track name. * * Delegates to Room::setOnVideoFrameCallback. * - * @note Only **one** callback may be registered per (participant, source) - * pair. Calling this again with the same identity and source will + * @note Only **one** callback may be registered per (participant, track_name) + * pair. Calling this again with the same identity and track_name will * silently replace the previous callback. * * @param participant_identity Identity of the remote participant. - * @param source Track source (e.g. SOURCE_CAMERA). + * @param track_name Name of the remote track. * @param callback Function to invoke per video frame. */ void setOnVideoFrameCallback(const std::string &participant_identity, - livekit::TrackSource source, + const std::string &track_name, VideoFrameCallback callback); /** * Clear the audio frame callback for a specific remote participant + track - * source. + * name. * * Delegates to Room::clearOnAudioFrameCallback. */ void clearOnAudioFrameCallback(const std::string &participant_identity, - livekit::TrackSource source); + const std::string &track_name); /** * Clear the video frame callback for a specific remote participant + track - * source. + * name. * * Delegates to Room::clearOnVideoFrameCallback. */ void clearOnVideoFrameCallback(const std::string &participant_identity, - livekit::TrackSource source); + const std::string &track_name); // --------------------------------------------------------------- // RPC (Remote Procedure Call) diff --git a/bridge/src/livekit_bridge.cpp b/bridge/src/livekit_bridge.cpp index b15587ec..8cf661c9 100644 --- a/bridge/src/livekit_bridge.cpp +++ b/bridge/src/livekit_bridge.cpp @@ -238,7 +238,7 @@ LiveKitBridge::createVideoTrack(const std::string &name, int width, int height, // --------------------------------------------------------------- void LiveKitBridge::setOnAudioFrameCallback( - const std::string &participant_identity, livekit::TrackSource source, + const std::string &participant_identity, const std::string &track_name, AudioFrameCallback callback) { std::lock_guard lock(mutex_); if (!room_) { @@ -246,12 +246,12 @@ void LiveKitBridge::setOnAudioFrameCallback( "ignored\n"; return; } - room_->setOnAudioFrameCallback(participant_identity, source, + room_->setOnAudioFrameCallback(participant_identity, track_name, std::move(callback)); } void LiveKitBridge::setOnVideoFrameCallback( - const std::string &participant_identity, livekit::TrackSource source, + const std::string &participant_identity, const std::string &track_name, VideoFrameCallback callback) { std::lock_guard lock(mutex_); if (!room_) { @@ -259,26 +259,26 @@ void LiveKitBridge::setOnVideoFrameCallback( "ignored\n"; return; } - room_->setOnVideoFrameCallback(participant_identity, source, + room_->setOnVideoFrameCallback(participant_identity, track_name, std::move(callback)); } void LiveKitBridge::clearOnAudioFrameCallback( - const std::string &participant_identity, livekit::TrackSource source) { + const std::string &participant_identity, const std::string &track_name) { std::lock_guard lock(mutex_); if (!room_) { return; } - room_->clearOnAudioFrameCallback(participant_identity, source); + room_->clearOnAudioFrameCallback(participant_identity, track_name); } void LiveKitBridge::clearOnVideoFrameCallback( - const std::string &participant_identity, livekit::TrackSource source) { + const std::string &participant_identity, const std::string &track_name) { std::lock_guard lock(mutex_); if (!room_) { return; } - room_->clearOnVideoFrameCallback(participant_identity, source); + room_->clearOnVideoFrameCallback(participant_identity, track_name); } // --------------------------------------------------------------- diff --git a/bridge/tests/test_livekit_bridge.cpp b/bridge/tests/test_livekit_bridge.cpp index 43c8f6fb..e75690bc 100644 --- a/bridge/tests/test_livekit_bridge.cpp +++ b/bridge/tests/test_livekit_bridge.cpp @@ -101,12 +101,10 @@ TEST_F(LiveKitBridgeTest, SetAndClearAudioCallbackBeforeConnectDoesNotCrash) { LiveKitBridge bridge; EXPECT_NO_THROW({ - bridge.setOnAudioFrameCallback("remote-participant", - livekit::TrackSource::SOURCE_MICROPHONE, + bridge.setOnAudioFrameCallback("remote-participant", "mic", [](const livekit::AudioFrame &) {}); - bridge.clearOnAudioFrameCallback("remote-participant", - livekit::TrackSource::SOURCE_MICROPHONE); + bridge.clearOnAudioFrameCallback("remote-participant", "mic"); }) << "set/clear audio callback before connect should be safe (warns)"; } @@ -115,11 +113,10 @@ TEST_F(LiveKitBridgeTest, SetAndClearVideoCallbackBeforeConnectDoesNotCrash) { EXPECT_NO_THROW({ bridge.setOnVideoFrameCallback( - "remote-participant", livekit::TrackSource::SOURCE_CAMERA, + "remote-participant", "cam", [](const livekit::VideoFrame &, std::int64_t) {}); - bridge.clearOnVideoFrameCallback("remote-participant", - livekit::TrackSource::SOURCE_CAMERA); + bridge.clearOnVideoFrameCallback("remote-participant", "cam"); }) << "set/clear video callback before connect should be safe (warns)"; } @@ -127,10 +124,8 @@ TEST_F(LiveKitBridgeTest, ClearNonExistentCallbackIsNoOp) { LiveKitBridge bridge; EXPECT_NO_THROW({ - bridge.clearOnAudioFrameCallback("nonexistent", - livekit::TrackSource::SOURCE_MICROPHONE); - bridge.clearOnVideoFrameCallback("nonexistent", - livekit::TrackSource::SOURCE_CAMERA); + bridge.clearOnAudioFrameCallback("nonexistent", "mic"); + bridge.clearOnVideoFrameCallback("nonexistent", "cam"); }) << "Clearing a callback that was never registered should be a no-op"; } diff --git a/include/livekit/room.h b/include/livekit/room.h index e65d7d1f..841ec121 100644 --- a/include/livekit/room.h +++ b/include/livekit/room.h @@ -240,13 +240,6 @@ class Room { // Frame callbacks // --------------------------------------------------------------- - /** - * @brief Sets the audio frame callback via SubscriptionThreadDispatcher. - */ - void setOnAudioFrameCallback(const std::string &participant_identity, - TrackSource source, AudioFrameCallback callback, - const AudioStream::Options &opts = {}); - /** * @brief Sets the audio frame callback via SubscriptionThreadDispatcher. */ @@ -255,13 +248,6 @@ class Room { AudioFrameCallback callback, const AudioStream::Options &opts = {}); - /** - * @brief Sets the video frame callback via SubscriptionThreadDispatcher. - */ - void setOnVideoFrameCallback(const std::string &participant_identity, - TrackSource source, VideoFrameCallback callback, - const VideoStream::Options &opts = {}); - /** * @brief Sets the video frame callback via SubscriptionThreadDispatcher. */ @@ -279,23 +265,12 @@ class Room { VideoFrameEventCallback callback, const VideoStream::Options &opts = {}); - /** - * @brief Clears the audio frame callback via SubscriptionThreadDispatcher. - */ - void clearOnAudioFrameCallback(const std::string &participant_identity, - TrackSource source); /** * @brief Clears the audio frame callback via SubscriptionThreadDispatcher. */ void clearOnAudioFrameCallback(const std::string &participant_identity, const std::string &track_name); - /** - * @brief Clears the video frame callback via SubscriptionThreadDispatcher. - */ - void clearOnVideoFrameCallback(const std::string &participant_identity, - TrackSource source); - /** * @brief Clears the video frame callback via SubscriptionThreadDispatcher. */ diff --git a/include/livekit/subscription_thread_dispatcher.h b/include/livekit/subscription_thread_dispatcher.h index c647c565..d6d61d33 100644 --- a/include/livekit/subscription_thread_dispatcher.h +++ b/include/livekit/subscription_thread_dispatcher.h @@ -71,7 +71,7 @@ using DataFrameCallbackId = std::uint64_t; * registration requests here, and then calls \ref handleTrackSubscribed and * \ref handleTrackUnsubscribed as room events arrive. * - * For each registered `(participant identity, TrackSource)` pair, this class + * For each registered `(participant identity, track name)` pair, this class * may create a dedicated \ref AudioStream or \ref VideoStream and a matching * reader thread. That thread blocks on stream reads and invokes the * registered callback with decoded frames. @@ -93,23 +93,6 @@ class SubscriptionThreadDispatcher { /// Stops all active readers and clears all registered callbacks. ~SubscriptionThreadDispatcher(); - /** - * Register or replace an audio frame callback for a remote subscription. - * - * The callback is keyed by remote participant identity plus \p source. - * If the matching remote audio track is already subscribed, \ref Room may - * immediately call \ref handleTrackSubscribed to start a reader. - * - * @param participant_identity Identity of the remote participant. - * @param source Track source to match. - * @param callback Function invoked for each decoded audio frame. - * @param opts Options used when creating the backing - * \ref AudioStream. - */ - void setOnAudioFrameCallback(const std::string &participant_identity, - TrackSource source, AudioFrameCallback callback, - const AudioStream::Options &opts = {}); - /** * Register or replace an audio frame callback for a remote subscription. * @@ -128,23 +111,6 @@ class SubscriptionThreadDispatcher { AudioFrameCallback callback, const AudioStream::Options &opts = {}); - /** - * Register or replace a video frame callback for a remote subscription. - * - * The callback is keyed by remote participant identity plus \p source. - * If the matching remote video track is already subscribed, \ref Room may - * immediately call \ref handleTrackSubscribed to start a reader. - * - * @param participant_identity Identity of the remote participant. - * @param source Track source to match. - * @param callback Function invoked for each decoded video frame. - * @param opts Options used when creating the backing - * \ref VideoStream. - */ - void setOnVideoFrameCallback(const std::string &participant_identity, - TrackSource source, VideoFrameCallback callback, - const VideoStream::Options &opts = {}); - /** * Register or replace a video frame callback for a remote subscription. * @@ -183,18 +149,6 @@ class SubscriptionThreadDispatcher { VideoFrameEventCallback callback, const VideoStream::Options &opts = {}); - /** - * Remove an audio callback registration and stop any active reader. - * - * If an audio reader thread is active for the given key, its stream is - * closed and the thread is joined before this call returns. - * - * @param participant_identity Identity of the remote participant. - * @param source Track source to clear. - */ - void clearOnAudioFrameCallback(const std::string &participant_identity, - TrackSource source); - /** * Remove an audio callback registration and stop any active reader. * @@ -207,18 +161,6 @@ class SubscriptionThreadDispatcher { void clearOnAudioFrameCallback(const std::string &participant_identity, const std::string &track_name); - /** - * Remove a video callback registration and stop any active reader. - * - * If a video reader thread is active for the given key, its stream is - * closed and the thread is joined before this call returns. - * - * @param participant_identity Identity of the remote participant. - * @param source Track source to clear. - */ - void clearOnVideoFrameCallback(const std::string &participant_identity, - TrackSource source); - /** * Remove a video callback registration and stop any active reader. * @@ -237,32 +179,30 @@ class SubscriptionThreadDispatcher { * \ref Room calls this after it has processed a track-subscription event and * updated its publication state. If a matching callback registration exists, * the dispatcher creates the appropriate stream type and launches a reader - * thread for the `(participant, source)` key. + * thread for the `(participant, track_name)` key. * * If no matching callback is registered, this is a no-op. * * @param participant_identity Identity of the remote participant. - * @param source Track source associated with the subscription. + * @param track_name Track name associated with the subscription. * @param track Subscribed remote track to read from. */ void handleTrackSubscribed(const std::string &participant_identity, - TrackSource source, const std::string &track_name, + const std::string &track_name, const std::shared_ptr &track); /** * Stop reader dispatch for an unsubscribed remote track. * * \ref Room calls this when a remote track is unsubscribed. Any active - * reader stream for the given `(participant, source)` key is closed and its - * thread is joined. Callback registration is preserved so future + * reader stream for the given `(participant, track_name)` key is closed and + * its thread is joined. Callback registration is preserved so future * re-subscription can start dispatch again automatically. * * @param participant_identity Identity of the remote participant. - * @param source Track source associated with the subscription. * @param track_name Track name associated with the subscription. */ void handleTrackUnsubscribed(const std::string &participant_identity, - TrackSource source, const std::string &track_name); // --------------------------------------------------------------- @@ -334,16 +274,14 @@ class SubscriptionThreadDispatcher { friend class SubscriptionThreadDispatcherTest; /// Compound lookup key for callback dispatch: - /// either `(participant, source, "")` or `(participant, SOURCE_UNKNOWN, - /// track_name)`. + /// `(participant_identity, track_name)`. struct CallbackKey { std::string participant_identity; - TrackSource source; std::string track_name; bool operator==(const CallbackKey &o) const { return participant_identity == o.participant_identity && - source == o.source && track_name == o.track_name; + track_name == o.track_name; } }; @@ -351,9 +289,8 @@ class SubscriptionThreadDispatcher { struct CallbackKeyHash { std::size_t operator()(const CallbackKey &k) const { auto h1 = std::hash{}(k.participant_identity); - auto h2 = std::hash{}(static_cast(k.source)); - auto h3 = std::hash{}(k.track_name); - return h1 ^ (h2 << 1) ^ (h3 << 2); + auto h2 = std::hash{}(k.track_name); + return h1 ^ (h2 << 1); } }; diff --git a/include/livekit/video_source.h b/include/livekit/video_source.h index c015a437..13589c14 100644 --- a/include/livekit/video_source.h +++ b/include/livekit/video_source.h @@ -54,7 +54,7 @@ struct VideoFrameMetadata { struct VideoCaptureOptions { std::int64_t timestamp_us = 0; VideoRotation rotation = VideoRotation::VIDEO_ROTATION_0; - // Populate meta data when you want to send user timestamps or frame IDs. + // Populate metadata when you want to send user timestamps or frame IDs. std::optional metadata; }; diff --git a/src/room.cpp b/src/room.cpp index ae1af703..93732144 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -274,16 +274,6 @@ void Room::unregisterByteStreamHandler(const std::string &topic) { // Frame callback registration // ------------------------------------------------------------------- -void Room::setOnAudioFrameCallback(const std::string &participant_identity, - TrackSource source, - AudioFrameCallback callback, - const AudioStream::Options &opts) { - if (subscription_thread_dispatcher_) { - subscription_thread_dispatcher_->setOnAudioFrameCallback( - participant_identity, source, std::move(callback), opts); - } -} - void Room::setOnAudioFrameCallback(const std::string &participant_identity, const std::string &track_name, AudioFrameCallback callback, @@ -294,16 +284,6 @@ void Room::setOnAudioFrameCallback(const std::string &participant_identity, } } -void Room::setOnVideoFrameCallback(const std::string &participant_identity, - TrackSource source, - VideoFrameCallback callback, - const VideoStream::Options &opts) { - if (subscription_thread_dispatcher_) { - subscription_thread_dispatcher_->setOnVideoFrameCallback( - participant_identity, source, std::move(callback), opts); - } -} - void Room::setOnVideoFrameCallback(const std::string &participant_identity, const std::string &track_name, VideoFrameCallback callback, @@ -324,14 +304,6 @@ void Room::setOnVideoFrameEventCallback(const std::string &participant_identity, } } -void Room::clearOnAudioFrameCallback(const std::string &participant_identity, - TrackSource source) { - if (subscription_thread_dispatcher_) { - subscription_thread_dispatcher_->clearOnAudioFrameCallback( - participant_identity, source); - } -} - void Room::clearOnAudioFrameCallback(const std::string &participant_identity, const std::string &track_name) { if (subscription_thread_dispatcher_) { @@ -340,14 +312,6 @@ void Room::clearOnAudioFrameCallback(const std::string &participant_identity, } } -void Room::clearOnVideoFrameCallback(const std::string &participant_identity, - TrackSource source) { - if (subscription_thread_dispatcher_) { - subscription_thread_dispatcher_->clearOnVideoFrameCallback( - participant_identity, source); - } -} - void Room::clearOnVideoFrameCallback(const std::string &participant_identity, const std::string &track_name) { if (subscription_thread_dispatcher_) { @@ -660,15 +624,14 @@ void Room::OnEvent(const FfiEvent &event) { if (subscription_thread_dispatcher_ && remote_track && rpublication) { subscription_thread_dispatcher_->handleTrackSubscribed( - identity, rpublication->source(), rpublication->name(), - remote_track); + identity, rpublication->name(), remote_track); } break; } case proto::RoomEvent::kTrackUnsubscribed: { TrackUnsubscribedEvent ev; - TrackSource unsub_source = TrackSource::SOURCE_UNKNOWN; std::string unsub_identity; + std::string unsub_track_name; { std::lock_guard guard(lock_); const auto &tu = re.track_unsubscribed(); @@ -690,7 +653,7 @@ void Room::OnEvent(const FfiEvent &event) { break; } auto publication = pubIt->second; - unsub_source = publication->source(); + unsub_track_name = publication->name(); auto track = publication->track(); publication->setTrack(nullptr); publication->setSubscribed(false); @@ -703,11 +666,9 @@ void Room::OnEvent(const FfiEvent &event) { delegate_snapshot->onTrackUnsubscribed(*this, ev); } - if (subscription_thread_dispatcher_ && - unsub_source != TrackSource::SOURCE_UNKNOWN) { + if (subscription_thread_dispatcher_) { subscription_thread_dispatcher_->handleTrackUnsubscribed( - unsub_identity, unsub_source, - ev.publication ? ev.publication->name() : ""); + unsub_identity, unsub_track_name); } break; } diff --git a/src/subscription_thread_dispatcher.cpp b/src/subscription_thread_dispatcher.cpp index 0c5fe32e..a0a2d2c1 100644 --- a/src/subscription_thread_dispatcher.cpp +++ b/src/subscription_thread_dispatcher.cpp @@ -51,24 +51,10 @@ SubscriptionThreadDispatcher::~SubscriptionThreadDispatcher() { stopAll(); } -void SubscriptionThreadDispatcher::setOnAudioFrameCallback( - const std::string &participant_identity, TrackSource source, - AudioFrameCallback callback, const AudioStream::Options &opts) { - const CallbackKey key{participant_identity, source, ""}; - const std::lock_guard lock(lock_); - const bool replacing = audio_callbacks_.find(key) != audio_callbacks_.end(); - audio_callbacks_[key] = RegisteredAudioCallback{std::move(callback), opts}; - LK_LOG_DEBUG("Registered audio frame callback for participant={} source={} " - "replacing_existing={} total_audio_callbacks={}", - participant_identity, static_cast(source), replacing, - audio_callbacks_.size()); -} - void SubscriptionThreadDispatcher::setOnAudioFrameCallback( const std::string &participant_identity, const std::string &track_name, AudioFrameCallback callback, const AudioStream::Options &opts) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, - track_name}; + const CallbackKey key{participant_identity, track_name}; const std::lock_guard lock(lock_); const bool replacing = audio_callbacks_.find(key) != audio_callbacks_.end(); audio_callbacks_[key] = RegisteredAudioCallback{std::move(callback), opts}; @@ -78,28 +64,10 @@ void SubscriptionThreadDispatcher::setOnAudioFrameCallback( participant_identity, track_name, replacing, audio_callbacks_.size()); } -void SubscriptionThreadDispatcher::setOnVideoFrameCallback( - const std::string &participant_identity, TrackSource source, - VideoFrameCallback callback, const VideoStream::Options &opts) { - const CallbackKey key{participant_identity, source, ""}; - const std::lock_guard lock(lock_); - const bool replacing = video_callbacks_.find(key) != video_callbacks_.end(); - video_callbacks_[key] = RegisteredVideoCallback{ - std::move(callback), - VideoFrameEventCallback{}, - opts, - }; - LK_LOG_DEBUG("Registered legacy video frame callback for participant={} " - "source={} replacing_existing={} total_video_callbacks={}", - participant_identity, static_cast(source), replacing, - video_callbacks_.size()); -} - void SubscriptionThreadDispatcher::setOnVideoFrameEventCallback( const std::string &participant_identity, const std::string &track_name, VideoFrameEventCallback callback, const VideoStream::Options &opts) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, - track_name}; + const CallbackKey key{participant_identity, track_name}; const std::lock_guard lock(lock_); const bool replacing = video_callbacks_.find(key) != video_callbacks_.end(); video_callbacks_[key] = RegisteredVideoCallback{ @@ -116,8 +84,7 @@ void SubscriptionThreadDispatcher::setOnVideoFrameEventCallback( void SubscriptionThreadDispatcher::setOnVideoFrameCallback( const std::string &participant_identity, const std::string &track_name, VideoFrameCallback callback, const VideoStream::Options &opts) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, - track_name}; + const CallbackKey key{participant_identity, track_name}; const std::lock_guard lock(lock_); const bool replacing = video_callbacks_.find(key) != video_callbacks_.end(); video_callbacks_[key] = RegisteredVideoCallback{ @@ -131,30 +98,9 @@ void SubscriptionThreadDispatcher::setOnVideoFrameCallback( participant_identity, track_name, replacing, video_callbacks_.size()); } -void SubscriptionThreadDispatcher::clearOnAudioFrameCallback( - const std::string &participant_identity, TrackSource source) { - const CallbackKey key{participant_identity, source, ""}; - std::thread old_thread; - bool removed_callback = false; - { - const std::lock_guard lock(lock_); - removed_callback = audio_callbacks_.erase(key) > 0; - old_thread = extractReaderThreadLocked(key); - LK_LOG_DEBUG( - "Clearing audio frame callback for participant={} source={} " - "removed_callback={} stopped_reader={} remaining_audio_callbacks={}", - participant_identity, static_cast(source), removed_callback, - old_thread.joinable(), audio_callbacks_.size()); - } - if (old_thread.joinable()) { - old_thread.join(); - } -} - void SubscriptionThreadDispatcher::clearOnAudioFrameCallback( const std::string &participant_identity, const std::string &track_name) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, - track_name}; + const CallbackKey key{participant_identity, track_name}; std::thread old_thread; bool removed_callback = false; { @@ -172,30 +118,9 @@ void SubscriptionThreadDispatcher::clearOnAudioFrameCallback( } } -void SubscriptionThreadDispatcher::clearOnVideoFrameCallback( - const std::string &participant_identity, TrackSource source) { - const CallbackKey key{participant_identity, source, ""}; - std::thread old_thread; - bool removed_callback = false; - { - const std::lock_guard lock(lock_); - removed_callback = video_callbacks_.erase(key) > 0; - old_thread = extractReaderThreadLocked(key); - LK_LOG_DEBUG( - "Clearing video frame callback for participant={} source={} " - "removed_callback={} stopped_reader={} remaining_video_callbacks={}", - participant_identity, static_cast(source), removed_callback, - old_thread.joinable(), video_callbacks_.size()); - } - if (old_thread.joinable()) { - old_thread.join(); - } -} - void SubscriptionThreadDispatcher::clearOnVideoFrameCallback( const std::string &participant_identity, const std::string &track_name) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, - track_name}; + const CallbackKey key{participant_identity, track_name}; std::thread old_thread; bool removed_callback = false; { @@ -214,32 +139,23 @@ void SubscriptionThreadDispatcher::clearOnVideoFrameCallback( } void SubscriptionThreadDispatcher::handleTrackSubscribed( - const std::string &participant_identity, TrackSource source, - const std::string &track_name, const std::shared_ptr &track) { + const std::string &participant_identity, const std::string &track_name, + const std::shared_ptr &track) { if (!track) { - LK_LOG_WARN( - "Ignoring subscribed track dispatch for participant={} source={} " - "because track is null", - participant_identity, static_cast(source)); + LK_LOG_WARN("Ignoring subscribed track dispatch for participant={} " + "track_name={} because track is null", + participant_identity, track_name); return; } - LK_LOG_DEBUG("Handling subscribed track for participant={} source={} kind={}", - participant_identity, static_cast(source), - trackKindName(track->kind())); + LK_LOG_DEBUG( + "Handling subscribed track for participant={} track_name={} kind={}", + participant_identity, track_name, trackKindName(track->kind())); - CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, - track_name}; - const CallbackKey fallback_key{participant_identity, source, ""}; + const CallbackKey key{participant_identity, track_name}; std::thread old_thread; { const std::lock_guard lock(lock_); - if ((track->kind() == TrackKind::KIND_AUDIO && - audio_callbacks_.find(key) == audio_callbacks_.end()) || - (track->kind() == TrackKind::KIND_VIDEO && - video_callbacks_.find(key) == video_callbacks_.end())) { - key = fallback_key; - } old_thread = startReaderLocked(key, track); } if (old_thread.joinable()) { @@ -248,28 +164,19 @@ void SubscriptionThreadDispatcher::handleTrackSubscribed( } void SubscriptionThreadDispatcher::handleTrackUnsubscribed( - const std::string &participant_identity, TrackSource source, - const std::string &track_name) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, - track_name}; - const CallbackKey fallback_key{participant_identity, source, ""}; + const std::string &participant_identity, const std::string &track_name) { + const CallbackKey key{participant_identity, track_name}; std::thread old_thread; - std::thread fallback_old_thread; { const std::lock_guard lock(lock_); old_thread = extractReaderThreadLocked(key); - fallback_old_thread = extractReaderThreadLocked(fallback_key); - LK_LOG_DEBUG("Handling unsubscribed track for participant={} source={} " - "track_name={} stopped_reader={} fallback_stopped_reader={}", - participant_identity, static_cast(source), track_name, - old_thread.joinable(), fallback_old_thread.joinable()); + LK_LOG_DEBUG("Handling unsubscribed track for participant={} " + "track_name={} stopped_reader={}", + participant_identity, track_name, old_thread.joinable()); } if (old_thread.joinable()) { old_thread.join(); } - if (fallback_old_thread.joinable()) { - fallback_old_thread.join(); - } } // ------------------------------------------------------------------- @@ -432,17 +339,14 @@ std::thread SubscriptionThreadDispatcher::extractReaderThreadLocked( const CallbackKey &key) { auto it = active_readers_.find(key); if (it == active_readers_.end()) { - LK_LOG_TRACE("No active reader to extract for participant={} source={} " - "track_name={}", - key.participant_identity, static_cast(key.source), - key.track_name); + LK_LOG_TRACE( + "No active reader to extract for participant={} track_name={}", + key.participant_identity, key.track_name); return {}; } - LK_LOG_DEBUG("Extracting active reader for participant={} source={} " - "track_name={}", - key.participant_identity, static_cast(key.source), - key.track_name); + LK_LOG_DEBUG("Extracting active reader for participant={} track_name={}", + key.participant_identity, key.track_name); ActiveReader reader = std::move(it->second); active_readers_.erase(it); @@ -460,9 +364,9 @@ std::thread SubscriptionThreadDispatcher::startReaderLocked( if (track->kind() == TrackKind::KIND_AUDIO) { auto it = audio_callbacks_.find(key); if (it == audio_callbacks_.end()) { - LK_LOG_TRACE("Skipping audio reader start for participant={} source={} " - "because no audio callback is registered", - key.participant_identity, static_cast(key.source)); + LK_LOG_TRACE("Skipping audio reader start for participant={} " + "track_name={} because no audio callback is registered", + key.participant_identity, key.track_name); return {}; } return startAudioReaderLocked(key, track, it->second.callback, @@ -471,75 +375,74 @@ std::thread SubscriptionThreadDispatcher::startReaderLocked( if (track->kind() == TrackKind::KIND_VIDEO) { auto it = video_callbacks_.find(key); if (it == video_callbacks_.end()) { - LK_LOG_TRACE("Skipping video reader start for participant={} source={} " - "because no video callback is registered", - key.participant_identity, static_cast(key.source)); + LK_LOG_TRACE("Skipping video reader start for participant={} " + "track_name={} because no video callback is registered", + key.participant_identity, key.track_name); return {}; } return startVideoReaderLocked(key, track, it->second); } if (track->kind() == TrackKind::KIND_UNKNOWN) { - LK_LOG_WARN( - "Skipping reader start for participant={} source={} because track " - "kind is unknown", - key.participant_identity, static_cast(key.source)); + LK_LOG_WARN("Skipping reader start for participant={} track_name={} " + "because track kind is unknown", + key.participant_identity, key.track_name); return {}; } - LK_LOG_WARN( - "Skipping reader start for participant={} source={} because track kind " - "is unsupported", - key.participant_identity, static_cast(key.source)); + LK_LOG_WARN("Skipping reader start for participant={} track_name={} " + "because track kind is unsupported", + key.participant_identity, key.track_name); return {}; } std::thread SubscriptionThreadDispatcher::startAudioReaderLocked( const CallbackKey &key, const std::shared_ptr &track, const AudioFrameCallback &cb, const AudioStream::Options &opts) { - LK_LOG_DEBUG("Starting audio reader for participant={} source={}", - key.participant_identity, static_cast(key.source)); + LK_LOG_DEBUG("Starting audio reader for participant={} track_name={}", + key.participant_identity, key.track_name); auto old_thread = extractReaderThreadLocked(key); if (static_cast(active_readers_.size()) >= kMaxActiveReaders) { - LK_LOG_ERROR( - "Cannot start audio reader for {} source={}: active reader limit ({}) " - "reached", - key.participant_identity, static_cast(key.source), - kMaxActiveReaders); + LK_LOG_ERROR("Cannot start audio reader for {} track_name={}: active " + "reader limit ({}) reached", + key.participant_identity, key.track_name, kMaxActiveReaders); return old_thread; } const auto stream = AudioStream::fromTrack(track, opts); if (!stream) { - LK_LOG_ERROR("Failed to create AudioStream for {} source={}", - key.participant_identity, static_cast(key.source)); + LK_LOG_ERROR("Failed to create AudioStream for {} track_name={}", + key.participant_identity, key.track_name); return old_thread; } ActiveReader reader; reader.audio_stream = stream; const std::string participant_identity = key.participant_identity; - const TrackSource source = key.source; + const std::string track_name = key.track_name; // NOLINTBEGIN(bugprone-lambda-function-name) - reader.thread = std::thread([stream, cb, participant_identity, source]() { - LK_LOG_DEBUG("Audio reader thread started for participant={} source={}", - participant_identity, static_cast(source)); - AudioFrameEvent ev; - while (stream->read(ev)) { - try { - cb(ev.frame); - } catch (const std::exception &e) { - LK_LOG_ERROR("Audio frame callback exception: {}", e.what()); - } - } - LK_LOG_DEBUG("Audio reader thread exiting for participant={} source={}", - participant_identity, static_cast(source)); - }); + reader.thread = + std::thread([stream, cb, participant_identity, track_name]() { + LK_LOG_DEBUG( + "Audio reader thread started for participant={} track_name={}", + participant_identity, track_name); + AudioFrameEvent ev; + while (stream->read(ev)) { + try { + cb(ev.frame); + } catch (const std::exception &e) { + LK_LOG_ERROR("Audio frame callback exception: {}", e.what()); + } + } + LK_LOG_DEBUG( + "Audio reader thread exiting for participant={} track_name={}", + participant_identity, track_name); + }); // NOLINTEND(bugprone-lambda-function-name) active_readers_[key] = std::move(reader); - LK_LOG_DEBUG("Started audio reader for participant={} source={} " + LK_LOG_DEBUG("Started audio reader for participant={} track_name={} " "active_readers={}", - key.participant_identity, static_cast(key.source), + key.participant_identity, key.track_name, active_readers_.size()); return old_thread; } @@ -547,23 +450,21 @@ std::thread SubscriptionThreadDispatcher::startAudioReaderLocked( std::thread SubscriptionThreadDispatcher::startVideoReaderLocked( const CallbackKey &key, const std::shared_ptr &track, const RegisteredVideoCallback &callback) { - LK_LOG_DEBUG("Starting video reader for participant={} source={}", - key.participant_identity, static_cast(key.source)); + LK_LOG_DEBUG("Starting video reader for participant={} track_name={}", + key.participant_identity, key.track_name); auto old_thread = extractReaderThreadLocked(key); if (static_cast(active_readers_.size()) >= kMaxActiveReaders) { - LK_LOG_ERROR( - "Cannot start video reader for {} source={}: active reader limit ({}) " - "reached", - key.participant_identity, static_cast(key.source), - kMaxActiveReaders); + LK_LOG_ERROR("Cannot start video reader for {} track_name={}: active " + "reader limit ({}) reached", + key.participant_identity, key.track_name, kMaxActiveReaders); return old_thread; } auto stream = VideoStream::fromTrack(track, callback.options); if (!stream) { - LK_LOG_ERROR("Failed to create VideoStream for {} source={}", - key.participant_identity, static_cast(key.source)); + LK_LOG_ERROR("Failed to create VideoStream for {} track_name={}", + key.participant_identity, key.track_name); return old_thread; } @@ -572,12 +473,13 @@ std::thread SubscriptionThreadDispatcher::startVideoReaderLocked( auto legacy_cb = callback.legacy_callback; auto event_cb = callback.event_callback; const std::string participant_identity = key.participant_identity; - const TrackSource source = key.source; + const std::string track_name = key.track_name; // NOLINTBEGIN(bugprone-lambda-function-name) reader.thread = std::thread([stream = std::move(stream), legacy_cb, event_cb, - participant_identity, source]() { - LK_LOG_DEBUG("Video reader thread started for participant={} source={}", - participant_identity, static_cast(source)); + participant_identity, track_name]() { + LK_LOG_DEBUG( + "Video reader thread started for participant={} track_name={}", + participant_identity, track_name); VideoFrameEvent ev; while (stream->read(ev)) { try { @@ -590,14 +492,15 @@ std::thread SubscriptionThreadDispatcher::startVideoReaderLocked( LK_LOG_ERROR("Video frame callback exception: {}", e.what()); } } - LK_LOG_DEBUG("Video reader thread exiting for participant={} source={}", - participant_identity, static_cast(source)); + LK_LOG_DEBUG( + "Video reader thread exiting for participant={} track_name={}", + participant_identity, track_name); }); // NOLINTEND(bugprone-lambda-function-name) active_readers_[key] = std::move(reader); - LK_LOG_DEBUG("Started video reader for participant={} source={} " + LK_LOG_DEBUG("Started video reader for participant={} track_name={} " "active_readers={}", - key.participant_identity, static_cast(key.source), + key.participant_identity, key.track_name, active_readers_.size()); return old_thread; } diff --git a/src/tests/unit/test_room_callbacks.cpp b/src/tests/unit/test_room_callbacks.cpp index 90ac35b4..b447ea58 100644 --- a/src/tests/unit/test_room_callbacks.cpp +++ b/src/tests/unit/test_room_callbacks.cpp @@ -36,21 +36,6 @@ class RoomCallbackTest : public ::testing::Test { void TearDown() override { livekit::shutdown(); } }; -TEST_F(RoomCallbackTest, AudioCallbackRegistrationIsAccepted) { - Room room; - - EXPECT_NO_THROW(room.setOnAudioFrameCallback( - "alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame &) {})); -} - -TEST_F(RoomCallbackTest, VideoCallbackRegistrationIsAccepted) { - Room room; - - EXPECT_NO_THROW( - room.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, - [](const VideoFrame &, std::int64_t) {})); -} - TEST_F(RoomCallbackTest, AudioCallbackRegistrationByTrackNameIsAccepted) { Room room; @@ -68,10 +53,6 @@ TEST_F(RoomCallbackTest, VideoCallbackRegistrationByTrackNameIsAccepted) { TEST_F(RoomCallbackTest, ClearingMissingCallbacksIsNoOp) { Room room; - EXPECT_NO_THROW( - room.clearOnAudioFrameCallback("nobody", TrackSource::SOURCE_MICROPHONE)); - EXPECT_NO_THROW( - room.clearOnVideoFrameCallback("nobody", TrackSource::SOURCE_CAMERA)); EXPECT_NO_THROW(room.clearOnAudioFrameCallback("nobody", "missing-audio")); EXPECT_NO_THROW(room.clearOnVideoFrameCallback("nobody", "missing-video")); } @@ -82,40 +63,36 @@ TEST_F(RoomCallbackTest, ReRegisteringSameAudioKeyDoesNotThrow) { std::atomic counter2{0}; EXPECT_NO_THROW(room.setOnAudioFrameCallback( - "alice", TrackSource::SOURCE_MICROPHONE, + "alice", "mic-main", [&counter1](const AudioFrame &) { counter1++; })); EXPECT_NO_THROW(room.setOnAudioFrameCallback( - "alice", TrackSource::SOURCE_MICROPHONE, + "alice", "mic-main", [&counter2](const AudioFrame &) { counter2++; })); } TEST_F(RoomCallbackTest, ReRegisteringSameVideoKeyDoesNotThrow) { Room room; - EXPECT_NO_THROW( - room.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, - [](const VideoFrame &, std::int64_t) {})); - EXPECT_NO_THROW( - room.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, - [](const VideoFrame &, std::int64_t) {})); + EXPECT_NO_THROW(room.setOnVideoFrameCallback( + "alice", "cam-main", [](const VideoFrame &, std::int64_t) {})); + EXPECT_NO_THROW(room.setOnVideoFrameCallback( + "alice", "cam-main", [](const VideoFrame &, std::int64_t) {})); } TEST_F(RoomCallbackTest, DistinctAudioAndVideoCallbacksCanCoexist) { Room room; - EXPECT_NO_THROW(room.setOnAudioFrameCallback( - "alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame &) {})); - EXPECT_NO_THROW( - room.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, - [](const VideoFrame &, std::int64_t) {})); - EXPECT_NO_THROW(room.setOnAudioFrameCallback( - "bob", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame &) {})); - EXPECT_NO_THROW( - room.setOnVideoFrameCallback("bob", TrackSource::SOURCE_CAMERA, - [](const VideoFrame &, std::int64_t) {})); + EXPECT_NO_THROW(room.setOnAudioFrameCallback("alice", "mic-main", + [](const AudioFrame &) {})); + EXPECT_NO_THROW(room.setOnVideoFrameCallback( + "alice", "cam-main", [](const VideoFrame &, std::int64_t) {})); + EXPECT_NO_THROW(room.setOnAudioFrameCallback("bob", "mic-main", + [](const AudioFrame &) {})); + EXPECT_NO_THROW(room.setOnVideoFrameCallback( + "bob", "cam-main", [](const VideoFrame &, std::int64_t) {})); } -TEST_F(RoomCallbackTest, SameSourceDifferentTrackNamesAreAccepted) { +TEST_F(RoomCallbackTest, SameParticipantDifferentTrackNamesAreAccepted) { Room room; EXPECT_NO_THROW(room.setOnVideoFrameCallback( @@ -132,15 +109,6 @@ TEST_F(RoomCallbackTest, ClearingTrackNameCallbackIsAccepted) { EXPECT_NO_THROW(room.clearOnAudioFrameCallback("alice", "mic-main")); } -TEST_F(RoomCallbackTest, SourceAndTrackNameCallbacksCanCoexist) { - Room room; - - EXPECT_NO_THROW(room.setOnAudioFrameCallback( - "alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame &) {})); - EXPECT_NO_THROW(room.setOnAudioFrameCallback("alice", "mic-main", - [](const AudioFrame &) {})); -} - TEST_F(RoomCallbackTest, DataCallbackRegistrationReturnsUsableIds) { Room room; @@ -169,9 +137,9 @@ TEST_F(RoomCallbackTest, RemovingUnknownDataCallbackIsNoOp) { TEST_F(RoomCallbackTest, DestroyRoomWithRegisteredCallbacksIsSafe) { EXPECT_NO_THROW({ Room room; - room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, + room.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame &) {}); - room.setOnVideoFrameCallback("bob", TrackSource::SOURCE_CAMERA, + room.setOnVideoFrameCallback("bob", "cam-main", [](const VideoFrame &, std::int64_t) {}); room.addOnDataFrameCallback( "carol", "track", @@ -182,9 +150,9 @@ TEST_F(RoomCallbackTest, DestroyRoomWithRegisteredCallbacksIsSafe) { TEST_F(RoomCallbackTest, DestroyRoomAfterClearingCallbacksIsSafe) { EXPECT_NO_THROW({ Room room; - room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, + room.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame &) {}); - room.clearOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE); + room.clearOnAudioFrameCallback("alice", "mic-main"); const auto id = room.addOnDataFrameCallback( "alice", "track", @@ -205,9 +173,9 @@ TEST_F(RoomCallbackTest, ConcurrentRegistrationDoesNotCrash) { threads.emplace_back([&room, t, kIterations]() { for (int i = 0; i < kIterations; ++i) { const std::string id = "participant-" + std::to_string(t); - room.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, + room.setOnAudioFrameCallback(id, "mic-main", [](const AudioFrame &) {}); - room.clearOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE); + room.clearOnAudioFrameCallback(id, "mic-main"); } }); } @@ -231,9 +199,9 @@ TEST_F(RoomCallbackTest, ConcurrentMixedRegistrationDoesNotCrash) { threads.emplace_back([&room, t, kIterations]() { const std::string id = "p-" + std::to_string(t); for (int i = 0; i < kIterations; ++i) { - room.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, + room.setOnAudioFrameCallback(id, "mic-main", [](const AudioFrame &) {}); - room.setOnVideoFrameCallback(id, TrackSource::SOURCE_CAMERA, + room.setOnVideoFrameCallback(id, "cam-main", [](const VideoFrame &, std::int64_t) {}); const auto data_id = room.addOnDataFrameCallback(id, "track", @@ -257,13 +225,13 @@ TEST_F(RoomCallbackTest, ManyDistinctAudioCallbacksCanBeRegisteredAndCleared) { for (int i = 0; i < kCount; ++i) { EXPECT_NO_THROW(room.setOnAudioFrameCallback( - "participant-" + std::to_string(i), TrackSource::SOURCE_MICROPHONE, + "participant-" + std::to_string(i), "mic-main", [](const AudioFrame &) {})); } for (int i = 0; i < kCount; ++i) { EXPECT_NO_THROW(room.clearOnAudioFrameCallback( - "participant-" + std::to_string(i), TrackSource::SOURCE_MICROPHONE)); + "participant-" + std::to_string(i), "mic-main")); } } diff --git a/src/tests/unit/test_subscription_thread_dispatcher.cpp b/src/tests/unit/test_subscription_thread_dispatcher.cpp index bb3bdfd1..f6b59a4e 100644 --- a/src/tests/unit/test_subscription_thread_dispatcher.cpp +++ b/src/tests/unit/test_subscription_thread_dispatcher.cpp @@ -67,27 +67,21 @@ class SubscriptionThreadDispatcherTest : public ::testing::Test { // ============================================================================ TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyEqualKeysCompareEqual) { - CallbackKey a{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey b{"alice", TrackSource::SOURCE_MICROPHONE, ""}; + CallbackKey a{"alice", "mic-main"}; + CallbackKey b{"alice", "mic-main"}; EXPECT_TRUE(a == b); } TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyDifferentIdentityNotEqual) { - CallbackKey a{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey b{"bob", TrackSource::SOURCE_MICROPHONE, ""}; - EXPECT_FALSE(a == b); -} - -TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyDifferentSourceNotEqual) { - CallbackKey a{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey b{"alice", TrackSource::SOURCE_CAMERA, ""}; + CallbackKey a{"alice", "mic-main"}; + CallbackKey b{"bob", "mic-main"}; EXPECT_FALSE(a == b); } TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyDifferentTrackNameNotEqual) { - CallbackKey a{"alice", TrackSource::SOURCE_UNKNOWN, "cam-main"}; - CallbackKey b{"alice", TrackSource::SOURCE_UNKNOWN, "cam-backup"}; + CallbackKey a{"alice", "cam-main"}; + CallbackKey b{"alice", "cam-backup"}; EXPECT_FALSE(a == b); } @@ -97,8 +91,8 @@ TEST_F(SubscriptionThreadDispatcherTest, TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyHashEqualKeysProduceSameHash) { - CallbackKey a{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey b{"alice", TrackSource::SOURCE_MICROPHONE, ""}; + CallbackKey a{"alice", "mic-main"}; + CallbackKey b{"alice", "mic-main"}; CallbackKeyHash hasher; EXPECT_EQ(hasher(a), hasher(b)); } @@ -106,22 +100,20 @@ TEST_F(SubscriptionThreadDispatcherTest, TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyHashDifferentKeysLikelyDifferentHash) { CallbackKeyHash hasher; - CallbackKey mic{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey cam{"alice", TrackSource::SOURCE_CAMERA, ""}; - CallbackKey bob{"bob", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey named{"alice", TrackSource::SOURCE_UNKNOWN, "mic-main"}; + CallbackKey mic{"alice", "mic-main"}; + CallbackKey cam{"alice", "cam-main"}; + CallbackKey bob{"bob", "mic-main"}; EXPECT_NE(hasher(mic), hasher(cam)); EXPECT_NE(hasher(mic), hasher(bob)); - EXPECT_NE(hasher(mic), hasher(named)); } TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyWorksAsUnorderedMapKey) { std::unordered_map map; - CallbackKey k1{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey k2{"bob", TrackSource::SOURCE_CAMERA, ""}; - CallbackKey k3{"alice", TrackSource::SOURCE_CAMERA, ""}; + CallbackKey k1{"alice", "mic-main"}; + CallbackKey k2{"bob", "cam-main"}; + CallbackKey k3{"alice", "cam-main"}; map[k1] = 1; map[k2] = 2; @@ -142,8 +134,8 @@ TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyWorksAsUnorderedMapKey) { } TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyEmptyIdentityWorks) { - CallbackKey a{"", TrackSource::SOURCE_UNKNOWN, ""}; - CallbackKey b{"", TrackSource::SOURCE_UNKNOWN, ""}; + CallbackKey a{"", ""}; + CallbackKey b{"", ""}; CallbackKeyHash hasher; EXPECT_TRUE(a == b); EXPECT_EQ(hasher(a), hasher(b)); @@ -161,14 +153,6 @@ TEST_F(SubscriptionThreadDispatcherTest, MaxActiveReadersIs20) { // Registration and clearing // ============================================================================ -TEST_F(SubscriptionThreadDispatcherTest, SetAudioCallbackStoresRegistration) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, - [](const AudioFrame &) {}); - - EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); -} - TEST_F(SubscriptionThreadDispatcherTest, SetAudioCallbackByTrackNameStoresRegistration) { SubscriptionThreadDispatcher dispatcher; @@ -177,17 +161,7 @@ TEST_F(SubscriptionThreadDispatcherTest, EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); EXPECT_EQ( - audioCallbacks(dispatcher) - .count(CallbackKey{"alice", TrackSource::SOURCE_UNKNOWN, "mic-main"}), - 1u); -} - -TEST_F(SubscriptionThreadDispatcherTest, SetVideoCallbackStoresRegistration) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, - [](const VideoFrame &, std::int64_t) {}); - - EXPECT_EQ(videoCallbacks(dispatcher).size(), 1u); + audioCallbacks(dispatcher).count(CallbackKey{"alice", "mic-main"}), 1u); } TEST_F(SubscriptionThreadDispatcherTest, @@ -198,20 +172,7 @@ TEST_F(SubscriptionThreadDispatcherTest, EXPECT_EQ(videoCallbacks(dispatcher).size(), 1u); EXPECT_EQ( - videoCallbacks(dispatcher) - .count(CallbackKey{"alice", TrackSource::SOURCE_UNKNOWN, "cam-main"}), - 1u); -} - -TEST_F(SubscriptionThreadDispatcherTest, - ClearAudioCallbackRemovesRegistration) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, - [](const AudioFrame &) {}); - ASSERT_EQ(audioCallbacks(dispatcher).size(), 1u); - - dispatcher.clearOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE); - EXPECT_EQ(audioCallbacks(dispatcher).size(), 0u); + videoCallbacks(dispatcher).count(CallbackKey{"alice", "cam-main"}), 1u); } TEST_F(SubscriptionThreadDispatcherTest, @@ -225,17 +186,6 @@ TEST_F(SubscriptionThreadDispatcherTest, EXPECT_EQ(audioCallbacks(dispatcher).size(), 0u); } -TEST_F(SubscriptionThreadDispatcherTest, - ClearVideoCallbackRemovesRegistration) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, - [](const VideoFrame &, std::int64_t) {}); - ASSERT_EQ(videoCallbacks(dispatcher).size(), 1u); - - dispatcher.clearOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA); - EXPECT_EQ(videoCallbacks(dispatcher).size(), 0u); -} - TEST_F(SubscriptionThreadDispatcherTest, ClearVideoCallbackByTrackNameRemovesRegistration) { SubscriptionThreadDispatcher dispatcher; @@ -249,12 +199,10 @@ TEST_F(SubscriptionThreadDispatcherTest, TEST_F(SubscriptionThreadDispatcherTest, ClearNonExistentCallbackIsNoOp) { SubscriptionThreadDispatcher dispatcher; - EXPECT_NO_THROW(dispatcher.clearOnAudioFrameCallback( - "nobody", TrackSource::SOURCE_MICROPHONE)); - EXPECT_NO_THROW(dispatcher.clearOnVideoFrameCallback( - "nobody", TrackSource::SOURCE_CAMERA)); - EXPECT_NO_THROW(dispatcher.clearOnAudioFrameCallback("nobody", "missing")); - EXPECT_NO_THROW(dispatcher.clearOnVideoFrameCallback("nobody", "missing")); + EXPECT_NO_THROW( + dispatcher.clearOnAudioFrameCallback("nobody", "missing-audio")); + EXPECT_NO_THROW( + dispatcher.clearOnVideoFrameCallback("nobody", "missing-video")); } TEST_F(SubscriptionThreadDispatcherTest, @@ -264,10 +212,10 @@ TEST_F(SubscriptionThreadDispatcherTest, std::atomic counter2{0}; dispatcher.setOnAudioFrameCallback( - "alice", TrackSource::SOURCE_MICROPHONE, + "alice", "mic-main", [&counter1](const AudioFrame &) { counter1++; }); dispatcher.setOnAudioFrameCallback( - "alice", TrackSource::SOURCE_MICROPHONE, + "alice", "mic-main", [&counter2](const AudioFrame &) { counter2++; }); EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u) @@ -277,76 +225,48 @@ TEST_F(SubscriptionThreadDispatcherTest, TEST_F(SubscriptionThreadDispatcherTest, OverwriteVideoCallbackKeepsSingleEntry) { SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, + dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame &, std::int64_t) {}); - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, + dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame &, std::int64_t) {}); EXPECT_EQ(videoCallbacks(dispatcher).size(), 1u); } -TEST_F(SubscriptionThreadDispatcherTest, - OverwriteTrackNameAudioCallbackKeepsSingleEntry) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", "mic-main", - [](const AudioFrame &) {}); - dispatcher.setOnAudioFrameCallback("alice", "mic-main", - [](const AudioFrame &) {}); - - EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); -} - TEST_F(SubscriptionThreadDispatcherTest, MultipleDistinctCallbacksAreIndependent) { SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame &) {}); - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, + dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame &, std::int64_t) {}); - dispatcher.setOnAudioFrameCallback("bob", TrackSource::SOURCE_MICROPHONE, + dispatcher.setOnAudioFrameCallback("bob", "mic-main", [](const AudioFrame &) {}); - dispatcher.setOnVideoFrameCallback("bob", TrackSource::SOURCE_CAMERA, + dispatcher.setOnVideoFrameCallback("bob", "cam-main", [](const VideoFrame &, std::int64_t) {}); EXPECT_EQ(audioCallbacks(dispatcher).size(), 2u); EXPECT_EQ(videoCallbacks(dispatcher).size(), 2u); - dispatcher.clearOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE); + dispatcher.clearOnAudioFrameCallback("alice", "mic-main"); EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); EXPECT_EQ(videoCallbacks(dispatcher).size(), 2u); } -TEST_F(SubscriptionThreadDispatcherTest, ClearingOneSourceDoesNotAffectOther) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, - [](const AudioFrame &) {}); - dispatcher.setOnAudioFrameCallback("alice", - TrackSource::SOURCE_SCREENSHARE_AUDIO, - [](const AudioFrame &) {}); - ASSERT_EQ(audioCallbacks(dispatcher).size(), 2u); - - dispatcher.clearOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE); - EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); - - CallbackKey remaining{"alice", TrackSource::SOURCE_SCREENSHARE_AUDIO, ""}; - EXPECT_EQ(audioCallbacks(dispatcher).count(remaining), 1u); -} - TEST_F(SubscriptionThreadDispatcherTest, - SourceAndTrackNameAudioCallbacksAreIndependent) { + ClearingOneTrackNameDoesNotAffectOther) { SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, - [](const AudioFrame &) {}); dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame &) {}); + dispatcher.setOnAudioFrameCallback("alice", "mic-backup", + [](const AudioFrame &) {}); ASSERT_EQ(audioCallbacks(dispatcher).size(), 2u); dispatcher.clearOnAudioFrameCallback("alice", "mic-main"); EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); - EXPECT_EQ( - audioCallbacks(dispatcher) - .count(CallbackKey{"alice", TrackSource::SOURCE_MICROPHONE, ""}), - 1u); + + CallbackKey remaining{"alice", "mic-backup"}; + EXPECT_EQ(audioCallbacks(dispatcher).count(remaining), 1u); } // ============================================================================ @@ -361,7 +281,7 @@ TEST_F(SubscriptionThreadDispatcherTest, NoActiveReadersInitially) { TEST_F(SubscriptionThreadDispatcherTest, ActiveReadersEmptyAfterCallbackRegistration) { SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame &) {}); EXPECT_TRUE(activeReaders(dispatcher).empty()) << "Registering a callback without a subscribed track should not spawn " @@ -376,9 +296,9 @@ TEST_F(SubscriptionThreadDispatcherTest, DestroyDispatcherWithRegisteredCallbacksIsSafe) { EXPECT_NO_THROW({ SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame &) {}); - dispatcher.setOnVideoFrameCallback("bob", TrackSource::SOURCE_CAMERA, + dispatcher.setOnVideoFrameCallback("bob", "cam-main", [](const VideoFrame &, std::int64_t) {}); }); } @@ -387,10 +307,9 @@ TEST_F(SubscriptionThreadDispatcherTest, DestroyDispatcherAfterClearingCallbacksIsSafe) { EXPECT_NO_THROW({ SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame &) {}); - dispatcher.clearOnAudioFrameCallback("alice", - TrackSource::SOURCE_MICROPHONE); + dispatcher.clearOnAudioFrameCallback("alice", "mic-main"); }); } @@ -410,10 +329,9 @@ TEST_F(SubscriptionThreadDispatcherTest, ConcurrentRegistrationDoesNotCrash) { threads.emplace_back([&dispatcher, t, kIterations]() { for (int i = 0; i < kIterations; ++i) { std::string id = "participant-" + std::to_string(t); - dispatcher.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, + dispatcher.setOnAudioFrameCallback(id, "mic-main", [](const AudioFrame &) {}); - dispatcher.clearOnAudioFrameCallback(id, - TrackSource::SOURCE_MICROPHONE); + dispatcher.clearOnAudioFrameCallback(id, "mic-main"); } }); } @@ -438,11 +356,10 @@ TEST_F(SubscriptionThreadDispatcherTest, threads.emplace_back([&dispatcher, t, kIterations]() { std::string id = "p-" + std::to_string(t); for (int i = 0; i < kIterations; ++i) { - dispatcher.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, + dispatcher.setOnAudioFrameCallback(id, "mic-main", [](const AudioFrame &) {}); dispatcher.setOnVideoFrameCallback( - id, TrackSource::SOURCE_CAMERA, - [](const VideoFrame &, std::int64_t) {}); + id, "cam-main", [](const VideoFrame &, std::int64_t) {}); } }); } @@ -465,7 +382,7 @@ TEST_F(SubscriptionThreadDispatcherTest, ManyDistinctCallbacksCanBeRegistered) { for (int i = 0; i < kCount; ++i) { dispatcher.setOnAudioFrameCallback("participant-" + std::to_string(i), - TrackSource::SOURCE_MICROPHONE, + "mic-main", [](const AudioFrame &) {}); } @@ -473,7 +390,7 @@ TEST_F(SubscriptionThreadDispatcherTest, ManyDistinctCallbacksCanBeRegistered) { for (int i = 0; i < kCount; ++i) { dispatcher.clearOnAudioFrameCallback("participant-" + std::to_string(i), - TrackSource::SOURCE_MICROPHONE); + "mic-main"); } EXPECT_EQ(audioCallbacks(dispatcher).size(), 0u); @@ -671,9 +588,9 @@ TEST_F(SubscriptionThreadDispatcherTest, TEST_F(SubscriptionThreadDispatcherTest, MixedAudioVideoDataCallbacksAreIndependent) { SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame &) {}); - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, + dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame &, std::int64_t) {}); dispatcher.addOnDataFrameCallback( "alice", "data-track", From ee4c5d6168b3682af1e3b5565301f2d61943390a Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Thu, 23 Apr 2026 17:01:21 -0600 Subject: [PATCH 2/2] Deprecation section of the README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e9228565..25558314 100644 --- a/README.md +++ b/README.md @@ -559,6 +559,10 @@ lk token create \ --grant '{"canPublish":true,"canSubscribe":true,"canPublishData":true}' ``` +# Deprecation +- livekit_bridge (bridge/ folder) is deprecated. Avoid using it. Migrate to the base SDK. This will be removed on 06/01/2026. +- setOn*FrameCallback with TrackSource is deprecated. Use track name instead. This will be removed on 06/01/2026. +
LiveKit Ecosystem