diff --git a/CMakeLists.txt b/CMakeLists.txt index cc5a94e..07d31d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,22 +141,52 @@ add_custom_target(build_rust_ffi ALL VERBATIM ) +# A workaround to strip out the protozero_plugin.o symbols which cause our examples fail to link on Linux. +# Make sure CMAKE_AR is defined; if not, you can hardcode "ar" +if(NOT CMAKE_AR) + find_program(CMAKE_AR ar REQUIRED) +endif() + +add_custom_command( + TARGET build_rust_ffi + POST_BUILD + COMMAND ${CMAKE_AR} -dv $ protozero_plugin.o + COMMENT "Removing protozero_plugin.o (stray main) from liblivekit_ffi.a" +) + # ---- C++ wrapper library ---- add_library(livekit + include/livekit/audio_frame.h + include/livekit/audio_source.h include/livekit/room.h + include/livekit/room_delegate.h include/livekit/ffi_handle.h include/livekit/ffi_client.h + include/livekit/local_audio_track.h include/livekit/participant.h + include/livekit/local_participant.h include/livekit/livekit.h include/livekit/stats.h include/livekit/track.h + include/livekit/track_publication.h + include/livekit/local_track_publication.h + include/livekit/remote_track_publication.h + src/audio_frame.cpp + src/audio_source.cpp src/ffi_handle.cpp src/ffi_client.cpp + src/local_audio_track.cpp src/room.cpp - src/room_event_converter.cpp - src/room_event_converter.h + src/room_proto_converter.cpp + src/room_proto_converter.h + src/local_participant.cpp src/stats.cpp src/track.cpp + src/track_proto_converter.cpp + src/track_proto_converter.h + src/track_publication.cpp + src/local_track_publication.cpp + src/remote_track_publication.cpp ) # Add generated proto objects to the wrapper diff --git a/examples/simple_room/main.cpp b/examples/simple_room/main.cpp index 71f788a..5d69c70 100644 --- a/examples/simple_room/main.cpp +++ b/examples/simple_room/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -28,7 +29,7 @@ void print_usage(const char *prog) { << " LIVEKIT_URL, LIVEKIT_TOKEN\n"; } -void handle_sigint(int) { g_running = false; } +void handle_sigint(int) { g_running.store(false); } bool parse_args(int argc, char *argv[], std::string &url, std::string &token) { // 1) --help @@ -118,6 +119,47 @@ class SimpleRoomDelegate : public livekit::RoomDelegate { } }; +// Test utils to run a capture loop to publish noisy audio frames to the room +void runNoiseCaptureLoop(const std::shared_ptr &source) { + const int sample_rate = source->sample_rate(); + const int num_channels = source->num_channels(); + const int frame_ms = 10; + const int samples_per_channel = sample_rate * frame_ms / 1000; + + std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution noise_dist(-5000, 5000); + using Clock = std::chrono::steady_clock; + auto next_deadline = Clock::now(); + while (g_running.load(std::memory_order_relaxed)) { + AudioFrame frame = + AudioFrame::create(sample_rate, num_channels, samples_per_channel); + const std::size_t total_samples = + static_cast(num_channels) * + static_cast(samples_per_channel); + for (std::size_t i = 0; i < total_samples; ++i) { + frame.data()[i] = noise_dist(rng); + } + try { + source->captureFrame(frame); + } catch (const std::exception &e) { + // If something goes wrong, log and break out + std::cerr << "Error in captureFrame: " << e.what() << std::endl; + break; + } + + // Pace the loop to roughly real-time + next_deadline += std::chrono::milliseconds(frame_ms); + std::this_thread::sleep_until(next_deadline); + } + + // Optionally clear queued audio on exit + try { + source->clearQueue(); + } catch (...) { + // ignore errors on shutdown + std::cout << "Error in clearQueue" << std::endl; + } +} } // namespace int main(int argc, char *argv[]) { @@ -168,14 +210,46 @@ int main(int argc, char *argv[]) { << info.reliable_dc_buffered_amount_low_threshold << "\n" << " Creation time (ms): " << info.creation_time << "\n"; - // TOD(shijing), implement local and remoteParticipants in the room + auto audioSource = std::make_shared(44100, 1, 10); + auto audioTrack = + LocalAudioTrack::createLocalAudioTrack("micTrack", audioSource); + + TrackPublishOptions opts; + opts.source = TrackSource::SOURCE_MICROPHONE; + opts.dtx = false; + opts.simulcast = false; + try { + // publishTrack takes std::shared_ptr, LocalAudioTrack derives from + // Track + auto pub = room.local_participant()->publishTrack(audioTrack, opts); + + std::cout << "Published track:\n" + << " SID: " << pub->sid() << "\n" + << " Name: " << pub->name() << "\n" + << " Kind: " << static_cast(pub->kind()) << "\n" + << " Source: " << static_cast(pub->source()) << "\n" + << " Simulcasted: " << std::boolalpha << pub->simulcasted() + << "\n" + << " Muted: " << std::boolalpha << pub->muted() << "\n"; + } catch (const std::exception &e) { + std::cerr << "Failed to publish track: " << e.what() << std::endl; + } + + // TODO, if we have pre-buffering feature, we might consider starting the + // thread right after creating the source. + std::thread audioThread(runNoiseCaptureLoop, audioSource); // Keep the app alive until Ctrl-C so we continue receiving events, // similar to asyncio.run(main()) keeping the loop running. while (g_running.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + // Shutdown the audio thread. + if (audioThread.joinable()) { + audioThread.join(); + } + FfiClient::instance().shutdown(); std::cout << "Exiting.\n"; return 0; diff --git a/include/livekit/audio_frame.h b/include/livekit/audio_frame.h new file mode 100644 index 0000000..ae43351 --- /dev/null +++ b/include/livekit/audio_frame.h @@ -0,0 +1,96 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace livekit { + +namespace proto { +class AudioFrameBufferInfo; +class OwnedAudioFrameBuffer; +} // namespace proto + +class AudioFrame { +public: + /** + * Construct an AudioFrame from raw PCM samples. + * + * @param data Interleaved PCM samples (int16). + * @param sample_rate Sample rate (Hz). + * @param num_channels Number of channels. + * @param samples_per_channel Number of samples per channel. + * + * Throws std::invalid_argument if the data size is inconsistent with + * num_channels * samples_per_channel. + */ + AudioFrame(std::vector data, int sample_rate, int num_channels, + int samples_per_channel); + + /** + * Create a new zero-initialized AudioFrame instance. + */ + static AudioFrame create(int sample_rate, int num_channels, + int samples_per_channel); + + /** + * Construct an AudioFrame by copying data out of an OwnedAudioFrameBuffer. + */ + static AudioFrame fromOwnedInfo(const proto::OwnedAudioFrameBuffer &owned); + + /** + * Build a proto AudioFrameBufferInfo pointing at this frame’s data. + * + * The underlying buffer must stay alive as long as the native side + * uses the pointer. + * + */ + proto::AudioFrameBufferInfo toProto() const; + + // ---- Accessors ---- + + const std::vector &data() const noexcept { return data_; } + std::vector &data() noexcept { return data_; } + + /// Number of samples in the buffer (per all channels). + std::size_t total_samples() const noexcept { return data_.size(); } + + /// Sample rate in Hz. + int sample_rate() const noexcept { return sample_rate_; } + + /// Number of channels. + int num_channels() const noexcept { return num_channels_; } + + /// Samples per channel. + int samples_per_channel() const noexcept { return samples_per_channel_; } + + /// Duration in seconds (samples_per_channel / sample_rate). + double duration() const noexcept; + + /// A human-readable description (like Python __repr__). + std::string to_string() const; + +private: + std::vector data_; + int sample_rate_; + int num_channels_; + int samples_per_channel_; +}; + +} // namespace livekit \ No newline at end of file diff --git a/include/livekit/audio_source.h b/include/livekit/audio_source.h new file mode 100644 index 0000000..8e52bbd --- /dev/null +++ b/include/livekit/audio_source.h @@ -0,0 +1,127 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "livekit/audio_frame.h" +#include "livekit/ffi_handle.h" + +namespace livekit { + +namespace proto { +class FfiRequest; +class FfiResponse; +} // namespace proto + +class FfiClient; + +/** + * Represents a real-time audio source with an internal audio queue. + */ +class AudioSource { +public: + /** + * Create a new native audio source. + * + * @param sample_rate Sample rate in Hz. + * @param num_channels Number of channels. + * @param queue_size_ms Max buffer duration for the internal queue in ms. + */ + AudioSource(int sample_rate, int num_channels, int queue_size_ms = 1000); + + ~AudioSource(); + + AudioSource(const AudioSource &) = delete; + AudioSource &operator=(const AudioSource &) = delete; + AudioSource(AudioSource &&) noexcept = default; + AudioSource &operator=(AudioSource &&) noexcept = default; + + /// The sample rate of the audio source in Hz. + int sample_rate() const noexcept { return sample_rate_; } + + /// The number of audio channels. + int num_channels() const noexcept { return num_channels_; } + + /// Underlying FFI handle ID used in FFI requests. + std::uint64_t ffi_handle_id() const noexcept { + return static_cast(handle_.get()); + } + + /// Current duration of queued audio (in seconds). + double queuedDuration() const noexcept; + + /** + * Clears the internal audio queue on the native side and resets local + * queue tracking. + */ + void clearQueue(); + + /** + * Push an AudioFrame into the audio source and BLOCK until the FFI callback + * confirms that the native side has finished processing (consuming) the + * frame. Safe usage: The frame's internal buffer must remain valid only until + * this function returns. Because this call blocks until the corresponding FFI + * callback arrives, the caller may safely destroy or reuse the frame + * afterward. + * @param frame The audio frame to send. No-op if the frame contains + * zero samples. + * @param timeout_ms Maximum time to wait for the FFI callback. + * - If timeout_ms > 0: block up to this duration. + * A timeout will cause std::runtime_error. + * - If timeout_ms == 0: wait indefinitely until the + * callback arrives (recommended for production unless the caller needs + * explicit timeout control). + * + * Notes: + * - This is a blocking call. + * - timeout_ms == 0 (infinite wait) is the safest mode because it + * guarantees the callback completes before the function returns, which in + * turn guarantees that the audio buffer lifetime is fully protected. The + * caller does not need to manage or extend the frame lifetime manually. + * + * - May throw std::runtime_error if: + * • the FFI reports an error + * + * - The underlying FFI request *must* eventually produce a callback for + * each frame. If the FFI layer is misbehaving or the event loop is stalled, + * a timeout may occur in bounded-wait mode. + */ + void captureFrame(const AudioFrame &frame, int timeout_ms = 20); + + /** + * Block until the currently queued audio has (roughly) played out. + */ + void waitForPlayout() const; + +private: + // Internal helper to reset the local queue tracking (like _release_waiter). + void resetQueueTracking() noexcept; + + int sample_rate_; + int num_channels_; + int queue_size_ms_; + + // RAII wrapper for this audio source's FFI handle + FfiHandle handle_; + + // Queue tracking (all in seconds; based on steady_clock in the .cpp). + mutable double last_capture_{0.0}; + mutable double q_size_{0.0}; +}; + +} // namespace livekit diff --git a/include/livekit/ffi_client.h b/include/livekit/ffi_client.h index 7d259b9..602f9c8 100644 --- a/include/livekit/ffi_client.h +++ b/include/livekit/ffi_client.h @@ -29,12 +29,17 @@ namespace livekit { namespace proto { +class AudioFrameBufferInfo; +class ConnectCallback; class FfiEvent; class FfiResponse; class FfiRequest; -class RoomInfo; +class OwnedTrackPublication; +class TranscriptionSegment; } // namespace proto +struct TrackPublishOptions; + using FfiCallbackFn = void (*)(const uint8_t *, size_t); extern "C" void livekit_ffi_initialize(FfiCallbackFn cb, bool capture_logs, const char *sdk, @@ -70,16 +75,45 @@ class FfiClient { void RemoveListener(ListenerId id); // Room APIs - std::future connectAsync(const std::string &url, - const std::string &token); + std::future connectAsync(const std::string &url, + const std::string &token); // Track APIs std::future> getTrackStatsAsync(uintptr_t track_handle); + // Participant APIs + std::future + publishTrackAsync(std::uint64_t local_participant_handle, + std::uint64_t track_handle, + const TrackPublishOptions &options); + std::future unpublishTrackAsync(std::uint64_t local_participant_handle, + const std::string &track_sid, + bool stop_on_unpublish); + std::future + publishDataAsync(std::uint64_t local_participant_handle, + const std::uint8_t *data_ptr, std::uint64_t data_len, + bool reliable, + const std::vector &destination_identities, + const std::string &topic); + std::future publishTranscriptionAsync( + std::uint64_t local_participant_handle, + const std::string &participant_identity, const std::string &track_id, + const std::vector &segments); + std::future + publishSipDtmfAsync(std::uint64_t local_participant_handle, + std::uint32_t code, const std::string &digit, + const std::vector &destination_identities); + std::future + setLocalMetadataAsync(std::uint64_t local_participant_handle, + const std::string &metadata); + std::future + captureAudioFrameAsync(std::uint64_t source_handle, + const proto::AudioFrameBufferInfo &buffer); + // Generic function for sending a request to the Rust FFI. // Note: For asynchronous requests, use the dedicated async functions instead - // of SendRequest. - proto::FfiResponse SendRequest(const proto::FfiRequest &request) const; + // of sendRequest. + proto::FfiResponse sendRequest(const proto::FfiRequest &request) const; private: // Base class for type-erased pending ops diff --git a/include/livekit/ffi_handle.h b/include/livekit/ffi_handle.h index c710454..55ef6b6 100644 --- a/include/livekit/ffi_handle.h +++ b/include/livekit/ffi_handle.h @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2025 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index bfb2287..015521c 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -14,5 +14,12 @@ * limitations under the License. */ +#include "audio_frame.h" +#include "audio_source.h" +#include "local_audio_track.h" +#include "local_participant.h" +#include "local_track_publication.h" +#include "participant.h" #include "room.h" #include "room_delegate.h" +#include "track_publication.h" \ No newline at end of file diff --git a/include/livekit/local_audio_track.h b/include/livekit/local_audio_track.h new file mode 100644 index 0000000..66cc52b --- /dev/null +++ b/include/livekit/local_audio_track.h @@ -0,0 +1,52 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "track.h" +#include +#include + +namespace livekit { + +namespace proto { +class OwnedTrack; +} + +class AudioSource; + +// ============================================================ +// LocalAudioTrack +// ============================================================ +class LocalAudioTrack : public Track { +public: + explicit LocalAudioTrack(FfiHandle handle, const proto::OwnedTrack &track); + + static std::shared_ptr + createLocalAudioTrack(const std::string &name, + const std::shared_ptr &source); + + // Mute/unmute + void mute(); + void unmute(); + + std::string to_string() const; + +private: + // Optional: you may add private helpers if needed +}; + +} // namespace livekit \ No newline at end of file diff --git a/include/livekit/local_participant.h b/include/livekit/local_participant.h new file mode 100644 index 0000000..65dcb16 --- /dev/null +++ b/include/livekit/local_participant.h @@ -0,0 +1,134 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "livekit/ffi_handle.h" +#include "livekit/participant.h" +#include "livekit/room_delegate.h" + +#include +#include +#include +#include +#include + +namespace livekit { + +struct ParticipantTrackPermission; + +class FfiClient; +class Track; +class LocalTrackPublication; +struct Transcription; + +/** + * Represents the local participant in a room. + * + * C++ analogue of the Python LocalParticipant, built on top of the C++ + * Participant base class. + */ +class LocalParticipant : public Participant { +public: + using PublicationMap = + std::unordered_map>; + + LocalParticipant(FfiHandle handle, std::string sid, std::string name, + std::string identity, std::string metadata, + std::unordered_map attributes, + ParticipantKind kind, DisconnectReason reason); + + /// Track publications associated with this participant, keyed by track SID. + const PublicationMap &trackPublications() const noexcept { + return track_publications_; + } + + /** + * Publish arbitrary data to the room. + * + * @param payload Raw bytes to send. + * @param reliable Whether to send reliably or not. + * @param destination_identities Optional list of participant identities. + * @param topic Optional topic string. + * + * Throws std::runtime_error if FFI reports an error (if you wire that up). + */ + void publishData(const std::vector &payload, + bool reliable = true, + const std::vector &destination_identities = {}, + const std::string &topic = {}); + + /** + * Publish SIP DTMF message. + */ + void publishDtmf(int code, const std::string &digit); + + /** + * Publish transcription data to the room. + * + * @param transcription + */ + void publishTranscription(const Transcription &transcription); + + // ------------------------------------------------------------------------- + // Metadata APIs (set metadata / name / attributes) + // ------------------------------------------------------------------------- + + void setMetadata(const std::string &metadata); + void setName(const std::string &name); + void + setAttributes(const std::unordered_map &attributes); + + // ------------------------------------------------------------------------- + // Subscription permissions + // ------------------------------------------------------------------------- + + /** + * Set track subscription permissions for this participant. + * + * @param allow_all_participants If true, all participants may subscribe. + * @param participant_permissions Optional participant-specific permissions. + */ + void + setTrackSubscriptionPermissions(bool allow_all_participants, + const std::vector + &participant_permissions = {}); + + // ------------------------------------------------------------------------- + // Track publish / unpublish (synchronous analogue) + // ------------------------------------------------------------------------- + + /** + * Publish a local track to the room. + * + * Throws std::runtime_error on error (e.g. publish failure). + */ + std::shared_ptr + publishTrack(const std::shared_ptr &track, + const TrackPublishOptions &options); + + /** + * Unpublish a track from the room by SID. + * + * If the publication exists in the local map, it is removed. + */ + void unpublishTrack(const std::string &track_sid); + +private: + PublicationMap track_publications_; +}; + +} // namespace livekit diff --git a/include/livekit/local_track_publication.h b/include/livekit/local_track_publication.h new file mode 100644 index 0000000..b53e4f5 --- /dev/null +++ b/include/livekit/local_track_publication.h @@ -0,0 +1,38 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "livekit/track_publication.h" + +namespace livekit { + +namespace proto { +class OwnedTrackPublication; +} + +class Track; + +class LocalTrackPublication : public TrackPublication { +public: + /// Construct from an OwnedTrackPublication proto. + explicit LocalTrackPublication(const proto::OwnedTrackPublication &owned); + + /// Typed accessor for the attached LocalTrack (if any). + std::shared_ptr track() const noexcept; +}; + +} // namespace livekit diff --git a/include/livekit/participant.h b/include/livekit/participant.h index b09f4e8..826263b 100644 --- a/include/livekit/participant.h +++ b/include/livekit/participant.h @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2025 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,41 +22,24 @@ #include #include "livekit/ffi_handle.h" +#include "livekit/room_delegate.h" #include "livekit_ffi.h" namespace livekit { enum class ParticipantKind { Standard = 0, Ingress, Egress, Sip, Agent }; -enum class DisconnectReason { - Unknown = 0, - ClientInitiated, - DuplicateIdentity, - ServerShutdown, - ParticipantRemoved, - RoomDeleted, - StateMismatch, - JoinFailure, - Migration, - SignalClose, - RoomClosed, - UserUnavailable, - UserRejected, - SipTrunkFailure, - ConnectionTimeout, - MediaFailure -}; - class Participant { public: // TODO, consider holding a weak ptr of FfiHandle if it is useful. - Participant(std::weak_ptr handle, std::string sid, - std::string name, std::string identity, std::string metadata, + Participant(FfiHandle handle, std::string sid, std::string name, + std::string identity, std::string metadata, std::unordered_map attributes, ParticipantKind kind, DisconnectReason reason) - : handle_(handle), sid_(std::move(sid)), name_(std::move(name)), - identity_(std::move(identity)), metadata_(std::move(metadata)), - attributes_(std::move(attributes)), kind_(kind), reason_(reason) {} + : handle_(std::move(handle)), sid_(std::move(sid)), + name_(std::move(name)), identity_(std::move(identity)), + metadata_(std::move(metadata)), attributes_(std::move(attributes)), + kind_(kind), reason_(reason) {} // Plain getters/setters (caller ensures threading) const std::string &sid() const noexcept { return sid_; } @@ -70,15 +53,10 @@ class Participant { ParticipantKind kind() const noexcept { return kind_; } DisconnectReason disconnectReason() const noexcept { return reason_; } - uintptr_t ffiHandleId() const noexcept { - if (auto h = handle_.lock()) { - return h->get(); - } - return INVALID_HANDLE; - } + uintptr_t ffiHandleId() const noexcept { return handle_.get(); } private: - std::weak_ptr handle_; + FfiHandle handle_; std::string sid_, name_, identity_, metadata_; std::unordered_map attributes_; ParticipantKind kind_; diff --git a/include/livekit/remote_track_publication.h b/include/livekit/remote_track_publication.h new file mode 100644 index 0000000..9066058 --- /dev/null +++ b/include/livekit/remote_track_publication.h @@ -0,0 +1,44 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "livekit/track_publication.h" + +namespace livekit { + +namespace proto { +class OwnedTrackPublication; +} + +class Track; + +class RemoteTrackPublication : public TrackPublication { +public: + explicit RemoteTrackPublication(const proto::OwnedTrackPublication &owned); + + /// Typed accessor for the attached RemoteTrack (if any). + std::shared_ptr track() const noexcept; + + bool subscribed() const noexcept { return subscribed_; } + + void setSubscribed(bool subscribed); + +private: + bool subscribed_{false}; +}; + +} // namespace livekit diff --git a/include/livekit/room.h b/include/livekit/room.h index 8fe9a34..54f0d99 100644 --- a/include/livekit/room.h +++ b/include/livekit/room.h @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2025 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ #include "livekit/ffi_client.h" #include "livekit/ffi_handle.h" #include "livekit/room_delegate.h" +#include #include namespace livekit { @@ -30,20 +31,26 @@ namespace proto { class FfiEvent; } +class LocalParticipant; + class Room { public: - Room() = default; + Room(); + ~Room(); void setDelegate(RoomDelegate *delegate); bool Connect(const std::string &url, const std::string &token); // Accessors RoomInfoData room_info() const; + LocalParticipant *local_participant() const; private: mutable std::mutex lock_; bool connected_{false}; RoomDelegate *delegate_ = nullptr; // Not owned RoomInfoData room_info_; + std::shared_ptr room_handle_; + std::unique_ptr local_participant_; void OnEvent(const proto::FfiEvent &event); }; diff --git a/include/livekit/room_delegate.h b/include/livekit/room_delegate.h index 8e5f03b..72411fc 100644 --- a/include/livekit/room_delegate.h +++ b/include/livekit/room_delegate.h @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2025 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ namespace livekit { class Room; +enum class VideoCodec; +enum class TrackSource; enum class ConnectionQuality { Poor, @@ -52,25 +54,28 @@ enum class EncryptionState { }; enum class DisconnectReason { - // mirror your proto DisconnectReason values as needed - Unknown, + Unknown = 0, ClientInitiated, - ServerInitiated, + DuplicateIdentity, + ServerShutdown, + ParticipantRemoved, + RoomDeleted, + StateMismatch, + JoinFailure, + Migration, + SignalClose, + RoomClosed, + UserUnavailable, + UserRejected, + SipTrunkFailure, + ConnectionTimeout, + MediaFailure }; // --------------------------------------------------------- // Basic data types corresponding to proto messages // --------------------------------------------------------- -struct TranscriptionSegmentData { - std::string id; - std::string text; - std::uint64_t start_time = 0; - std::uint64_t end_time = 0; - bool is_final = false; - std::string language; -}; - struct ChatMessageData { std::string id; std::int64_t timestamp = 0; @@ -156,6 +161,40 @@ struct DataStreamTrailerData { std::map attributes; }; +// ------------- rooom.proto options ------------------------ + +struct VideoEncodingOptions { + std::uint64_t max_bitrate = 0; + double max_framerate = 0.0; +}; + +struct AudioEncodingOptions { + std::uint64_t max_bitrate = 0; +}; + +struct TrackPublishOptions { + std::optional video_encoding; + std::optional audio_encoding; + std::optional video_codec; + std::optional dtx; + std::optional red; + std::optional simulcast; + std::optional source; + std::optional stream; + std::optional preconnect_buffer; +}; + +// ------------- rooom.proto Transcription ------------------------ + +struct TranscriptionSegment { + std::string id; + std::string text; + std::uint64_t start_time = 0; + std::uint64_t end_time = 0; + bool final = false; + std::string language; +}; + // --------------------------------------------------------- // Event structs – “public” representations of RoomEvent.* // --------------------------------------------------------- @@ -271,10 +310,10 @@ struct DataPacketReceivedEvent { std::optional sip_dtmf; }; -struct TranscriptionReceivedEvent { +struct Transcription { std::optional participant_identity; std::optional track_sid; - std::vector segments; + std::vector segments; }; struct ConnectionStateChangedEvent { @@ -423,8 +462,7 @@ class RoomDelegate { // Data / transcription / chat virtual void onDataPacketReceived(Room &, const DataPacketReceivedEvent &) {} - virtual void onTranscriptionReceived(Room &, - const TranscriptionReceivedEvent &) {} + virtual void onTranscriptionReceived(Room &, const Transcription &) {} virtual void onChatMessageReceived(Room &, const ChatMessageReceivedEvent &) { } diff --git a/include/livekit/stats.h b/include/livekit/stats.h index d05a22f..c9aa1f6 100644 --- a/include/livekit/stats.h +++ b/include/livekit/stats.h @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2025 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/include/livekit/track.h b/include/livekit/track.h index ae5fd0c..2c5f9a9 100644 --- a/include/livekit/track.h +++ b/include/livekit/track.h @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2025 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,6 +58,12 @@ enum class AudioTrackFeature { TF_PRECONNECT_BUFFER = 6, }; +struct ParticipantTrackPermission { + std::string participant_identity; + std::optional allow_all; + std::vector allowed_track_sids; +}; + // ============================================================ // Base Track // ============================================================ @@ -81,15 +87,8 @@ class Track { std::optional mime_type() const noexcept { return mime_type_; } // Handle access - bool has_handle() const noexcept { return !handle_.expired(); } - uintptr_t ffi_handle_id() const noexcept { - if (auto h = handle_.lock()) - return h->get(); - return 0; - } - std::shared_ptr lock_handle() const noexcept { - return handle_.lock(); - } + bool has_handle() const noexcept { return !handle_.valid(); } + uintptr_t ffi_handle_id() const noexcept { return handle_.get(); } // Async get stats std::future> getStats() const; @@ -100,8 +99,8 @@ class Track { void setName(std::string n) noexcept { name_ = std::move(n); } protected: - Track(std::weak_ptr handle, std::string sid, std::string name, - TrackKind kind, StreamState state, bool muted, bool remote); + Track(FfiHandle handle, std::string sid, std::string name, TrackKind kind, + StreamState state, bool muted, bool remote); void setPublicationFields(std::optional source, std::optional simulcasted, @@ -110,7 +109,7 @@ class Track { std::optional mime_type); private: - std::weak_ptr handle_; // non-owning + FfiHandle handle_; // Owned std::string sid_; std::string name_; diff --git a/include/livekit/track_publication.h b/include/livekit/track_publication.h new file mode 100644 index 0000000..9536437 --- /dev/null +++ b/include/livekit/track_publication.h @@ -0,0 +1,104 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "livekit/ffi_handle.h" +#include "livekit/track.h" // TrackKind, TrackSource, AudioTrackFeature + +namespace livekit { + +// TODO, move this EncryptionType to e2ee_types.h +enum class EncryptionType { + NONE = 0, + GCM = 1, + CUSTOM = 2, +}; + +class Track; +class LocalTrack; +class RemoteTrack; + +/** + * C++ analogue of Python TrackPublication. + * + * Wraps the immutable publication info plus an FFI handle, and + * holds a weak reference to the associated Track (if any). + */ +class TrackPublication { +public: + virtual ~TrackPublication() = default; + + TrackPublication(const TrackPublication &) = delete; + TrackPublication &operator=(const TrackPublication &) = delete; + TrackPublication(TrackPublication &&) noexcept = default; + TrackPublication &operator=(TrackPublication &&) noexcept = default; + + // Basic metadata + const std::string &sid() const noexcept { return sid_; } + const std::string &name() const noexcept { return name_; } + TrackKind kind() const noexcept { return kind_; } + TrackSource source() const noexcept { return source_; } + bool simulcasted() const noexcept { return simulcasted_; } + std::uint32_t width() const noexcept { return width_; } + std::uint32_t height() const noexcept { return height_; } + const std::string &mimeType() const noexcept { return mime_type_; } + bool muted() const noexcept { return muted_; } + + EncryptionType encryptionType() const noexcept { return encryption_type_; } + const std::vector &audioFeatures() const noexcept { + return audio_features_; + } + + /// Underlying FFI handle value. + uintptr_t ffiHandleId() const noexcept { return handle_.get(); } + + /// Associated Track (if attached). + std::shared_ptr track() const noexcept { return track_.lock(); } + void setTrack(const std::shared_ptr &track) noexcept { + track_ = track; + } + +protected: + TrackPublication(FfiHandle handle, std::string sid, std::string name, + TrackKind kind, TrackSource source, bool simulcasted, + std::uint32_t width, std::uint32_t height, + std::string mime_type, bool muted, + EncryptionType encryption_type, + std::vector audio_features); + + FfiHandle handle_; + std::weak_ptr track_; + + std::string sid_; + std::string name_; + TrackKind kind_; + TrackSource source_; + bool simulcasted_{false}; + std::uint32_t width_{0}; + std::uint32_t height_{0}; + std::string mime_type_; + bool muted_{false}; + EncryptionType encryption_type_; + std::vector audio_features_; +}; + +} // namespace livekit diff --git a/src/audio_frame.cpp b/src/audio_frame.cpp new file mode 100644 index 0000000..0df9f76 --- /dev/null +++ b/src/audio_frame.cpp @@ -0,0 +1,123 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "livekit/audio_frame.h" + +#include +#include +#include +#include +#include + +#include "audio_frame.pb.h" +#include "handle.pb.h" +#include "livekit/ffi_handle.h" + +namespace livekit { + +AudioFrame::AudioFrame(std::vector data, int sample_rate, + int num_channels, int samples_per_channel) + : data_(std::move(data)), sample_rate_(sample_rate), + num_channels_(num_channels), samples_per_channel_(samples_per_channel) { + const std::size_t expected = static_cast(num_channels_) * + static_cast(samples_per_channel_); + + if (data_.size() < expected) { + throw std::invalid_argument( + "AudioFrame: data size must be >= num_channels * samples_per_channel"); + } + if (data_.size() % expected != 0) { + throw std::invalid_argument( + "AudioFrame: data size must be an exact multiple of " + "num_channels * samples_per_channel"); + } +} + +AudioFrame AudioFrame::create(int sample_rate, int num_channels, + int samples_per_channel) { + const std::size_t count = static_cast(num_channels) * + static_cast(samples_per_channel); + std::vector data(count, 0); + return AudioFrame(std::move(data), sample_rate, num_channels, + samples_per_channel); +} + +AudioFrame +AudioFrame::fromOwnedInfo(const proto::OwnedAudioFrameBuffer &owned) { + const auto &info = owned.info(); + + const int num_channels = static_cast(info.num_channels()); + const int samples_per_channel = static_cast(info.samples_per_channel()); + const int sample_rate = static_cast(info.sample_rate()); + + const std::size_t count = static_cast(num_channels) * + static_cast(samples_per_channel); + + const std::int16_t *ptr = + reinterpret_cast(info.data_ptr()); + + if (ptr == nullptr && count > 0) { + throw std::runtime_error( + "AudioFrame::fromOwnedInfo: null data_ptr with nonzero size"); + } + + std::vector data; + if (count > 0) { + data.assign(ptr, ptr + count); + } + + { + FfiHandle guard(static_cast(owned.handle().id())); + // guard is destroyed at end of scope, which should call into the FFI to + // drop the OwnedAudioFrameBuffer. + } + + return AudioFrame(std::move(data), sample_rate, num_channels, + samples_per_channel); +} + +proto::AudioFrameBufferInfo AudioFrame::toProto() const { + proto::AudioFrameBufferInfo info; + const std::uint64_t ptr = + data_.empty() ? 0 : reinterpret_cast(data_.data()); + + info.set_data_ptr(ptr); + info.set_num_channels(static_cast(num_channels_)); + info.set_sample_rate(static_cast(sample_rate_)); + info.set_samples_per_channel( + static_cast(samples_per_channel_)); + return info; +} + +double AudioFrame::duration() const noexcept { + if (sample_rate_ <= 0) { + return 0.0; + } + return static_cast(samples_per_channel_) / + static_cast(sample_rate_); +} + +std::string AudioFrame::to_string() const { + std::ostringstream oss; + oss << "rtc.AudioFrame(sample_rate=" << sample_rate_ + << ", num_channels=" << num_channels_ + << ", samples_per_channel=" << samples_per_channel_ + << ", duration=" << std::fixed << std::setprecision(3) << duration() + << ")"; + return oss.str(); +} + +} // namespace livekit diff --git a/src/audio_source.cpp b/src/audio_source.cpp new file mode 100644 index 0000000..36fb226 --- /dev/null +++ b/src/audio_source.cpp @@ -0,0 +1,148 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "livekit/audio_source.h" + +#include +#include +#include + +#include "audio_frame.pb.h" +#include "ffi.pb.h" +#include "livekit/audio_frame.h" +#include "livekit/ffi_client.h" + +namespace livekit { + +using Clock = std::chrono::steady_clock; + +// Helper to get monotonic time in seconds (similar to time.monotonic()). +static double now_seconds() { + auto now = Clock::now().time_since_epoch(); + return std::chrono::duration_cast>(now).count(); +} + +// ============================================================================ +// AudioSource +// ============================================================================ + +AudioSource::AudioSource(int sample_rate, int num_channels, int queue_size_ms) + : sample_rate_(sample_rate), num_channels_(num_channels), + queue_size_ms_(queue_size_ms) { + proto::FfiRequest req; + auto *msg = req.mutable_new_audio_source(); + msg->set_type(proto::AudioSourceType::AUDIO_SOURCE_NATIVE); + msg->set_sample_rate(static_cast(sample_rate_)); + msg->set_num_channels(static_cast(num_channels_)); + msg->set_queue_size_ms(static_cast(queue_size_ms_)); + + proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + + const auto &source_info = resp.new_audio_source().source(); + // Wrap FFI handle in RAII FfiHandle + handle_ = FfiHandle(static_cast(source_info.handle().id())); +} + +AudioSource::~AudioSource() { + // Let FfiHandle::~FfiHandle() drop the native handle. + // If you later add an explicit "dispose" request, you can send it here. +} + +double AudioSource::queuedDuration() const noexcept { + if (last_capture_ == 0.0) { + return 0.0; + } + + double now = now_seconds(); + double elapsed = now - last_capture_; + double remaining = q_size_ - elapsed; + return remaining > 0.0 ? remaining : 0.0; +} + +void AudioSource::resetQueueTracking() noexcept { + last_capture_ = 0.0; + q_size_ = 0.0; +} + +void AudioSource::clearQueue() { + if (!handle_) { + resetQueueTracking(); + return; + } + + proto::FfiRequest req; + auto *msg = req.mutable_clear_audio_buffer(); + msg->set_source_handle(static_cast(handle_.get())); + + (void)FfiClient::instance().sendRequest(req); + + // Reset local queue tracking. + resetQueueTracking(); +} + +void AudioSource::captureFrame(const AudioFrame &frame, int timeout_ms) { + using namespace std::chrono_literals; + if (!handle_) { + return; + } + + if (frame.samples_per_channel() == 0) { + return; + } + + // Queue tracking, same logic as before + double now = now_seconds(); + double elapsed = (last_capture_ == 0.0) ? 0.0 : (now - last_capture_); + double frame_duration = static_cast(frame.samples_per_channel()) / + static_cast(sample_rate_); + q_size_ += frame_duration - elapsed; + if (q_size_ < 0.0) { + q_size_ = 0.0; // clamp + } + last_capture_ = now; + + // Build AudioFrameBufferInfo from the wrapper + proto::AudioFrameBufferInfo buf = frame.toProto(); + // Use async FFI API and block until the callback completes + auto fut = FfiClient::instance().captureAudioFrameAsync(handle_.get(), buf); + if (timeout_ms == 0) { + fut.get(); // may throw std::runtime_error from async layer + return; + } + // This will throw std::runtime_error if the callback reported an error + auto status = fut.wait_for(std::chrono::milliseconds(timeout_ms)); + if (status == std::future_status::ready || + status == std::future_status::deferred) { + fut.get(); + } else { // std::future_status::timeout + std::cerr << "captureAudioFrameAsync timed out after " << timeout_ms + << " ms\n"; + } +} + +void AudioSource::waitForPlayout() const { + // Python uses a future + event loop timer that fires after q_size. + // Here we approximate that by simply sleeping for the current queued + // duration. + double dur = queuedDuration(); + if (dur <= 0.0) { + return; + } + + std::this_thread::sleep_for(std::chrono::duration(dur)); +} + +} // namespace livekit diff --git a/src/ffi_client.cpp b/src/ffi_client.cpp index d09d048..0fe4d3b 100644 --- a/src/ffi_client.cpp +++ b/src/ffi_client.cpp @@ -20,12 +20,14 @@ #include "ffi.pb.h" #include "livekit/ffi_client.h" #include "livekit/ffi_handle.h" +#include "livekit/track.h" #include "livekit_ffi.h" +#include "room_proto_converter.h" namespace livekit { FfiClient::FfiClient() { - livekit_ffi_initialize(&LivekitFfiCallback, true, LIVEKIT_BUILD_FLAVOR, + livekit_ffi_initialize(&LivekitFfiCallback, false, LIVEKIT_BUILD_FLAVOR, LIVEKIT_BUILD_VERSION_FULL); } @@ -45,7 +47,7 @@ void FfiClient::RemoveListener(ListenerId id) { } proto::FfiResponse -FfiClient::SendRequest(const proto::FfiRequest &request) const { +FfiClient::sendRequest(const proto::FfiRequest &request) const { std::string bytes; if (!request.SerializeToString(&bytes) || bytes.empty()) { throw std::runtime_error("failed to serialize FfiRequest"); @@ -55,8 +57,6 @@ FfiClient::SendRequest(const proto::FfiRequest &request) const { FfiHandleId handle = livekit_ffi_request(reinterpret_cast(bytes.data()), bytes.size(), &resp_ptr, &resp_len); - std::cout << "receive a handle " << handle << std::endl; - if (handle == INVALID_HANDLE) { throw std::runtime_error( "failed to send request, received an invalid handle"); @@ -132,16 +132,16 @@ std::future FfiClient::registerAsync( } // Room APIs Implementation -std::future +std::future FfiClient::connectAsync(const std::string &url, const std::string &token) { - livekit::proto::FfiRequest req; + proto::FfiRequest req; auto *connect = req.mutable_connect(); connect->set_url(url); connect->set_token(token); connect->mutable_options()->set_auto_subscribe(true); - livekit::proto::FfiResponse resp = SendRequest(req); + proto::FfiResponse resp = sendRequest(req); if (!resp.has_connect()) { throw std::runtime_error("FfiResponse missing connect"); } @@ -149,25 +149,23 @@ FfiClient::connectAsync(const std::string &url, const std::string &token) { const AsyncId async_id = resp.connect().async_id(); // Now we register an async op that completes with RoomInfo - return registerAsync( + return registerAsync( // match lambda: is this the connect event with our async_id? - [async_id](const livekit::proto::FfiEvent &event) { + [async_id](const proto::FfiEvent &event) { return event.has_connect() && event.connect().async_id() == async_id; }, // handler lambda: fill the promise with RoomInfo or an exception - [](const livekit::proto::FfiEvent &event, - std::promise &pr) { - const auto &ce = event.connect(); + [](const proto::FfiEvent &event, + std::promise &pr) { + const auto &connectCb = event.connect(); - if (!ce.error().empty()) { + if (!connectCb.error().empty()) { pr.set_exception( - std::make_exception_ptr(std::runtime_error(ce.error()))); + std::make_exception_ptr(std::runtime_error(connectCb.error()))); return; } - // ce.result().room().info() is a const ref, so we copy it - livekit::proto::RoomInfo info = ce.result().room().info(); - pr.set_value(std::move(info)); + pr.set_value(connectCb); }); } @@ -177,7 +175,7 @@ FfiClient::getTrackStatsAsync(uintptr_t track_handle) { proto::FfiRequest req; auto *get_stats_req = req.mutable_get_stats(); get_stats_req->set_track_handle(track_handle); - proto::FfiResponse resp = SendRequest(req); + proto::FfiResponse resp = sendRequest(req); if (!resp.has_get_stats()) { throw std::runtime_error("FfiResponse missing get_stats"); } @@ -213,4 +211,245 @@ FfiClient::getTrackStatsAsync(uintptr_t track_handle) { }); } +// Participant APIs Implementation +std::future +FfiClient::publishTrackAsync(std::uint64_t local_participant_handle, + std::uint64_t track_handle, + const TrackPublishOptions &options) { + proto::FfiRequest req; + auto *msg = req.mutable_publish_track(); + msg->set_local_participant_handle(local_participant_handle); + msg->set_track_handle(track_handle); + auto optionProto = toProto(options); + msg->mutable_options()->CopyFrom(optionProto); + + proto::FfiResponse resp = sendRequest(req); + if (!resp.has_publish_track()) { + throw std::runtime_error("FfiResponse missing publish_track"); + } + const AsyncId async_id = resp.publish_track().async_id(); + return registerAsync( + // Match: is this our PublishTrackCallback? + [async_id](const proto::FfiEvent &event) { + return event.has_publish_track() && + event.publish_track().async_id() == async_id; + }, + // Handler: resolve with publication or throw error + [](const proto::FfiEvent &event, + std::promise &pr) { + const auto &cb = event.publish_track(); + + // Oneof message { string error = 2; OwnedTrackPublication publication = + // 3; } + if (cb.has_error() && !cb.error().empty()) { + pr.set_exception( + std::make_exception_ptr(std::runtime_error(cb.error()))); + return; + } + if (!cb.has_publication()) { + pr.set_exception(std::make_exception_ptr( + std::runtime_error("PublishTrackCallback missing publication"))); + return; + } + + proto::OwnedTrackPublication pub = cb.publication(); + pr.set_value(std::move(pub)); + }); +} + +std::future +FfiClient::unpublishTrackAsync(std::uint64_t local_participant_handle, + const std::string &track_sid, + bool stop_on_unpublish) { + proto::FfiRequest req; + auto *msg = req.mutable_unpublish_track(); + msg->set_local_participant_handle(local_participant_handle); + msg->set_track_sid(track_sid); + msg->set_stop_on_unpublish(stop_on_unpublish); + proto::FfiResponse resp = sendRequest(req); + if (!resp.has_unpublish_track()) { + throw std::runtime_error("FfiResponse missing unpublish_track"); + } + const AsyncId async_id = resp.unpublish_track().async_id(); + return registerAsync( + [async_id](const proto::FfiEvent &event) { + return event.has_unpublish_track() && + event.unpublish_track().async_id() == async_id; + }, + [](const proto::FfiEvent &event, std::promise &pr) { + const auto &cb = event.unpublish_track(); + if (cb.has_error() && !cb.error().empty()) { + pr.set_exception( + std::make_exception_ptr(std::runtime_error(cb.error()))); + return; + } + pr.set_value(); + }); +} + +std::future FfiClient::publishDataAsync( + std::uint64_t local_participant_handle, const std::uint8_t *data_ptr, + std::uint64_t data_len, bool reliable, + const std::vector &destination_identities, + const std::string &topic) { + proto::FfiRequest req; + auto *msg = req.mutable_publish_data(); + msg->set_local_participant_handle(local_participant_handle); + msg->set_data_ptr(reinterpret_cast(data_ptr)); + msg->set_data_len(data_len); + msg->set_reliable(reliable); + msg->set_topic(topic); + for (const auto &id : destination_identities) { + msg->add_destination_identities(id); + } + + proto::FfiResponse resp = sendRequest(req); + if (!resp.has_publish_data()) { + throw std::runtime_error("FfiResponse missing publish_data"); + } + const AsyncId async_id = resp.publish_data().async_id(); + return registerAsync( + [async_id](const proto::FfiEvent &event) { + return event.has_publish_data() && + event.publish_data().async_id() == async_id; + }, + [](const proto::FfiEvent &event, std::promise &pr) { + const auto &cb = event.publish_data(); + if (cb.has_error() && !cb.error().empty()) { + pr.set_exception( + std::make_exception_ptr(std::runtime_error(cb.error()))); + return; + } + pr.set_value(); + }); +} + +std::future FfiClient::publishTranscriptionAsync( + std::uint64_t local_participant_handle, + const std::string &participant_identity, const std::string &track_id, + const std::vector &segments) { + proto::FfiRequest req; + auto *msg = req.mutable_publish_transcription(); + msg->set_local_participant_handle(local_participant_handle); + msg->set_participant_identity(participant_identity); + msg->set_track_id(track_id); + for (const auto &seg : segments) { + auto *dst = msg->add_segments(); + dst->CopyFrom(seg); + } + proto::FfiResponse resp = sendRequest(req); + if (!resp.has_publish_transcription()) { + throw std::runtime_error("FfiResponse missing publish_transcription"); + } + const AsyncId async_id = resp.publish_transcription().async_id(); + return registerAsync( + [async_id](const proto::FfiEvent &event) { + return event.has_publish_transcription() && + event.publish_transcription().async_id() == async_id; + }, + [](const proto::FfiEvent &event, std::promise &pr) { + const auto &cb = event.publish_transcription(); + if (cb.has_error() && !cb.error().empty()) { + pr.set_exception( + std::make_exception_ptr(std::runtime_error(cb.error()))); + return; + } + pr.set_value(); + }); +} + +std::future FfiClient::publishSipDtmfAsync( + std::uint64_t local_participant_handle, std::uint32_t code, + const std::string &digit, + const std::vector &destination_identities) { + proto::FfiRequest req; + auto *msg = req.mutable_publish_sip_dtmf(); + msg->set_local_participant_handle(local_participant_handle); + msg->set_code(code); + msg->set_digit(digit); + for (const auto &id : destination_identities) { + msg->add_destination_identities(id); + } + proto::FfiResponse resp = sendRequest(req); + if (!resp.has_publish_sip_dtmf()) { + throw std::runtime_error("FfiResponse missing publish_sip_dtmf"); + } + const AsyncId async_id = resp.publish_sip_dtmf().async_id(); + return registerAsync( + [async_id](const proto::FfiEvent &event) { + return event.has_publish_sip_dtmf() && + event.publish_sip_dtmf().async_id() == async_id; + }, + [](const proto::FfiEvent &event, std::promise &pr) { + const auto &cb = event.publish_sip_dtmf(); + if (cb.has_error() && !cb.error().empty()) { + pr.set_exception( + std::make_exception_ptr(std::runtime_error(cb.error()))); + return; + } + pr.set_value(); + }); +} + +std::future +FfiClient::setLocalMetadataAsync(std::uint64_t local_participant_handle, + const std::string &metadata) { + proto::FfiRequest req; + auto *msg = req.mutable_set_local_metadata(); + msg->set_local_participant_handle(local_participant_handle); + msg->set_metadata(metadata); + proto::FfiResponse resp = sendRequest(req); + if (!resp.has_set_local_metadata()) { + throw std::runtime_error("FfiResponse missing set_local_metadata"); + } + const AsyncId async_id = resp.set_local_metadata().async_id(); + return registerAsync( + [async_id](const proto::FfiEvent &event) { + return event.has_set_local_metadata() && + event.set_local_metadata().async_id() == async_id; + }, + [](const proto::FfiEvent &event, std::promise &pr) { + const auto &cb = event.set_local_metadata(); + if (cb.has_error() && !cb.error().empty()) { + pr.set_exception( + std::make_exception_ptr(std::runtime_error(cb.error()))); + return; + } + pr.set_value(); + }); +} + +std::future +FfiClient::captureAudioFrameAsync(std::uint64_t source_handle, + const proto::AudioFrameBufferInfo &buffer) { + proto::FfiRequest req; + auto *msg = req.mutable_capture_audio_frame(); + msg->set_source_handle(source_handle); + msg->mutable_buffer()->CopyFrom(buffer); + + proto::FfiResponse resp = sendRequest(req); + if (!resp.has_capture_audio_frame()) { + throw std::runtime_error("FfiResponse missing capture_audio_frame"); + } + + const AsyncId async_id = resp.capture_audio_frame().async_id(); + + return registerAsync( + // match predicate + [async_id](const proto::FfiEvent &event) { + return event.has_capture_audio_frame() && + event.capture_audio_frame().async_id() == async_id; + }, + // completion handler + [](const proto::FfiEvent &event, std::promise &pr) { + const auto &cb = event.capture_audio_frame(); + if (cb.has_error() && !cb.error().empty()) { + pr.set_exception( + std::make_exception_ptr(std::runtime_error(cb.error()))); + return; + } + pr.set_value(); + }); +} + } // namespace livekit diff --git a/src/ffi_handle.cpp b/src/ffi_handle.cpp index d6c61cd..46608da 100644 --- a/src/ffi_handle.cpp +++ b/src/ffi_handle.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2025 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/local_audio_track.cpp b/src/local_audio_track.cpp new file mode 100644 index 0000000..663d0e3 --- /dev/null +++ b/src/local_audio_track.cpp @@ -0,0 +1,81 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "livekit/local_audio_track.h" + +#include "ffi.pb.h" +#include "livekit/audio_source.h" +#include "livekit/ffi_client.h" +#include "track.pb.h" +#include "track_proto_converter.h" + +namespace livekit { + +LocalAudioTrack::LocalAudioTrack(FfiHandle handle, + const proto::OwnedTrack &track) + : Track(std::move(handle), track.info().sid(), track.info().name(), + fromProto(track.info().kind()), + fromProto(track.info().stream_state()), track.info().muted(), + false) {} + +std::shared_ptr LocalAudioTrack::createLocalAudioTrack( + const std::string &name, const std::shared_ptr &source) { + proto::FfiRequest req; + auto *msg = req.mutable_create_audio_track(); + msg->set_name(name); + msg->set_source_handle(static_cast(source->ffi_handle_id())); + + proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + const proto::OwnedTrack &owned = resp.create_audio_track().track(); + FfiHandle handle(static_cast(owned.handle().id())); + return std::make_shared(std::move(handle), owned); +} + +void LocalAudioTrack::mute() { + if (!has_handle()) { + setMuted(true); + return; + } + + proto::FfiRequest req; + auto *msg = req.mutable_local_track_mute(); + msg->set_track_handle(static_cast(ffi_handle_id())); + msg->set_mute(true); + + (void)FfiClient::instance().sendRequest(req); + setMuted(true); +} + +void LocalAudioTrack::unmute() { + if (!has_handle()) { + setMuted(false); + return; + } + + proto::FfiRequest req; + auto *msg = req.mutable_local_track_mute(); + msg->set_track_handle(static_cast(ffi_handle_id())); + msg->set_mute(false); + + (void)FfiClient::instance().sendRequest(req); + setMuted(false); +} + +std::string LocalAudioTrack::to_string() const { + return "rtc.LocalAudioTrack(sid=" + sid() + ", name=" + name() + ")"; +} + +} // namespace livekit \ No newline at end of file diff --git a/src/local_participant.cpp b/src/local_participant.cpp new file mode 100644 index 0000000..2f52573 --- /dev/null +++ b/src/local_participant.cpp @@ -0,0 +1,254 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "livekit/local_participant.h" + +#include "livekit/ffi_client.h" +#include "livekit/ffi_handle.h" +#include "livekit/local_track_publication.h" +#include "livekit/room_delegate.h" +#include "livekit/track.h" + +#include "ffi.pb.h" +#include "participant.pb.h" +#include "room.pb.h" +#include "room_proto_converter.h" +#include "track.pb.h" +#include "track_proto_converter.h" + +#include + +namespace livekit { + +using proto::FfiRequest; +using proto::FfiResponse; + +LocalParticipant::LocalParticipant( + FfiHandle handle, std::string sid, std::string name, std::string identity, + std::string metadata, + std::unordered_map attributes, + ParticipantKind kind, DisconnectReason reason) + : Participant(std::move(handle), std::move(sid), std::move(name), + std::move(identity), std::move(metadata), + std::move(attributes), kind, reason) {} + +void LocalParticipant::publishData( + const std::vector &payload, bool reliable, + const std::vector &destination_identities, + const std::string &topic) { + if (payload.empty()) { + return; + } + + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error( + "LocalParticipant::publishData: invalid FFI handle"); + } + + // Use async FFI API and block until completion. + auto fut = FfiClient::instance().publishDataAsync( + static_cast(handle_id), payload.data(), + static_cast(payload.size()), reliable, + destination_identities, topic); + + fut.get(); +} + +void LocalParticipant::publishDtmf(int code, const std::string &digit) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error( + "LocalParticipant::publishDtmf: invalid FFI handle"); + } + + // TODO, should we take destination as inputs? + std::vector destination_identities; + auto fut = FfiClient::instance().publishSipDtmfAsync( + static_cast(handle_id), static_cast(code), + digit, destination_identities); + + fut.get(); +} + +void LocalParticipant::publishTranscription( + const Transcription &transcription) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error( + "LocalParticipant::publishTranscription: invalid FFI handle"); + } + + std::vector segs; + segs.reserve(transcription.segments.size()); + for (const auto &seg : transcription.segments) { + segs.push_back(toProto(seg)); + } + + // Handle optional participant_identity / track_sid + const std::string participant_identity = + transcription.participant_identity.has_value() + ? *transcription.participant_identity + : std::string{}; + const std::string track_sid = transcription.track_sid.has_value() + ? *transcription.track_sid + : std::string{}; + // Call async API and block until completion + auto fut = FfiClient::instance().publishTranscriptionAsync( + static_cast(handle_id), participant_identity, track_sid, + segs); + fut.get(); +} + +void LocalParticipant::setMetadata(const std::string &metadata) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error( + "LocalParticipant::setMetadata: invalid FFI handle"); + } + auto fut = FfiClient::instance().setLocalMetadataAsync( + static_cast(handle_id), metadata); + + fut.get(); +} + +void LocalParticipant::setName(const std::string &name) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error("LocalParticipant::setName: invalid FFI handle"); + } + + // No async helper defined for SetLocalName in FfiClient yet, so keep using + // the direct request. + FfiRequest req; + auto *msg = req.mutable_set_local_name(); + msg->set_local_participant_handle(static_cast(handle_id)); + msg->set_name(name); + + (void)FfiClient::instance().sendRequest(req); +} + +void LocalParticipant::setAttributes( + const std::unordered_map &attributes) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error( + "LocalParticipant::setAttributes: invalid FFI handle"); + } + + // No async helper defined for SetLocalAttributes in FfiClient yet. + FfiRequest req; + auto *msg = req.mutable_set_local_attributes(); + msg->set_local_participant_handle(static_cast(handle_id)); + + for (const auto &kv : attributes) { + auto *entry = msg->add_attributes(); + entry->set_key(kv.first); + entry->set_value(kv.second); + } + + (void)FfiClient::instance().sendRequest(req); +} + +// ---------------------------------------------------------------------------- +// Subscription permissions +// ---------------------------------------------------------------------------- + +void LocalParticipant::setTrackSubscriptionPermissions( + bool allow_all_participants, + const std::vector &participant_permissions) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error( + "LocalParticipant::setTrackSubscriptionPermissions: invalid FFI " + "handle"); + } + + // No dedicated async helper; do it directly. + FfiRequest req; + auto *msg = req.mutable_set_track_subscription_permissions(); + msg->set_local_participant_handle(static_cast(handle_id)); + msg->set_all_participants_allowed(allow_all_participants); + + for (const auto &perm : participant_permissions) { + auto *p = msg->add_permissions(); + p->CopyFrom(toProto(perm)); + } + + (void)FfiClient::instance().sendRequest(req); +} + +// ---------------------------------------------------------------------------- +// Track publish / unpublish +// ---------------------------------------------------------------------------- + +std::shared_ptr +LocalParticipant::publishTrack(const std::shared_ptr &track, + const TrackPublishOptions &options) { + if (!track) { + throw std::invalid_argument( + "LocalParticipant::publishTrack: track is null"); + } + + auto participant_handle = ffiHandleId(); + if (participant_handle == 0) { + throw std::runtime_error( + "LocalParticipant::publishTrack: invalid participant FFI handle"); + } + + auto track_handle = track->ffi_handle_id(); + if (track_handle == 0) { + throw std::runtime_error( + "LocalParticipant::publishTrack: invalid track FFI handle"); + } + auto fut = FfiClient::instance().publishTrackAsync( + static_cast(participant_handle), + static_cast(track_handle), options); + + // Will throw if the async op fails (error in callback). + proto::OwnedTrackPublication owned_pub = fut.get(); + + // Construct a LocalTrackPublication from the proto publication. + auto publication = std::make_shared(owned_pub); + + // Cache in local map by track SID. + const std::string sid = publication->sid(); + track_publications_[sid] = publication; + + return publication; +} + +void LocalParticipant::unpublishTrack(const std::string &track_sid) { + if (track_sid.empty()) { + return; + } + + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + throw std::runtime_error( + "LocalParticipant::unpublishTrack: invalid FFI handle"); + } + + auto fut = FfiClient::instance().unpublishTrackAsync( + static_cast(handle_id), track_sid, + /*stop_on_unpublish=*/true); + + fut.get(); + + track_publications_.erase(track_sid); +} + +} // namespace livekit diff --git a/src/local_track_publication.cpp b/src/local_track_publication.cpp new file mode 100644 index 0000000..d1f3ece --- /dev/null +++ b/src/local_track_publication.cpp @@ -0,0 +1,39 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "livekit/local_track_publication.h" + +#include "livekit/track.h" +#include "track_proto_converter.h" + +namespace livekit { + +LocalTrackPublication::LocalTrackPublication( + const proto::OwnedTrackPublication &owned) + : TrackPublication( + FfiHandle(owned.handle().id()), owned.info().sid(), + owned.info().name(), fromProto(owned.info().kind()), + fromProto(owned.info().source()), owned.info().simulcasted(), + owned.info().width(), owned.info().height(), owned.info().mime_type(), + owned.info().muted(), fromProto(owned.info().encryption_type()), + convertAudioFeatures(owned.info().audio_features())) {} + +std::shared_ptr LocalTrackPublication::track() const noexcept { + auto base = TrackPublication::track(); + return std::static_pointer_cast(base); +} + +} // namespace livekit diff --git a/src/remote_track_publication.cpp b/src/remote_track_publication.cpp new file mode 100644 index 0000000..6a7f8d0 --- /dev/null +++ b/src/remote_track_publication.cpp @@ -0,0 +1,60 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "livekit/remote_track_publication.h" + +#include "ffi.pb.h" +#include "livekit/ffi_client.h" +#include "livekit/track.h" +#include "track_proto_converter.h" + +namespace livekit { + +RemoteTrackPublication::RemoteTrackPublication( + const proto::OwnedTrackPublication &owned) + : TrackPublication( + FfiHandle(owned.handle().id()), owned.info().sid(), + owned.info().name(), fromProto(owned.info().kind()), + fromProto(owned.info().source()), owned.info().simulcasted(), + owned.info().width(), owned.info().height(), owned.info().mime_type(), + owned.info().muted(), fromProto(owned.info().encryption_type()), + convertAudioFeatures(owned.info().audio_features())) {} + +std::shared_ptr RemoteTrackPublication::track() const noexcept { + auto base = TrackPublication::track(); + return std::static_pointer_cast(base); +} + +void RemoteTrackPublication::setSubscribed(bool subscribed) { + if (ffiHandleId() == 0) { + throw std::runtime_error( + "RemoteTrackPublication::setSubscribed: invalid FFI handle"); + } + + proto::FfiRequest req; + auto *msg = req.mutable_set_subscribed(); + msg->set_subscribe(subscribed); + msg->set_publication_handle(static_cast(ffiHandleId())); + + // Synchronous request; if you add an async version in FfiClient, you can + // wire that up instead. + auto resp = FfiClient::instance().sendRequest(req); + (void)resp; // currently unused, but you can inspect error fields here + + subscribed_ = subscribed; +} + +} // namespace livekit diff --git a/src/room.cpp b/src/room.cpp index e33109e..4cfa3c0 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2025 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,13 @@ #include "livekit/room.h" #include "livekit/ffi_client.h" +#include "livekit/local_participant.h" #include "livekit/room_delegate.h" #include "ffi.pb.h" #include "room.pb.h" -#include "room_event_converter.h" +#include "room_proto_converter.h" +#include "track_proto_converter.h" #include #include @@ -34,6 +36,10 @@ using proto::FfiRequest; using proto::FfiResponse; using proto::RoomOptions; +Room::Room() {} + +Room::~Room() {} + void Room::setDelegate(RoomDelegate *delegate) { std::lock_guard g(lock_); delegate_ = delegate; @@ -51,11 +57,39 @@ bool Room::Connect(const std::string &url, const std::string &token) { } auto fut = FfiClient::instance().connectAsync(url, token); try { - auto info = fut.get(); // fut will throw if it fails to connect to the room + auto connectCb = + fut.get(); // fut will throw if it fails to connect to the room { std::lock_guard g(lock_); connected_ = true; - room_info_ = fromProto(info); + const auto &ownedRoom = connectCb.result().room(); + room_handle_ = std::make_shared(ownedRoom.handle().id()); + room_info_ = fromProto(ownedRoom.info()); + } + // Setup local particpant + { + const auto &owned_local = connectCb.result().local_participant(); + const auto &pinfo = owned_local.info(); + + // Build attributes map + std::unordered_map attrs; + for (const auto &kv : pinfo.attributes()) { + attrs.emplace(kv.first, kv.second); + } + + auto kind = fromProto(pinfo.kind()); + auto reason = toDisconnectReason(pinfo.disconnect_reason()); + + // Participant base stores a weak_ptr, so share the room handle + FfiHandle participant_handle( + static_cast(owned_local.handle().id())); + local_participant_ = std::make_unique( + std::move(participant_handle), pinfo.sid(), pinfo.name(), + pinfo.identity(), pinfo.metadata(), std::move(attrs), kind, reason); + } + // Setup remote particpants + { + // TODO, implement this remote participant feature } return true; } catch (const std::exception &e) { @@ -71,6 +105,11 @@ RoomInfoData Room::room_info() const { return room_info_; } +LocalParticipant *Room::local_participant() const { + std::lock_guard g(lock_); + return local_participant_.get(); +} + void Room::OnEvent(const FfiEvent &event) { // Take a snapshot of the delegate under lock, but do NOT call it under the // lock. diff --git a/src/room_proto_converter.cpp b/src/room_proto_converter.cpp new file mode 100644 index 0000000..1df125c --- /dev/null +++ b/src/room_proto_converter.cpp @@ -0,0 +1,648 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "room_proto_converter.h" + +#include "room.pb.h" + +namespace livekit { + +// --------- enum conversions --------- + +ConnectionQuality toConnectionQuality(proto::ConnectionQuality in) { + switch (in) { + case proto::QUALITY_POOR: + return ConnectionQuality::Poor; + case proto::QUALITY_GOOD: + return ConnectionQuality::Good; + case proto::QUALITY_EXCELLENT: + return ConnectionQuality::Excellent; + case proto::QUALITY_LOST: + return ConnectionQuality::Lost; + default: + return ConnectionQuality::Good; + } +} + +ConnectionState toConnectionState(proto::ConnectionState in) { + switch (in) { + case proto::CONN_DISCONNECTED: + return ConnectionState::Disconnected; + case proto::CONN_CONNECTED: + return ConnectionState::Connected; + case proto::CONN_RECONNECTING: + return ConnectionState::Reconnecting; + default: + return ConnectionState::Disconnected; + } +} + +DataPacketKind toDataPacketKind(proto::DataPacketKind in) { + switch (in) { + case proto::KIND_LOSSY: + return DataPacketKind::Lossy; + case proto::KIND_RELIABLE: + return DataPacketKind::Reliable; + default: + return DataPacketKind::Reliable; + } +} + +EncryptionState toEncryptionState(proto::EncryptionState /*in*/) { + // TODO: fill out once you have the proto::EncryptionState enum + return EncryptionState::Unknown; +} + +DisconnectReason toDisconnectReason(proto::DisconnectReason /*in*/) { + // TODO: map each proto::DisconnectReason to your DisconnectReason enum + return DisconnectReason::Unknown; +} + +// --------- basic helper conversions --------- + +ChatMessageData fromProto(const proto::ChatMessage &in) { + ChatMessageData out; + out.id = in.id(); + out.timestamp = in.timestamp(); + out.message = in.message(); + if (in.has_edit_timestamp()) { + out.edit_timestamp = in.edit_timestamp(); + } + if (in.has_deleted()) { + out.deleted = in.deleted(); + } + if (in.has_generated()) { + out.generated = in.generated(); + } + return out; +} + +UserPacketData fromProto(const proto::UserPacket &in) { + UserPacketData out; + // TODO, double check following code is safe + const auto &buf = in.data().data(); + auto ptr = reinterpret_cast(buf.data_ptr()); + auto len = static_cast(buf.data_len()); + out.data.assign(ptr, ptr + len); + if (in.has_topic()) { + out.topic = in.topic(); + } + return out; +} + +SipDtmfData fromProto(const proto::SipDTMF &in) { + SipDtmfData out; + out.code = in.code(); + if (in.has_digit()) { + out.digit = in.digit(); + } + return out; +} + +RoomInfoData fromProto(const proto::RoomInfo &in) { + RoomInfoData out; + if (in.has_sid()) { + out.sid = in.sid(); + } + out.name = in.name(); + out.metadata = in.metadata(); + out.lossy_dc_buffered_amount_low_threshold = + in.lossy_dc_buffered_amount_low_threshold(); + out.reliable_dc_buffered_amount_low_threshold = + in.reliable_dc_buffered_amount_low_threshold(); + out.empty_timeout = in.empty_timeout(); + out.departure_timeout = in.departure_timeout(); + out.max_participants = in.max_participants(); + out.creation_time = in.creation_time(); + out.num_participants = in.num_participants(); + out.num_publishers = in.num_publishers(); + out.active_recording = in.active_recording(); + return out; +} + +AttributeEntry fromProto(const proto::AttributesEntry &in) { + AttributeEntry a; + a.key = in.key(); + a.value = in.value(); + return a; +} + +DataStreamHeaderData fromProto(const proto::DataStream_Header &in) { + DataStreamHeaderData out; + out.stream_id = in.stream_id(); + out.timestamp = in.timestamp(); + out.mime_type = in.mime_type(); + out.topic = in.topic(); + if (in.has_total_length()) { + out.total_length = in.total_length(); + } + for (const auto &kv : in.attributes()) { + out.attributes.emplace(kv.first, kv.second); + } + + // content_header oneof + switch (in.content_header_case()) { + case proto::DataStream_Header::kTextHeader: { + out.content_type = DataStreamHeaderData::ContentType::Text; + const auto &t = in.text_header(); + out.operation_type = + static_cast(t.operation_type()); + if (t.has_version()) { + out.version = t.version(); + } + if (t.has_reply_to_stream_id()) { + out.reply_to_stream_id = t.reply_to_stream_id(); + } + for (const auto &id : t.attached_stream_ids()) { + out.attached_stream_ids.push_back(id); + } + if (t.has_generated()) { + out.generated = t.generated(); + } + break; + } + case proto::DataStream_Header::kByteHeader: { + out.content_type = DataStreamHeaderData::ContentType::Byte; + const auto &b = in.byte_header(); + out.name = b.name(); + break; + } + case proto::DataStream_Header::CONTENT_HEADER_NOT_SET: + default: + out.content_type = DataStreamHeaderData::ContentType::None; + break; + } + + return out; +} + +DataStreamChunkData fromProto(const proto::DataStream_Chunk &in) { + DataStreamChunkData out; + out.stream_id = in.stream_id(); + out.chunk_index = in.chunk_index(); + out.content.assign(in.content().begin(), in.content().end()); + if (in.has_version()) { + out.version = in.version(); + } + if (in.has_iv()) { + out.iv.assign(in.iv().begin(), in.iv().end()); + } + return out; +} + +DataStreamTrailerData fromProto(const proto::DataStream_Trailer &in) { + DataStreamTrailerData out; + out.stream_id = in.stream_id(); + out.reason = in.reason(); + for (const auto &kv : in.attributes()) { + out.attributes.emplace(kv.first, kv.second); + } + return out; +} + +// --------- event conversions --------- + +ParticipantConnectedEvent +fromProto(const proto::ParticipantConnected & /*in*/) { + ParticipantConnectedEvent ev; + // in.info() is OwnedParticipant; you can fill more fields once you inspect + // it. For now, leave metadata/name/identity as TODO. + // TODO: map in.info().info().identity(), name(), metadata(), etc. + return ev; +} + +ParticipantDisconnectedEvent +fromProto(const proto::ParticipantDisconnected &in) { + ParticipantDisconnectedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.reason = toDisconnectReason(in.disconnect_reason()); + return ev; +} + +LocalTrackPublishedEvent fromProto(const proto::LocalTrackPublished &in) { + LocalTrackPublishedEvent ev; + ev.track_sid = in.track_sid(); + return ev; +} + +LocalTrackUnpublishedEvent fromProto(const proto::LocalTrackUnpublished &in) { + LocalTrackUnpublishedEvent ev; + ev.publication_sid = in.publication_sid(); + return ev; +} + +LocalTrackSubscribedEvent fromProto(const proto::LocalTrackSubscribed &in) { + LocalTrackSubscribedEvent ev; + ev.track_sid = in.track_sid(); + return ev; +} + +TrackPublishedEvent fromProto(const proto::TrackPublished &in) { + TrackPublishedEvent ev; + ev.participant_identity = in.participant_identity(); + // OwnedTrackPublication publication = 2; + // TODO: map publication info once you inspect OwnedTrackPublication + // ev.publication_sid = in.publication().info().sid(); + // ev.track_name = in.publication().info().name(); + // ev.track_kind = ...; + // ev.track_source = ...; + return ev; +} + +TrackUnpublishedEvent fromProto(const proto::TrackUnpublished &in) { + TrackUnpublishedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.publication_sid = in.publication_sid(); + return ev; +} + +TrackSubscribedEvent fromProto(const proto::TrackSubscribed &in) { + TrackSubscribedEvent ev; + ev.participant_identity = in.participant_identity(); + // OwnedTrack track = 2; + // TODO: map track info once you inspect OwnedTrack + // ev.track_sid = in.track().info().sid(); + // ev.track_name = in.track().info().name(); + // ev.track_kind = ...; + // ev.track_source = ...; + return ev; +} + +TrackUnsubscribedEvent fromProto(const proto::TrackUnsubscribed &in) { + TrackUnsubscribedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.track_sid = in.track_sid(); + return ev; +} + +TrackSubscriptionFailedEvent +fromProto(const proto::TrackSubscriptionFailed &in) { + TrackSubscriptionFailedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.track_sid = in.track_sid(); + ev.error = in.error(); + return ev; +} + +TrackMutedEvent fromProto(const proto::TrackMuted &in) { + TrackMutedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.track_sid = in.track_sid(); + return ev; +} + +TrackUnmutedEvent fromProto(const proto::TrackUnmuted &in) { + TrackUnmutedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.track_sid = in.track_sid(); + return ev; +} + +ActiveSpeakersChangedEvent fromProto(const proto::ActiveSpeakersChanged &in) { + ActiveSpeakersChangedEvent ev; + for (const auto &id : in.participant_identities()) { + ev.participant_identities.push_back(id); + } + return ev; +} + +RoomMetadataChangedEvent fromProto(const proto::RoomMetadataChanged &in) { + RoomMetadataChangedEvent ev; + ev.metadata = in.metadata(); + return ev; +} + +RoomSidChangedEvent fromProto(const proto::RoomSidChanged &in) { + RoomSidChangedEvent ev; + ev.sid = in.sid(); + return ev; +} + +ParticipantMetadataChangedEvent +fromProto(const proto::ParticipantMetadataChanged &in) { + ParticipantMetadataChangedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.metadata = in.metadata(); + return ev; +} + +ParticipantNameChangedEvent fromProto(const proto::ParticipantNameChanged &in) { + ParticipantNameChangedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.name = in.name(); + return ev; +} + +ParticipantAttributesChangedEvent +fromProto(const proto::ParticipantAttributesChanged &in) { + ParticipantAttributesChangedEvent ev; + ev.participant_identity = in.participant_identity(); + for (const auto &a : in.attributes()) { + ev.attributes.push_back(fromProto(a)); + } + for (const auto &a : in.changed_attributes()) { + ev.changed_attributes.push_back(fromProto(a)); + } + return ev; +} + +ParticipantEncryptionStatusChangedEvent +fromProto(const proto::ParticipantEncryptionStatusChanged &in) { + ParticipantEncryptionStatusChangedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.is_encrypted = in.is_encrypted(); + return ev; +} + +ConnectionQualityChangedEvent +fromProto(const proto::ConnectionQualityChanged &in) { + ConnectionQualityChangedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.quality = toConnectionQuality(in.quality()); + return ev; +} + +DataPacketReceivedEvent fromProto(const proto::DataPacketReceived &in) { + DataPacketReceivedEvent ev; + ev.kind = toDataPacketKind(in.kind()); + ev.participant_identity = in.participant_identity(); + + switch (in.value_case()) { + case proto::DataPacketReceived::kUser: + ev.user = fromProto(in.user()); + break; + case proto::DataPacketReceived::kSipDtmf: + ev.sip_dtmf = fromProto(in.sip_dtmf()); + break; + case proto::DataPacketReceived::VALUE_NOT_SET: + default: + break; + } + + return ev; +} + +ConnectionStateChangedEvent fromProto(const proto::ConnectionStateChanged &in) { + ConnectionStateChangedEvent ev; + ev.state = toConnectionState(in.state()); + return ev; +} + +DisconnectedEvent fromProto(const proto::Disconnected &in) { + DisconnectedEvent ev; + ev.reason = toDisconnectReason(in.reason()); + return ev; +} + +ReconnectingEvent fromProto(const proto::Reconnecting & /*in*/) { + return ReconnectingEvent{}; +} + +ReconnectedEvent fromProto(const proto::Reconnected & /*in*/) { + return ReconnectedEvent{}; +} + +RoomEosEvent fromProto(const proto::RoomEOS & /*in*/) { return RoomEosEvent{}; } + +DataStreamHeaderReceivedEvent +fromProto(const proto::DataStreamHeaderReceived &in) { + DataStreamHeaderReceivedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.header = fromProto(in.header()); + return ev; +} + +DataStreamChunkReceivedEvent +fromProto(const proto::DataStreamChunkReceived &in) { + DataStreamChunkReceivedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.chunk = fromProto(in.chunk()); + return ev; +} + +DataStreamTrailerReceivedEvent +fromProto(const proto::DataStreamTrailerReceived &in) { + DataStreamTrailerReceivedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.trailer = fromProto(in.trailer()); + return ev; +} + +DataChannelBufferedAmountLowThresholdChangedEvent +fromProto(const proto::DataChannelBufferedAmountLowThresholdChanged &in) { + DataChannelBufferedAmountLowThresholdChangedEvent ev; + ev.kind = toDataPacketKind(in.kind()); + ev.threshold = in.threshold(); + return ev; +} + +ByteStreamOpenedEvent fromProto(const proto::ByteStreamOpened &in) { + ByteStreamOpenedEvent ev; + // TODO: map reader handle once OwnedByteStreamReader is known + // ev.reader_handle = in.reader().handle().id(); + ev.participant_identity = in.participant_identity(); + return ev; +} + +TextStreamOpenedEvent fromProto(const proto::TextStreamOpened &in) { + TextStreamOpenedEvent ev; + // TODO: map reader handle once OwnedTextStreamReader is known + // ev.reader_handle = in.reader().handle().id(); + ev.participant_identity = in.participant_identity(); + return ev; +} + +RoomUpdatedEvent roomUpdatedFromProto(const proto::RoomInfo &in) { + RoomUpdatedEvent ev; + ev.info = fromProto(in); + return ev; +} + +RoomMovedEvent roomMovedFromProto(const proto::RoomInfo &in) { + RoomMovedEvent ev; + ev.info = fromProto(in); + return ev; +} + +ParticipantsUpdatedEvent fromProto(const proto::ParticipantsUpdated &in) { + ParticipantsUpdatedEvent ev; + // We only know that it has ParticipantInfo participants = 1; + // TODO: fill real identities once you inspect proto::ParticipantInfo + for (const auto &p : in.participants()) { + ev.participant_identities.push_back(p.identity()); + } + return ev; +} + +E2eeStateChangedEvent fromProto(const proto::E2eeStateChanged &in) { + E2eeStateChangedEvent ev; + ev.participant_identity = in.participant_identity(); + ev.state = toEncryptionState(in.state()); + return ev; +} + +ChatMessageReceivedEvent fromProto(const proto::ChatMessageReceived &in) { + ChatMessageReceivedEvent ev; + ev.message = fromProto(in.message()); + ev.participant_identity = in.participant_identity(); + return ev; +} + +// ---------------- Room Options ---------------- + +proto::AudioEncoding toProto(const AudioEncodingOptions &in) { + proto::AudioEncoding msg; + msg.set_max_bitrate(in.max_bitrate); + return msg; +} + +AudioEncodingOptions fromProto(const proto::AudioEncoding &in) { + AudioEncodingOptions out; + out.max_bitrate = in.max_bitrate(); + return out; +} + +proto::VideoEncoding toProto(const VideoEncodingOptions &in) { + proto::VideoEncoding msg; + msg.set_max_bitrate(in.max_bitrate); + msg.set_max_framerate(in.max_framerate); + return msg; +} + +VideoEncodingOptions fromProto(const proto::VideoEncoding &in) { + VideoEncodingOptions out; + out.max_bitrate = in.max_bitrate(); + out.max_framerate = in.max_framerate(); + return out; +} + +proto::TrackPublishOptions toProto(const TrackPublishOptions &in) { + proto::TrackPublishOptions msg; + if (in.video_encoding) { + msg.mutable_video_encoding()->CopyFrom(toProto(*in.video_encoding)); + } + if (in.audio_encoding) { + msg.mutable_audio_encoding()->CopyFrom(toProto(*in.audio_encoding)); + } + if (in.video_codec) { + msg.set_video_codec(static_cast(*in.video_codec)); + } + if (in.dtx) { + msg.set_dtx(*in.dtx); + } + if (in.red) { + msg.set_red(*in.red); + } + if (in.simulcast) { + msg.set_simulcast(*in.simulcast); + } + if (in.source) { + msg.set_source(static_cast(*in.source)); + } + if (in.stream) { + msg.set_stream(*in.stream); + } + if (in.preconnect_buffer) { + msg.set_preconnect_buffer(*in.preconnect_buffer); + } + return msg; +} + +TrackPublishOptions fromProto(const proto::TrackPublishOptions &in) { + TrackPublishOptions out; + if (in.has_video_encoding()) { + out.video_encoding = fromProto(in.video_encoding()); + } + if (in.has_audio_encoding()) { + out.audio_encoding = fromProto(in.audio_encoding()); + } + if (in.has_video_codec()) { + out.video_codec = static_cast(in.video_codec()); + } + if (in.has_dtx()) { + out.dtx = in.dtx(); + } + if (in.has_red()) { + out.red = in.red(); + } + if (in.has_simulcast()) { + out.simulcast = in.simulcast(); + } + if (in.has_source()) { + out.source = static_cast(in.source()); + } + if (in.has_stream()) { + out.stream = in.stream(); + } + if (in.has_preconnect_buffer()) { + out.preconnect_buffer = in.preconnect_buffer(); + } + return out; +} + +proto::TranscriptionSegment toProto(const TranscriptionSegment &in) { + proto::TranscriptionSegment msg; + msg.set_id(in.id); + msg.set_text(in.text); + msg.set_start_time(in.start_time); + msg.set_end_time(in.end_time); + msg.set_final(in.final); + msg.set_language(in.language); + return msg; +} + +TranscriptionSegment fromProto(const proto::TranscriptionSegment &in) { + TranscriptionSegment out; + out.id = in.id(); + out.text = in.text(); + out.start_time = in.start_time(); + out.end_time = in.end_time(); + out.final = in.final(); + out.language = in.language(); + return out; +} + +proto::TranscriptionReceived toProto(const Transcription &in) { + proto::TranscriptionReceived msg; + if (in.participant_identity) { + msg.set_participant_identity(*in.participant_identity); + } + if (in.track_sid) { + msg.set_track_sid(*in.track_sid); + } + for (const auto &seg : in.segments) { + auto *pseg = msg.add_segments(); + pseg->CopyFrom(toProto(seg)); + } + return msg; +} + +Transcription fromProto(const proto::TranscriptionReceived &in) { + Transcription out; + if (in.has_participant_identity()) { + out.participant_identity = in.participant_identity(); + } + if (in.has_track_sid()) { + out.track_sid = in.track_sid(); + } + out.segments.reserve(in.segments_size()); + for (const auto &pseg : in.segments()) { + out.segments.push_back(fromProto(pseg)); + } + return out; +} + +} // namespace livekit diff --git a/src/room_proto_converter.h b/src/room_proto_converter.h new file mode 100644 index 0000000..eafba5a --- /dev/null +++ b/src/room_proto_converter.h @@ -0,0 +1,125 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "livekit/room_delegate.h" +#include "room.pb.h" + +namespace livekit { + +// --------- basic helper conversions --------- + +ConnectionQuality toConnectionQuality(proto::ConnectionQuality in); +ConnectionState toConnectionState(proto::ConnectionState in); +DataPacketKind toDataPacketKind(proto::DataPacketKind in); +EncryptionState toEncryptionState(proto::EncryptionState in); +DisconnectReason toDisconnectReason(proto::DisconnectReason in); + +ChatMessageData fromProto(const proto::ChatMessage &in); +UserPacketData fromProto(const proto::UserPacket &in); +SipDtmfData fromProto(const proto::SipDTMF &in); +RoomInfoData fromProto(const proto::RoomInfo &in); +AttributeEntry fromProto(const proto::AttributesEntry &in); + +DataStreamHeaderData fromProto(const proto::DataStream_Header &in); +DataStreamChunkData fromProto(const proto::DataStream_Chunk &in); +DataStreamTrailerData fromProto(const proto::DataStream_Trailer &in); + +// --------- event conversions (RoomEvent.oneof message) --------- + +ParticipantConnectedEvent fromProto(const proto::ParticipantConnected &in); +ParticipantDisconnectedEvent +fromProto(const proto::ParticipantDisconnected &in); + +LocalTrackPublishedEvent fromProto(const proto::LocalTrackPublished &in); +LocalTrackUnpublishedEvent fromProto(const proto::LocalTrackUnpublished &in); +LocalTrackSubscribedEvent fromProto(const proto::LocalTrackSubscribed &in); + +TrackPublishedEvent fromProto(const proto::TrackPublished &in); +TrackUnpublishedEvent fromProto(const proto::TrackUnpublished &in); +TrackSubscribedEvent fromProto(const proto::TrackSubscribed &in); +TrackUnsubscribedEvent fromProto(const proto::TrackUnsubscribed &in); +TrackSubscriptionFailedEvent +fromProto(const proto::TrackSubscriptionFailed &in); +TrackMutedEvent fromProto(const proto::TrackMuted &in); +TrackUnmutedEvent fromProto(const proto::TrackUnmuted &in); + +ActiveSpeakersChangedEvent fromProto(const proto::ActiveSpeakersChanged &in); + +RoomMetadataChangedEvent fromProto(const proto::RoomMetadataChanged &in); +RoomSidChangedEvent fromProto(const proto::RoomSidChanged &in); + +ParticipantMetadataChangedEvent +fromProto(const proto::ParticipantMetadataChanged &in); +ParticipantNameChangedEvent fromProto(const proto::ParticipantNameChanged &in); +ParticipantAttributesChangedEvent +fromProto(const proto::ParticipantAttributesChanged &in); +ParticipantEncryptionStatusChangedEvent +fromProto(const proto::ParticipantEncryptionStatusChanged &in); + +ConnectionQualityChangedEvent +fromProto(const proto::ConnectionQualityChanged &in); + +DataPacketReceivedEvent fromProto(const proto::DataPacketReceived &in); + +ConnectionStateChangedEvent fromProto(const proto::ConnectionStateChanged &in); +DisconnectedEvent fromProto(const proto::Disconnected &in); +ReconnectingEvent fromProto(const proto::Reconnecting &in); +ReconnectedEvent fromProto(const proto::Reconnected &in); +RoomEosEvent fromProto(const proto::RoomEOS &in); + +DataStreamHeaderReceivedEvent +fromProto(const proto::DataStreamHeaderReceived &in); +DataStreamChunkReceivedEvent +fromProto(const proto::DataStreamChunkReceived &in); +DataStreamTrailerReceivedEvent +fromProto(const proto::DataStreamTrailerReceived &in); + +DataChannelBufferedAmountLowThresholdChangedEvent +fromProto(const proto::DataChannelBufferedAmountLowThresholdChanged &in); + +ByteStreamOpenedEvent fromProto(const proto::ByteStreamOpened &in); +TextStreamOpenedEvent fromProto(const proto::TextStreamOpened &in); + +RoomUpdatedEvent +roomUpdatedFromProto(const proto::RoomInfo &in); // room_updated +RoomMovedEvent roomMovedFromProto(const proto::RoomInfo &in); // moved + +ParticipantsUpdatedEvent fromProto(const proto::ParticipantsUpdated &in); +E2eeStateChangedEvent fromProto(const proto::E2eeStateChanged &in); +ChatMessageReceivedEvent fromProto(const proto::ChatMessageReceived &in); + +// --------- room options conversions --------- + +proto::AudioEncoding toProto(const AudioEncodingOptions &in); +AudioEncodingOptions fromProto(const proto::AudioEncoding &in); + +proto::VideoEncoding toProto(const VideoEncodingOptions &in); +VideoEncodingOptions fromProto(const proto::VideoEncoding &in); + +proto::TrackPublishOptions toProto(const TrackPublishOptions &in); +TrackPublishOptions fromProto(const proto::TrackPublishOptions &in); + +// --------- room transcription conversions --------- + +proto::TranscriptionSegment toProto(const TranscriptionSegment &in); +TranscriptionSegment fromProto(const proto::TranscriptionSegment &in); + +proto::TranscriptionReceived toProto(const Transcription &in); +Transcription fromProto(const proto::TranscriptionReceived &in); + +} // namespace livekit diff --git a/src/stats.cpp b/src/stats.cpp index 8f3ba27..c3cea3f 100644 --- a/src/stats.cpp +++ b/src/stats.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2025 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/track.cpp b/src/track.cpp index f817001..f8c26a6 100644 --- a/src/track.cpp +++ b/src/track.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiveKit + * Copyright 2025 LiveKit * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ namespace livekit { -Track::Track(std::weak_ptr handle, std::string sid, std::string name, +Track::Track(FfiHandle handle, std::string sid, std::string name, TrackKind kind, StreamState state, bool muted, bool remote) : handle_(std::move(handle)), sid_(std::move(sid)), name_(std::move(name)), kind_(kind), state_(state), muted_(muted), remote_(remote) {} diff --git a/src/track_proto_converter.cpp b/src/track_proto_converter.cpp new file mode 100644 index 0000000..f84658c --- /dev/null +++ b/src/track_proto_converter.cpp @@ -0,0 +1,158 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "track_proto_converter.h" + +#include + +namespace livekit { + +proto::ParticipantTrackPermission +toProto(const ParticipantTrackPermission &in) { + proto::ParticipantTrackPermission out; + out.set_participant_identity(in.participant_identity); + if (in.allow_all.has_value()) { + out.set_allow_all(*in.allow_all); + } + for (const auto &sid : in.allowed_track_sids) { + out.add_allowed_track_sids(sid); + } + return out; +} + +ParticipantTrackPermission +fromProto(const proto::ParticipantTrackPermission &in) { + ParticipantTrackPermission out; + out.participant_identity = in.participant_identity(); + if (in.has_allow_all()) { + out.allow_all = in.allow_all(); + } else { + out.allow_all = std::nullopt; + } + out.allowed_track_sids.reserve(in.allowed_track_sids_size()); + for (const auto &sid : in.allowed_track_sids()) { + out.allowed_track_sids.push_back(sid); + } + return out; +} + +TrackKind fromProto(proto::TrackKind in) { + switch (in) { + case proto::TrackKind::KIND_AUDIO: + return TrackKind::KIND_AUDIO; + case proto::TrackKind::KIND_VIDEO: + return TrackKind::KIND_VIDEO; + case proto::TrackKind::KIND_UNKNOWN: + return TrackKind::KIND_UNKNOWN; + default: + return TrackKind::KIND_UNKNOWN; + } +} + +StreamState fromProto(proto::StreamState in) { + switch (in) { + case proto::StreamState::STATE_ACTIVE: + return StreamState::STATE_ACTIVE; + case proto::StreamState::STATE_PAUSED: + return StreamState::STATE_PAUSED; + case proto::StreamState::STATE_UNKNOWN: + return StreamState::STATE_UNKNOWN; + default: + return StreamState::STATE_UNKNOWN; + } +} + +TrackSource fromProto(proto::TrackSource in) { + switch (in) { + case proto::TrackSource::SOURCE_CAMERA: + return TrackSource::SOURCE_CAMERA; + case proto::TrackSource::SOURCE_MICROPHONE: + return TrackSource::SOURCE_MICROPHONE; + case proto::TrackSource::SOURCE_SCREENSHARE: + return TrackSource::SOURCE_SCREENSHARE; + case proto::TrackSource::SOURCE_SCREENSHARE_AUDIO: + return TrackSource::SOURCE_SCREENSHARE_AUDIO; + case proto::TrackSource::SOURCE_UNKNOWN: + return TrackSource::SOURCE_UNKNOWN; + default: + return TrackSource::SOURCE_UNKNOWN; + } +} + +AudioTrackFeature fromProto(proto::AudioTrackFeature in) { + switch (in) { + case proto::TF_STEREO: + return AudioTrackFeature::TF_STEREO; + case proto::TF_NO_DTX: + return AudioTrackFeature::TF_NO_DTX; + case proto::TF_AUTO_GAIN_CONTROL: + return AudioTrackFeature::TF_AUTO_GAIN_CONTROL; + case proto::TF_ECHO_CANCELLATION: + return AudioTrackFeature::TF_ECHO_CANCELLATION; + case proto::TF_NOISE_SUPPRESSION: + return AudioTrackFeature::TF_NOISE_SUPPRESSION; + case proto::TF_ENHANCED_NOISE_CANCELLATION: + return AudioTrackFeature::TF_ENHANCED_NOISE_CANCELLATION; + case proto::TF_PRECONNECT_BUFFER: + return AudioTrackFeature::TF_PRECONNECT_BUFFER; + default: + // Defensive fallback – pick something valid instead of UB. + return AudioTrackFeature::TF_STEREO; + } +} + +std::vector +convertAudioFeatures(const google::protobuf::RepeatedField &features) { + std::vector out; + out.reserve(features.size()); + for (int v : features) { + out.push_back(fromProto(static_cast(v))); + } + return out; +} + +ParticipantKind fromProto(proto::ParticipantKind in) { + switch (in) { + case proto::ParticipantKind::PARTICIPANT_KIND_STANDARD: + return ParticipantKind::Standard; + case proto::ParticipantKind::PARTICIPANT_KIND_INGRESS: + return ParticipantKind::Ingress; + case proto::ParticipantKind::PARTICIPANT_KIND_EGRESS: + return ParticipantKind::Egress; + case proto::ParticipantKind::PARTICIPANT_KIND_SIP: + return ParticipantKind::Sip; + case proto::ParticipantKind::PARTICIPANT_KIND_AGENT: + return ParticipantKind::Agent; + default: + return ParticipantKind::Standard; + } +} + +EncryptionType fromProto(proto::EncryptionType in) { + switch (in) { + case proto::NONE: + return EncryptionType::NONE; + case proto::GCM: + return EncryptionType::GCM; + case proto::CUSTOM: + return EncryptionType::CUSTOM; + default: + // Defensive fallback + return EncryptionType::NONE; + } +} + +} // namespace livekit \ No newline at end of file diff --git a/src/track_proto_converter.h b/src/track_proto_converter.h new file mode 100644 index 0000000..a1192a1 --- /dev/null +++ b/src/track_proto_converter.h @@ -0,0 +1,41 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "livekit/participant.h" +#include "livekit/track.h" +#include "livekit/track_publication.h" +#include "participant.pb.h" +#include "track.pb.h" + +namespace livekit { + +TrackKind fromProto(proto::TrackKind in); +StreamState fromProto(proto::StreamState in); +TrackSource fromProto(proto::TrackSource in); +AudioTrackFeature fromProto(proto::AudioTrackFeature in); +std::vector +convertAudioFeatures(const google::protobuf::RepeatedField &features); + +// Participant Utils +ParticipantKind fromProto(proto::ParticipantKind kind); +proto::ParticipantTrackPermission toProto(const ParticipantTrackPermission &in); +ParticipantTrackPermission +fromProto(const proto::ParticipantTrackPermission &in); + +// Track Publication Utils. +EncryptionType fromProto(proto::EncryptionType in); + +} // namespace livekit \ No newline at end of file diff --git a/src/track_publication.cpp b/src/track_publication.cpp new file mode 100644 index 0000000..8722a5b --- /dev/null +++ b/src/track_publication.cpp @@ -0,0 +1,33 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "livekit/track_publication.h" + +namespace livekit { + +TrackPublication::TrackPublication( + FfiHandle handle, std::string sid, std::string name, TrackKind kind, + TrackSource source, bool simulcasted, std::uint32_t width, + std::uint32_t height, std::string mime_type, bool muted, + EncryptionType encryption_type, + std::vector audio_features) + : handle_(std::move(handle)), sid_(std::move(sid)), name_(std::move(name)), + kind_(kind), source_(source), simulcasted_(simulcasted), width_(width), + height_(height), mime_type_(std::move(mime_type)), muted_(muted), + encryption_type_(encryption_type), + audio_features_(std::move(audio_features)) {} + +} // namespace livekit