diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index 687b8409..56f15908 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -445,11 +445,16 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # ---------- Publish Docs Job ---------- - publish-docs: - name: Publish Documentation + # Build docs for validation and retain them as a workflow artifact. Publishing + # is triggered separately by the GitHub release's authoritative state. + generate-docs: + name: Generate Documentation needs: release - uses: ./.github/workflows/publish-docs.yml + permissions: + contents: read + actions: read + uses: ./.github/workflows/generate-docs.yml with: version: ${{ needs.release.outputs.version }} - secrets: inherit + upload_artifact: true + artifact_name: livekit-cpp-docs-${{ github.run_id }} diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 7a030c96..04d087ca 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -1,18 +1,10 @@ name: Publish docs on: - workflow_dispatch: - inputs: - version: - description: 'Documentation version (e.g. v0.1.0)' - required: false - type: string - workflow_call: - inputs: - version: - description: 'Documentation version (e.g. v0.1.0)' - required: false - type: string + release: + # "published" covers newly published stable releases and prereleases. + # "released" also catches promotion of an existing prerelease to stable. + types: [published, released] permissions: contents: read @@ -23,7 +15,7 @@ jobs: name: Validate (build docs) uses: ./.github/workflows/generate-docs.yml with: - version: ${{ inputs.version || github.event.inputs.version || '' }} + version: ${{ github.event.release.tag_name }} upload_artifact: true # Suffix with run_id so concurrent publish runs cannot collide on the # artifact namespace within the same repository. @@ -32,6 +24,9 @@ jobs: publish: name: Publish (S3 + CloudFront) needs: validate + # The GitHub release is the source of truth. Drafts do not emit these + # events until published; prereleases validate docs but never deploy them. + if: github.event.release.draft == false && github.event.release.prerelease == false runs-on: ubuntu-latest steps: - name: Download docs artifact diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c53aaa8f..e08b0a57 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -219,9 +219,12 @@ jobs: - name: Start livekit-server if: matrix.e2e-testing id: livekit_server - uses: livekit/dev-server-action@61e2b4dcb170dd3591e0c9b0db3c3fe5db93b500 + uses: livekit/dev-server-action@6562d7d9343e46c26ead1223151f64f00e4fc37f # v1.1.0 with: github-token: ${{ github.token }} + version: v1.13.3 + config: | + enable_participant_data_blob: true # Needed by token helper script - name: Install livekit-cli diff --git a/CMakeLists.txt b/CMakeLists.txt index e4aeaeb1..c45b90f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -378,6 +378,7 @@ add_library(livekit SHARED src/data_track_frame.cpp src/data_stream.cpp src/data_track_error.cpp + src/data_track_schema.cpp src/data_track_stream.cpp src/e2ee.cpp src/ffi_handle.cpp diff --git a/client-sdk-rust b/client-sdk-rust index da3ee007..02dcf51d 160000 --- a/client-sdk-rust +++ b/client-sdk-rust @@ -1 +1 @@ -Subproject commit da3ee007c044e31422ce3412346c3d6c93fea0b4 +Subproject commit 02dcf51db3f24ce0fbe40e85930bd74cd6857c5a diff --git a/include/livekit/data_track_info.h b/include/livekit/data_track_info.h index 27310269..c748c295 100644 --- a/include/livekit/data_track_info.h +++ b/include/livekit/data_track_info.h @@ -16,8 +16,11 @@ #pragma once +#include #include +#include "livekit/data_track_schema.h" + namespace livekit { /// Metadata about a published data track. @@ -25,7 +28,7 @@ namespace livekit { /// Unlike audio/video tracks, data tracks are not part of the Track class /// hierarchy. They carry their own lightweight info struct. struct DataTrackInfo { - /// Publisher-assigned track name (unique per publisher). + /// @brief Publisher-assigned track name, unique per publisher. std::string name; /// SFU-assigned track identifier. @@ -33,6 +36,12 @@ struct DataTrackInfo { /// Whether frames on this track use end-to-end encryption. bool uses_e2ee = false; + + /// Schema associated with frames sent on the track, if any. + std::optional schema; + + /// Encoding of frames sent on the track, if specified. + std::optional frame_encoding; }; } // namespace livekit diff --git a/include/livekit/data_track_options.h b/include/livekit/data_track_options.h new file mode 100644 index 00000000..ff935ddd --- /dev/null +++ b/include/livekit/data_track_options.h @@ -0,0 +1,43 @@ +/* + * Copyright 2026 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 "livekit/data_track_schema.h" + +namespace livekit { + +/// Options for publishing a data track. +/// +/// The schema and frame encoding are optional metadata advertised to +/// subscribers; they are surfaced on the subscriber side via DataTrackInfo. +struct DataTrackPublishOptions { + /// Track name used to identify the track to other participants. + /// + /// Must not be empty and must be unique per publisher. + std::string name; + + /// Schema describing frames sent on the track, if any. + std::optional schema; + + /// Encoding of frames sent on the track, if any. + std::optional frame_encoding; +}; + +} // namespace livekit diff --git a/include/livekit/data_track_schema.h b/include/livekit/data_track_schema.h new file mode 100644 index 00000000..47a615fe --- /dev/null +++ b/include/livekit/data_track_schema.h @@ -0,0 +1,206 @@ +/* + * Copyright 2026 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 + +namespace livekit { + +/// Encoding used to interpret a data track schema definition. +/// +/// Identifies the interface definition language the schema is written in (for +/// example, a `.proto` file for @ref DataTrackSchemaEncoding::Protobuf), which +/// in turn dictates the wire format of the frames the schema describes. +/// +/// Almost all schemas use a well-known encoding, which converts implicitly: +/// @code +/// DataTrackSchemaEncoding encoding = DataTrackSchemaEncoding::Protobuf; +/// @endcode +/// For the uncommon case of an encoding outside the well-known set, use +/// @ref DataTrackSchemaEncoding::custom. +class DataTrackSchemaEncoding { +public: + /// Well-known schema encodings. + enum WellKnown { + /// Protocol Buffer IDL. + Protobuf, + /// FlatBuffer IDL. + Flatbuffer, + /// ROS 1 Message. + Ros1Msg, + /// ROS 2 Message. + Ros2Msg, + /// ROS 2 IDL. + Ros2Idl, + /// OMG IDL. + OmgIdl, + /// JSON Schema. + JsonSchema, + /// Another well-known encoding not known to this client version. + Other, + }; + + /// Construct a well-known encoding. + /// + /// The constructor is intentionally implicit for the common well-known case. + DataTrackSchemaEncoding(WellKnown wellKnown) : well_known_(wellKnown) {} + + /// Construct a custom, application-defined encoding. + /// + /// Prefer a well-known encoding wherever one applies. The identifier must be + /// non-empty and no longer than 25 characters. + /// + /// @param identifier Custom encoding identifier. + /// @return Custom schema encoding. + static DataTrackSchemaEncoding custom(std::string identifier) { + DataTrackSchemaEncoding encoding; + encoding.custom_ = std::move(identifier); + return encoding; + } + + /// Check whether this is a custom encoding. + /// + /// @return true if this is a custom encoding rather than a well-known one. + bool isCustom() const { return !custom_.empty(); } + + /// Get the well-known encoding. + /// + /// @return The well-known encoding. Only meaningful when @ref isCustom is false. + WellKnown wellKnown() const { return well_known_; } + + /// Get the custom identifier. + /// + /// @return The custom identifier. Empty when @ref isCustom is false. + const std::string& customIdentifier() const { return custom_; } + +private: + DataTrackSchemaEncoding() = default; + + WellKnown well_known_ = Other; + std::string custom_; +}; + +inline bool operator==(const DataTrackSchemaEncoding& a, const DataTrackSchemaEncoding& b) { + if (a.isCustom() || b.isCustom()) { + return a.customIdentifier() == b.customIdentifier(); + } + return a.wellKnown() == b.wellKnown(); +} +inline bool operator!=(const DataTrackSchemaEncoding& a, const DataTrackSchemaEncoding& b) { return !(a == b); } + +/// Encoding used for frames sent on a data track. +/// +/// The serialization format of the frame bytes (for example, +/// @ref DataTrackFrameEncoding::Protobuf); the structure of those bytes is +/// described by a schema (see @ref DataTrackSchemaEncoding). +/// +/// Almost all tracks use a well-known encoding, which converts implicitly: +/// @code +/// options.frame_encoding = DataTrackFrameEncoding::Json; +/// @endcode +/// For the uncommon case of an encoding outside the well-known set, use +/// @ref DataTrackFrameEncoding::custom. +class DataTrackFrameEncoding { +public: + /// Well-known frame encodings. + enum WellKnown { + /// ROS 1, described by a Ros1Msg schema. + Ros1, + /// CDR, described by a Ros2Msg, Ros2Idl, or OmgIdl schema. + Cdr, + /// Protocol Buffer, described by a Protobuf schema. + Protobuf, + /// FlatBuffer, described by a Flatbuffer schema. + Flatbuffer, + /// CBOR, self-describing. + Cbor, + /// MessagePack, self-describing. + Msgpack, + /// JSON, self-describing or described by a JsonSchema schema. + Json, + /// Another well-known encoding not known to this client version. + Other, + }; + + /// Construct a well-known encoding. + /// + /// The constructor is intentionally implicit for the common well-known case. + DataTrackFrameEncoding(WellKnown wellKnown) : well_known_(wellKnown) {} + + /// Construct a custom, application-defined encoding. + /// + /// Prefer a well-known encoding wherever one applies. The identifier must be + /// non-empty and no longer than 25 characters. + /// + /// @param identifier Custom encoding identifier. + /// @return Custom frame encoding. + static DataTrackFrameEncoding custom(std::string identifier) { + DataTrackFrameEncoding encoding; + encoding.custom_ = std::move(identifier); + return encoding; + } + + /// Check whether this is a custom encoding. + /// + /// @return true if this is a custom encoding rather than a well-known one. + bool isCustom() const { return !custom_.empty(); } + + /// Get the well-known encoding. + /// + /// @return The well-known encoding. Only meaningful when @ref isCustom is false. + WellKnown wellKnown() const { return well_known_; } + + /// Get the custom identifier. + /// + /// @return The custom identifier. Empty when @ref isCustom is false. + const std::string& customIdentifier() const { return custom_; } + +private: + DataTrackFrameEncoding() = default; + + WellKnown well_known_ = Other; + std::string custom_; +}; + +inline bool operator==(const DataTrackFrameEncoding& a, const DataTrackFrameEncoding& b) { + if (a.isCustom() || b.isCustom()) { + return a.customIdentifier() == b.customIdentifier(); + } + return a.wellKnown() == b.wellKnown(); +} +inline bool operator!=(const DataTrackFrameEncoding& a, const DataTrackFrameEncoding& b) { return !(a == b); } + +/// Uniquely identifies a data track schema. +/// +/// A compound identifier with two components: a name and an encoding. Two IDs +/// are equal only if both components match; the same name with a different +/// encoding refers to a distinct schema. +struct DataTrackSchemaId { + /// Name component of the schema identifier. + std::string name; + + /// Encoding of the schema definition. + DataTrackSchemaEncoding encoding = DataTrackSchemaEncoding::Other; +}; + +inline bool operator==(const DataTrackSchemaId& a, const DataTrackSchemaId& b) { + return a.name == b.name && a.encoding == b.encoding; +} +inline bool operator!=(const DataTrackSchemaId& a, const DataTrackSchemaId& b) { return !(a == b); } + +} // namespace livekit diff --git a/include/livekit/local_participant.h b/include/livekit/local_participant.h index 9369a914..bf08f00a 100644 --- a/include/livekit/local_participant.h +++ b/include/livekit/local_participant.h @@ -22,15 +22,20 @@ #include #include #include +#include #include #include #include +#include "livekit/data_track_error.h" +#include "livekit/data_track_options.h" +#include "livekit/data_track_schema.h" #include "livekit/ffi_handle.h" #include "livekit/local_audio_track.h" #include "livekit/local_data_track.h" #include "livekit/local_video_track.h" #include "livekit/participant.h" +#include "livekit/result.h" #include "livekit/room_event_types.h" #include "livekit/rpc_error.h" #include "livekit/visibility.h" @@ -172,6 +177,18 @@ class LIVEKIT_API LocalParticipant : public Participant { /// publication failed. Result, PublishDataTrackError> publishDataTrack(const std::string& name); + /// Publish a data track to the room with explicit options. + /// + /// Like publishDataTrack(const std::string&), but also lets the publisher + /// advertise an optional schema and frame encoding as metadata. These are + /// surfaced to subscribers via the remote DataTrackInfo. + /// + /// @param options Track name plus optional schema / frame encoding. + /// @return The published track on success, or a typed error describing why + /// publication failed. + Result, PublishDataTrackError> publishDataTrack( + const DataTrackPublishOptions& options); + /// Unpublish a data track from the room. /// /// Delegates to LocalDataTrack::unpublishDataTrack(). After this call, @@ -180,6 +197,38 @@ class LIVEKIT_API LocalParticipant : public Participant { /// @param track The data track to unpublish. Null is ignored. void unpublishDataTrack(const std::shared_ptr& track); + /// Store the definition of a data track schema. + /// + /// Called by a publisher to make a schema available to subscribers, who can + /// later look up its definition via getSchema(). Define a schema before + /// publishing any data track that references it, so subscribers can resolve + /// the schema by its ID. + /// + /// A schema can only be defined once. Attempting to redefine an existing + /// schema fails. + /// + /// @param id Identifies the schema (name and encoding). + /// @param definition The schema definition, stored as-is. It is neither + /// parsed nor validated against its encoding, so the + /// caller is responsible for ensuring it is well-formed. + /// + /// @return @c true if the schema was defined; otherwise @c false. Failure + /// details are written to the SDK log. + [[nodiscard]] bool defineSchema(const DataTrackSchemaId& id, const std::string& definition); + + /// Retrieve the definition for a data track schema. + /// + /// Called by a subscriber that wants to inspect the schema a participant + /// defined (see defineSchema()) for a data track it is publishing. + /// + /// @param id Identifies the schema to retrieve. + /// @param participant_identity Identity of the participant that defined the + /// schema. + /// @return The schema definition on success, or @c std::nullopt on failure. + /// Failure details are written to the SDK log. + [[nodiscard]] std::optional getSchema(const DataTrackSchemaId& id, + const std::string& participant_identity); + /// Initiate an RPC call to a remote participant. /// /// @param destination_identity Identity of the destination participant. diff --git a/src/data_track_proto_converter.h b/src/data_track_proto_converter.h new file mode 100644 index 00000000..584c7f96 --- /dev/null +++ b/src/data_track_proto_converter.h @@ -0,0 +1,38 @@ +/* + * Copyright 2026 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 "data_track.pb.h" +#include "livekit/data_track_info.h" +#include "livekit/data_track_schema.h" +#include "livekit/visibility.h" + +namespace livekit { + +LIVEKIT_INTERNAL_API proto::DataTrackSchemaEncoding toProto(const DataTrackSchemaEncoding& in); +LIVEKIT_INTERNAL_API DataTrackSchemaEncoding fromProto(const proto::DataTrackSchemaEncoding& in); + +LIVEKIT_INTERNAL_API proto::DataTrackFrameEncoding toProto(const DataTrackFrameEncoding& in); +LIVEKIT_INTERNAL_API DataTrackFrameEncoding fromProto(const proto::DataTrackFrameEncoding& in); + +LIVEKIT_INTERNAL_API proto::DataTrackSchemaId toProto(const DataTrackSchemaId& in); +LIVEKIT_INTERNAL_API DataTrackSchemaId fromProto(const proto::DataTrackSchemaId& in); + +// Converts an FFI data track info message into the public DataTrackInfo struct. +LIVEKIT_INTERNAL_API DataTrackInfo fromProto(const proto::DataTrackInfo& in); + +} // namespace livekit diff --git a/src/data_track_schema.cpp b/src/data_track_schema.cpp new file mode 100644 index 00000000..7a65e147 --- /dev/null +++ b/src/data_track_schema.cpp @@ -0,0 +1,167 @@ +/* + * Copyright 2026 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 "data_track_proto_converter.h" + +namespace livekit { + +proto::DataTrackSchemaEncoding toProto(const DataTrackSchemaEncoding& in) { + proto::DataTrackSchemaEncoding out; + if (in.isCustom()) { + out.set_custom(in.customIdentifier()); + return out; + } + switch (in.wellKnown()) { + case DataTrackSchemaEncoding::Protobuf: + out.set_well_known(proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF); + break; + case DataTrackSchemaEncoding::Flatbuffer: + out.set_well_known(proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER); + break; + case DataTrackSchemaEncoding::Ros1Msg: + out.set_well_known(proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG); + break; + case DataTrackSchemaEncoding::Ros2Msg: + out.set_well_known(proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG); + break; + case DataTrackSchemaEncoding::Ros2Idl: + out.set_well_known(proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL); + break; + case DataTrackSchemaEncoding::OmgIdl: + out.set_well_known(proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL); + break; + case DataTrackSchemaEncoding::JsonSchema: + out.set_well_known(proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA); + break; + case DataTrackSchemaEncoding::Other: + out.set_well_known(proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED); + break; + } + return out; +} + +DataTrackSchemaEncoding fromProto(const proto::DataTrackSchemaEncoding& in) { + if (in.encoding_case() == proto::DataTrackSchemaEncoding::kCustom) { + return DataTrackSchemaEncoding::custom(in.custom()); + } + switch (in.well_known()) { + case proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF: + return DataTrackSchemaEncoding::Protobuf; + case proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER: + return DataTrackSchemaEncoding::Flatbuffer; + case proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG: + return DataTrackSchemaEncoding::Ros1Msg; + case proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG: + return DataTrackSchemaEncoding::Ros2Msg; + case proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL: + return DataTrackSchemaEncoding::Ros2Idl; + case proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL: + return DataTrackSchemaEncoding::OmgIdl; + case proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA: + return DataTrackSchemaEncoding::JsonSchema; + default: + return DataTrackSchemaEncoding::Other; + } +} + +proto::DataTrackFrameEncoding toProto(const DataTrackFrameEncoding& in) { + proto::DataTrackFrameEncoding out; + if (in.isCustom()) { + out.set_custom(in.customIdentifier()); + return out; + } + switch (in.wellKnown()) { + case DataTrackFrameEncoding::Ros1: + out.set_well_known(proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_ROS1); + break; + case DataTrackFrameEncoding::Cdr: + out.set_well_known(proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_CDR); + break; + case DataTrackFrameEncoding::Protobuf: + out.set_well_known(proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_PROTOBUF); + break; + case DataTrackFrameEncoding::Flatbuffer: + out.set_well_known(proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_FLATBUFFER); + break; + case DataTrackFrameEncoding::Cbor: + out.set_well_known(proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_CBOR); + break; + case DataTrackFrameEncoding::Msgpack: + out.set_well_known(proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_MSGPACK); + break; + case DataTrackFrameEncoding::Json: + out.set_well_known(proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_JSON); + break; + case DataTrackFrameEncoding::Other: + out.set_well_known(proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED); + break; + } + return out; +} + +DataTrackFrameEncoding fromProto(const proto::DataTrackFrameEncoding& in) { + if (in.encoding_case() == proto::DataTrackFrameEncoding::kCustom) { + return DataTrackFrameEncoding::custom(in.custom()); + } + switch (in.well_known()) { + case proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_ROS1: + return DataTrackFrameEncoding::Ros1; + case proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_CDR: + return DataTrackFrameEncoding::Cdr; + case proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_PROTOBUF: + return DataTrackFrameEncoding::Protobuf; + case proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_FLATBUFFER: + return DataTrackFrameEncoding::Flatbuffer; + case proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_CBOR: + return DataTrackFrameEncoding::Cbor; + case proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_MSGPACK: + return DataTrackFrameEncoding::Msgpack; + case proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_JSON: + return DataTrackFrameEncoding::Json; + default: + return DataTrackFrameEncoding::Other; + } +} + +proto::DataTrackSchemaId toProto(const DataTrackSchemaId& in) { + proto::DataTrackSchemaId out; + out.set_name(in.name); + *out.mutable_encoding() = toProto(in.encoding); + return out; +} + +DataTrackSchemaId fromProto(const proto::DataTrackSchemaId& in) { + DataTrackSchemaId out; + out.name = in.name(); + out.encoding = fromProto(in.encoding()); + return out; +} + +DataTrackInfo fromProto(const proto::DataTrackInfo& in) { + DataTrackInfo out; + out.name = in.name(); + out.sid = in.sid(); + out.uses_e2ee = in.uses_e2ee(); + if (in.has_schema()) { + out.schema = fromProto(in.schema()); + } + if (in.has_frame_encoding()) { + out.frame_encoding = fromProto(in.frame_encoding()); + } + return out; +} + +} // namespace livekit diff --git a/src/ffi_client.cpp b/src/ffi_client.cpp index 89d6b0a8..8c0516b0 100644 --- a/src/ffi_client.cpp +++ b/src/ffi_client.cpp @@ -23,6 +23,7 @@ #include #include "data_track.pb.h" +#include "data_track_proto_converter.h" #include "ffi.pb.h" #include "livekit/build.h" #include "livekit/data_track_error.h" @@ -43,6 +44,13 @@ inline void logAndThrow(const std::string& error_msg) { throw std::runtime_error(error_msg); } +template +std::future makeReadyFuture(T value) { + std::promise promise; + promise.set_value(std::move(value)); + return promise.get_future(); +} + // Helper for debug logging of optional values const auto optional_to_string = [](const auto& value) -> std::string { if (!value) { @@ -138,6 +146,10 @@ std::optional ExtractAsyncId(const proto::FfiEvent& event) { // data track async completions case E::kPublishDataTrack: return event.publish_data_track().async_id(); + case E::kDefineSchema: + return event.define_schema().async_id(); + case E::kGetSchema: + return event.get_schema().async_id(); // NOT async completion: case E::kRoomEvent: @@ -786,7 +798,7 @@ std::future FfiClient::publishDataAsync(std::uint64_t local_participant_ha } std::future> FfiClient::publishDataTrackAsync( - std::uint64_t local_participant_handle, const std::string& track_name) { + std::uint64_t local_participant_handle, const DataTrackPublishOptions& options) { const AsyncId async_id = generateAsyncId(); auto fut = registerAsync>( @@ -812,7 +824,14 @@ std::future> FfiClient proto::FfiRequest req; auto* msg = req.mutable_publish_data_track(); msg->set_local_participant_handle(local_participant_handle); - msg->mutable_options()->set_name(track_name); + auto* opts = msg->mutable_options(); + opts->set_name(options.name); + if (options.schema.has_value()) { + *opts->mutable_schema() = toProto(*options.schema); + } + if (options.frame_encoding.has_value()) { + *opts->mutable_frame_encoding() = toProto(*options.frame_encoding); + } msg->set_request_async_id(async_id); try { @@ -1173,4 +1192,90 @@ std::future FfiClient::sendStreamTrailerAsync(std::uint64_t local_particip return fut; } +std::future> FfiClient::defineSchemaAsync(std::uint64_t local_participant_handle, + const DataTrackSchemaId& schema_id, + const std::string& definition) { + // Generate client-side async_id first + const AsyncId async_id = generateAsyncId(); + + // Register the async handler BEFORE sending the request + auto fut = registerAsync>( + async_id, + [async_id](const proto::FfiEvent& event) { + return event.has_define_schema() && event.define_schema().async_id() == async_id; + }, + [](const proto::FfiEvent& event, std::promise>& pr) { + const auto& cb = event.define_schema(); + if (cb.has_error() && !cb.error().empty()) { + pr.set_value(Result::failure(cb.error())); + return; + } + pr.set_value(Result::success()); + }); + + // Build and send the request + proto::FfiRequest req; + auto* msg = req.mutable_define_schema(); + msg->set_local_participant_handle(local_participant_handle); + *msg->mutable_schema_id() = toProto(schema_id); + msg->set_definition(definition); + msg->set_request_async_id(async_id); + + try { + const proto::FfiResponse resp = sendRequest(req); + if (!resp.has_define_schema()) { + cancelPendingByAsyncId(async_id); + return makeReadyFuture(Result::failure("FfiResponse missing define_schema")); + } + } catch (const std::exception& e) { + cancelPendingByAsyncId(async_id); + return makeReadyFuture(Result::failure(e.what())); + } + + return fut; +} + +std::future> FfiClient::getSchemaAsync(std::uint64_t local_participant_handle, + const DataTrackSchemaId& schema_id, + const std::string& participant_identity) { + // Generate client-side async_id first + const AsyncId async_id = generateAsyncId(); + + // Register the async handler BEFORE sending the request + auto fut = registerAsync>( + async_id, + [async_id](const proto::FfiEvent& event) { + return event.has_get_schema() && event.get_schema().async_id() == async_id; + }, + [](const proto::FfiEvent& event, std::promise>& pr) { + const auto& cb = event.get_schema(); + if (cb.has_error() && !cb.error().empty()) { + pr.set_value(Result::failure(cb.error())); + return; + } + pr.set_value(Result::success(cb.definition())); + }); + + // Build and send the request + proto::FfiRequest req; + auto* msg = req.mutable_get_schema(); + msg->set_local_participant_handle(local_participant_handle); + *msg->mutable_schema_id() = toProto(schema_id); + msg->set_participant_identity(participant_identity); + msg->set_request_async_id(async_id); + + try { + const proto::FfiResponse resp = sendRequest(req); + if (!resp.has_get_schema()) { + cancelPendingByAsyncId(async_id); + return makeReadyFuture(Result::failure("FfiResponse missing get_schema")); + } + } catch (const std::exception& e) { + cancelPendingByAsyncId(async_id); + return makeReadyFuture(Result::failure(e.what())); + } + + return fut; +} + } // namespace livekit diff --git a/src/ffi_client.h b/src/ffi_client.h index 9dc840e4..c0835027 100644 --- a/src/ffi_client.h +++ b/src/ffi_client.h @@ -31,6 +31,8 @@ #include "data_track.pb.h" #include "livekit/data_track_error.h" +#include "livekit/data_track_options.h" +#include "livekit/data_track_schema.h" #include "livekit/result.h" #include "livekit/room_event_types.h" #include "livekit/stats.h" @@ -123,9 +125,17 @@ class LIVEKIT_INTERNAL_API FfiClient { const std::string& payload, std::optional response_timeout_ms = std::nullopt); + // Data Track schema APIs + std::future> defineSchemaAsync(std::uint64_t local_participant_handle, + const DataTrackSchemaId& schema_id, + const std::string& definition); + std::future> getSchemaAsync(std::uint64_t local_participant_handle, + const DataTrackSchemaId& schema_id, + const std::string& participant_identity); + // Data Track APIs std::future> publishDataTrackAsync( - std::uint64_t local_participant_handle, const std::string& track_name); + std::uint64_t local_participant_handle, const DataTrackPublishOptions& options); Result subscribeDataTrack( std::uint64_t track_handle, std::optional buffer_size = std::nullopt); diff --git a/src/local_data_track.cpp b/src/local_data_track.cpp index 6173ab28..5986e283 100644 --- a/src/local_data_track.cpp +++ b/src/local_data_track.cpp @@ -17,6 +17,7 @@ #include "livekit/local_data_track.h" #include "data_track.pb.h" +#include "data_track_proto_converter.h" #include "ffi.pb.h" #include "ffi_client.h" #include "livekit/data_track_error.h" @@ -25,12 +26,7 @@ namespace livekit { LocalDataTrack::LocalDataTrack(const proto::OwnedLocalDataTrack& owned) - : handle_(static_cast(owned.handle().id())) { - const auto& pi = owned.info(); - info_.name = pi.name(); - info_.sid = pi.sid(); - info_.uses_e2ee = pi.uses_e2ee(); -} + : handle_(static_cast(owned.handle().id())), info_(fromProto(owned.info())) {} Result LocalDataTrack::tryPush(const DataTrackFrame& frame) { if (!handle_.valid()) { diff --git a/src/local_participant.cpp b/src/local_participant.cpp index 7fda68ac..1157aa0e 100644 --- a/src/local_participant.cpp +++ b/src/local_participant.cpp @@ -29,6 +29,7 @@ #include "livekit/local_video_track.h" #include "livekit/room_delegate.h" #include "livekit/track.h" +#include "lk_log.h" #include "participant.pb.h" #include "room.pb.h" #include "room_proto_converter.h" @@ -263,6 +264,13 @@ LocalParticipant::PublicationMap LocalParticipant::trackPublications() const { Result, PublishDataTrackError> LocalParticipant::publishDataTrack( const std::string& name) { + DataTrackPublishOptions options; + options.name = name; + return publishDataTrack(options); +} + +Result, PublishDataTrackError> LocalParticipant::publishDataTrack( + const DataTrackPublishOptions& options) { auto handle_id = ffiHandleId(); if (handle_id == 0) { return Result, PublishDataTrackError>::failure( @@ -271,7 +279,7 @@ Result, PublishDataTrackError> LocalParticipant: "handle"}); } - auto fut = FfiClient::instance().publishDataTrackAsync(static_cast(handle_id), name); + auto fut = FfiClient::instance().publishDataTrackAsync(static_cast(handle_id), options); auto result = fut.get(); if (!result) { @@ -290,6 +298,39 @@ void LocalParticipant::unpublishDataTrack(const std::shared_ptr& track->unpublishDataTrack(); } +bool LocalParticipant::defineSchema(const DataTrackSchemaId& id, const std::string& definition) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + LK_LOG_ERROR("LocalParticipant::defineSchema failed: invalid FFI handle"); + return false; + } + + auto fut = FfiClient::instance().defineSchemaAsync(static_cast(handle_id), id, definition); + auto result = fut.get(); + if (!result) { + LK_LOG_ERROR("LocalParticipant::defineSchema failed: {}", result.error()); + return false; + } + return true; +} + +std::optional LocalParticipant::getSchema(const DataTrackSchemaId& id, + const std::string& participant_identity) { + auto handle_id = ffiHandleId(); + if (handle_id == 0) { + LK_LOG_ERROR("LocalParticipant::getSchema failed: invalid FFI handle"); + return std::nullopt; + } + + auto fut = FfiClient::instance().getSchemaAsync(static_cast(handle_id), id, participant_identity); + auto result = fut.get(); + if (!result) { + LK_LOG_ERROR("LocalParticipant::getSchema failed: {}", result.error()); + return std::nullopt; + } + return std::move(result).value(); +} + std::string LocalParticipant::performRpc(const std::string& destination_identity, const std::string& method, const std::string& payload, const std::optional& response_timeout) { auto handle_id = ffiHandleId(); diff --git a/src/remote_data_track.cpp b/src/remote_data_track.cpp index 2d225396..d15896fe 100644 --- a/src/remote_data_track.cpp +++ b/src/remote_data_track.cpp @@ -19,6 +19,7 @@ #include #include "data_track.pb.h" +#include "data_track_proto_converter.h" #include "ffi.pb.h" #include "ffi_client.h" #include "lk_log.h" @@ -27,10 +28,7 @@ namespace livekit { RemoteDataTrack::RemoteDataTrack(const proto::OwnedRemoteDataTrack& owned) : handle_(static_cast(owned.handle().id())), publisher_identity_(owned.publisher_identity()) { - const auto& pi = owned.info(); - info_.name = pi.name(); - info_.sid = pi.sid(); - info_.uses_e2ee = pi.uses_e2ee(); + info_ = fromProto(owned.info()); } bool RemoteDataTrack::isPublished() const { diff --git a/src/tests/integration/test_data_track.cpp b/src/tests/integration/test_data_track.cpp index 8548527b..7f119443 100644 --- a/src/tests/integration/test_data_track.cpp +++ b/src/tests/integration/test_data_track.cpp @@ -21,8 +21,11 @@ // and run: // ./build-debug/bin/livekit_integration_tests +#include +#include #include #include +#include #include #include @@ -51,6 +54,7 @@ constexpr std::uint8_t kTransportPayloadValue = 0xFA; constexpr char kE2EESharedSecret[] = "password"; constexpr int kE2EEFrameCount = 5; constexpr int kTimestampFrameAttempts = 200; +constexpr std::size_t kMaxSchemaDefinitionBytes = 60000; std::string makeTrackName(const std::string& suffix) { return std::string(kTrackNamePrefix) + "_" + suffix + "_" + std::to_string(getTimestampUs()); @@ -588,6 +592,161 @@ TEST_F(DataTrackE2ETest, PublishDuplicateName) { first_track->unpublishDataTrack(); } +TEST_F(DataTrackE2ETest, DefineAndGetSchema) { + auto rooms = testRooms(2); + auto& publisher_room = rooms[0]; + auto& subscriber_room = rooms[1]; + + const auto publisher_identity = lockLocalParticipant(*publisher_room)->identity(); + const DataTrackSchemaId schema_id{"some_schema", DataTrackSchemaEncoding::JsonSchema}; + const std::string definition(kMaxSchemaDefinitionBytes, 'a'); + + ASSERT_TRUE(lockLocalParticipant(*publisher_room)->defineSchema(schema_id, definition)); + + const auto retrieved = lockLocalParticipant(*subscriber_room)->getSchema(schema_id, publisher_identity); + ASSERT_TRUE(retrieved); + EXPECT_EQ(*retrieved, definition); +} + +TEST_F(DataTrackE2ETest, DefineAndGetCustomSchemaEncoding) { + auto rooms = testRooms(2); + auto& publisher_room = rooms[0]; + auto& subscriber_room = rooms[1]; + + const auto publisher_identity = lockLocalParticipant(*publisher_room)->identity(); + const DataTrackSchemaId schema_id{"custom_schema", DataTrackSchemaEncoding::custom("custom-schema")}; + const std::string definition("custom schema definition"); + + ASSERT_TRUE(lockLocalParticipant(*publisher_room)->defineSchema(schema_id, definition)); + + const auto retrieved = lockLocalParticipant(*subscriber_room)->getSchema(schema_id, publisher_identity); + ASSERT_TRUE(retrieved); + EXPECT_EQ(*retrieved, definition); +} + +TEST_F(DataTrackE2ETest, DefineSchemaOverLimitFails) { + auto rooms = testRooms(1); + auto& room = rooms[0]; + + const DataTrackSchemaId schema_id{"some_schema", DataTrackSchemaEncoding::JsonSchema}; + // Deliberately exceed the maximum allowed schema definition size. + const std::string definition(2 * kMaxSchemaDefinitionBytes, 'a'); + + const auto result = lockLocalParticipant(*room)->defineSchema(schema_id, definition); + EXPECT_FALSE(result); +} + +TEST_F(DataTrackE2ETest, DefineDuplicateSchemaFails) { + auto rooms = testRooms(1); + auto& room = rooms[0]; + + const DataTrackSchemaId schema_id{"some_schema", DataTrackSchemaEncoding::JsonSchema}; + const std::string definition(kMaxSchemaDefinitionBytes, 'a'); + + ASSERT_TRUE(lockLocalParticipant(*room)->defineSchema(schema_id, definition)); + // Defining the same schema again must fail. + const auto duplicate_result = lockLocalParticipant(*room)->defineSchema(schema_id, definition); + EXPECT_FALSE(duplicate_result); +} + +TEST_F(DataTrackE2ETest, GetUndefinedSchemaFails) { + auto rooms = testRooms(1); + auto& room = rooms[0]; + + const auto identity = lockLocalParticipant(*room)->identity(); + const DataTrackSchemaId schema_id{"undefined", DataTrackSchemaEncoding::JsonSchema}; + + const auto result = lockLocalParticipant(*room)->getSchema(schema_id, identity); + EXPECT_FALSE(result.has_value()); +} + +TEST_F(DataTrackE2ETest, PublishWithSchemaAndFrameEncodingMetadata) { + const auto track_name = makeTrackName("schema_meta"); + + DataTrackPublishedDelegate subscriber_delegate; + std::vector room_configs(2); + room_configs[1].delegate = &subscriber_delegate; + + auto rooms = testRooms(room_configs); + auto& publisher_room = rooms[0]; + + DataTrackPublishOptions options; + options.name = track_name; + options.schema = DataTrackSchemaId{"sensor_msgs/Image", DataTrackSchemaEncoding::Ros2Msg}; + options.frame_encoding = DataTrackFrameEncoding::Cdr; + + auto publish_result = lockLocalParticipant(*publisher_room)->publishDataTrack(options); + if (!publish_result) { + FAIL() << describeDataTrackError(publish_result.error()); + } + auto local_track = publish_result.value(); + ASSERT_TRUE(local_track->isPublished()); + + const auto& local_info = local_track->info(); + ASSERT_TRUE(local_info.schema.has_value()); + EXPECT_EQ(local_info.schema->name, "sensor_msgs/Image"); + EXPECT_EQ(local_info.schema->encoding, DataTrackSchemaEncoding::Ros2Msg); + ASSERT_TRUE(local_info.frame_encoding.has_value()); + EXPECT_EQ(*local_info.frame_encoding, DataTrackFrameEncoding::Cdr); + + auto remote_track = subscriber_delegate.waitForTrack(kTrackWaitTimeout); + ASSERT_NE(remote_track, nullptr) << "Timed out waiting for remote data track"; + EXPECT_EQ(remote_track->info().name, track_name); + + const auto& remote_info = remote_track->info(); + ASSERT_TRUE(remote_info.schema.has_value()); + EXPECT_EQ(remote_info.schema->name, "sensor_msgs/Image"); + EXPECT_EQ(remote_info.schema->encoding, DataTrackSchemaEncoding::Ros2Msg); + ASSERT_TRUE(remote_info.frame_encoding.has_value()); + EXPECT_EQ(*remote_info.frame_encoding, DataTrackFrameEncoding::Cdr); + + local_track->unpublishDataTrack(); +} + +TEST_F(DataTrackE2ETest, PublishWithCustomSchemaAndFrameEncodingMetadata) { + const auto track_name = makeTrackName("custom_schema_meta"); + + DataTrackPublishedDelegate subscriber_delegate; + std::vector room_configs(2); + room_configs[1].delegate = &subscriber_delegate; + + auto rooms = testRooms(room_configs); + auto& publisher_room = rooms[0]; + + const DataTrackSchemaId schema_id{"custom_schema", DataTrackSchemaEncoding::custom("custom-schema")}; + const auto frame_encoding = DataTrackFrameEncoding::custom("custom-frame"); + + DataTrackPublishOptions options; + options.name = track_name; + options.schema = schema_id; + options.frame_encoding = frame_encoding; + + auto publish_result = lockLocalParticipant(*publisher_room)->publishDataTrack(options); + if (!publish_result) { + FAIL() << describeDataTrackError(publish_result.error()); + } + const auto& local_track = publish_result.value(); + ASSERT_TRUE(local_track->isPublished()); + + const auto& local_info = local_track->info(); + ASSERT_TRUE(local_info.schema.has_value()); + EXPECT_EQ(*local_info.schema, schema_id); + ASSERT_TRUE(local_info.frame_encoding.has_value()); + EXPECT_EQ(*local_info.frame_encoding, frame_encoding); + + auto remote_track = subscriber_delegate.waitForTrack(kTrackWaitTimeout); + ASSERT_NE(remote_track, nullptr) << "Timed out waiting for remote data track"; + EXPECT_EQ(remote_track->info().name, track_name); + + const auto& remote_info = remote_track->info(); + ASSERT_TRUE(remote_info.schema.has_value()); + EXPECT_EQ(*remote_info.schema, schema_id); + ASSERT_TRUE(remote_info.frame_encoding.has_value()); + EXPECT_EQ(*remote_info.frame_encoding, frame_encoding); + + local_track->unpublishDataTrack(); +} + TEST_F(DataTrackE2ETest, CanResubscribeToRemoteDataTrack) { const auto track_name = makeTrackName("resubscribe"); diff --git a/src/tests/unit/test_data_track_info.cpp b/src/tests/unit/test_data_track_info.cpp index 0e0eb03c..d3eed361 100644 --- a/src/tests/unit/test_data_track_info.cpp +++ b/src/tests/unit/test_data_track_info.cpp @@ -24,13 +24,20 @@ TEST(DataTrackInfoTest, DefaultConstructed) { EXPECT_TRUE(info.name.empty()); EXPECT_TRUE(info.sid.empty()); EXPECT_FALSE(info.uses_e2ee); + EXPECT_FALSE(info.schema.has_value()); + EXPECT_FALSE(info.frame_encoding.has_value()); } TEST(DataTrackInfoTest, AggregateInitialization) { - DataTrackInfo info{"name", "sid", true}; + DataTrackInfo info{"name", "sid", true, DataTrackSchemaId{"schema", DataTrackSchemaEncoding::JsonSchema}, + DataTrackFrameEncoding::Json}; EXPECT_EQ(info.name, "name"); EXPECT_EQ(info.sid, "sid"); EXPECT_TRUE(info.uses_e2ee); + ASSERT_TRUE(info.schema.has_value()); + EXPECT_EQ(*info.schema, (DataTrackSchemaId{"schema", DataTrackSchemaEncoding::JsonSchema})); + ASSERT_TRUE(info.frame_encoding.has_value()); + EXPECT_EQ(*info.frame_encoding, DataTrackFrameEncoding::Json); } } // namespace livekit::test diff --git a/src/tests/unit/test_data_track_schema.cpp b/src/tests/unit/test_data_track_schema.cpp new file mode 100644 index 00000000..8968bf31 --- /dev/null +++ b/src/tests/unit/test_data_track_schema.cpp @@ -0,0 +1,126 @@ +/* + * Copyright 2026 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 + +#include "data_track_proto_converter.h" + +namespace livekit::test { + +TEST(DataTrackSchemaEncodingTest, WellKnownValuesRoundTrip) { + const DataTrackSchemaEncoding encoding = DataTrackSchemaEncoding::JsonSchema; + + const auto proto_encoding = toProto(encoding); + ASSERT_EQ(proto_encoding.encoding_case(), proto::DataTrackSchemaEncoding::kWellKnown); + EXPECT_EQ(proto_encoding.well_known(), proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA); + EXPECT_EQ(fromProto(proto_encoding), encoding); +} + +TEST(DataTrackSchemaEncodingTest, CustomValueRoundTrips) { + const auto encoding = DataTrackSchemaEncoding::custom("custom-schema"); + + const auto proto_encoding = toProto(encoding); + ASSERT_EQ(proto_encoding.encoding_case(), proto::DataTrackSchemaEncoding::kCustom); + EXPECT_EQ(proto_encoding.custom(), "custom-schema"); + EXPECT_EQ(fromProto(proto_encoding), encoding); +} + +TEST(DataTrackSchemaEncodingTest, UnspecifiedAndMissingValuesMapToOther) { + EXPECT_EQ(fromProto(proto::DataTrackSchemaEncoding{}), DataTrackSchemaEncoding::Other); + + proto::DataTrackSchemaEncoding proto_encoding; + proto_encoding.set_well_known(proto::DataTrackSchemaEncoding::WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED); + EXPECT_EQ(fromProto(proto_encoding), DataTrackSchemaEncoding::Other); +} + +TEST(DataTrackFrameEncodingTest, WellKnownValuesRoundTrip) { + const DataTrackFrameEncoding encoding = DataTrackFrameEncoding::Msgpack; + + const auto proto_encoding = toProto(encoding); + ASSERT_EQ(proto_encoding.encoding_case(), proto::DataTrackFrameEncoding::kWellKnown); + EXPECT_EQ(proto_encoding.well_known(), proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_MSGPACK); + EXPECT_EQ(fromProto(proto_encoding), encoding); +} + +TEST(DataTrackFrameEncodingTest, CustomValueRoundTrips) { + const auto encoding = DataTrackFrameEncoding::custom("custom-frame"); + + const auto proto_encoding = toProto(encoding); + ASSERT_EQ(proto_encoding.encoding_case(), proto::DataTrackFrameEncoding::kCustom); + EXPECT_EQ(proto_encoding.custom(), "custom-frame"); + EXPECT_EQ(fromProto(proto_encoding), encoding); +} + +TEST(DataTrackFrameEncodingTest, UnspecifiedAndMissingValuesMapToOther) { + EXPECT_EQ(fromProto(proto::DataTrackFrameEncoding{}), DataTrackFrameEncoding::Other); + + proto::DataTrackFrameEncoding proto_encoding; + proto_encoding.set_well_known(proto::DataTrackFrameEncoding::WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED); + EXPECT_EQ(fromProto(proto_encoding), DataTrackFrameEncoding::Other); +} + +TEST(DataTrackSchemaIdTest, RoundTripsThroughProto) { + const DataTrackSchemaId id{"robot.telemetry", DataTrackSchemaEncoding::custom("custom-schema")}; + + const auto proto_id = toProto(id); + EXPECT_EQ(proto_id.name(), id.name); + ASSERT_TRUE(proto_id.has_encoding()); + EXPECT_EQ(proto_id.encoding().custom(), "custom-schema"); + + EXPECT_EQ(fromProto(proto_id), id); +} + +TEST(DataTrackSchemaIdTest, MissingEncodingMapsToOther) { + proto::DataTrackSchemaId proto_id; + proto_id.set_name("robot.telemetry"); + + const auto id = fromProto(proto_id); + EXPECT_EQ(id.name, "robot.telemetry"); + EXPECT_EQ(id.encoding, DataTrackSchemaEncoding::Other); +} + +TEST(DataTrackInfoTest, SchemaMetadataRoundTripsFromProto) { + proto::DataTrackInfo proto_info; + proto_info.set_name("telemetry"); + proto_info.set_sid("TR_data"); + proto_info.set_uses_e2ee(true); + proto_info.mutable_schema()->set_name("robot.telemetry"); + *proto_info.mutable_schema()->mutable_encoding() = toProto(DataTrackSchemaEncoding::JsonSchema); + *proto_info.mutable_frame_encoding() = toProto(DataTrackFrameEncoding::Json); + + const auto info = fromProto(proto_info); + EXPECT_EQ(info.name, "telemetry"); + EXPECT_EQ(info.sid, "TR_data"); + EXPECT_TRUE(info.uses_e2ee); + ASSERT_TRUE(info.schema.has_value()); + EXPECT_EQ(info.schema->name, "robot.telemetry"); + EXPECT_EQ(info.schema->encoding, DataTrackSchemaEncoding::JsonSchema); + ASSERT_TRUE(info.frame_encoding.has_value()); + EXPECT_EQ(*info.frame_encoding, DataTrackFrameEncoding::Json); +} + +TEST(DataTrackInfoTest, MissingSchemaMetadataStaysEmpty) { + proto::DataTrackInfo proto_info; + proto_info.set_name("telemetry"); + proto_info.set_sid("TR_data"); + proto_info.set_uses_e2ee(false); + + const auto info = fromProto(proto_info); + EXPECT_FALSE(info.schema.has_value()); + EXPECT_FALSE(info.frame_encoding.has_value()); +} + +} // namespace livekit::test diff --git a/src/tests/unit/test_ffi_client.cpp b/src/tests/unit/test_ffi_client.cpp index 2f41583a..4edf3243 100644 --- a/src/tests/unit/test_ffi_client.cpp +++ b/src/tests/unit/test_ffi_client.cpp @@ -510,12 +510,32 @@ TEST_F(FfiClientTest, NotInitialized_GetSessionStatsAsyncThrows) { TEST_F(FfiClientTest, NotInitialized_PublishDataTrackAsyncFails) { ASSERT_FALSE(FfiClient::instance().isInitialized()); - auto fut_result = FfiClient::instance().publishDataTrackAsync(1, "name"); + DataTrackPublishOptions options; + options.name = "name"; + auto fut_result = FfiClient::instance().publishDataTrackAsync(1, options); auto result = fut_result.get(); EXPECT_FALSE(result.ok()); EXPECT_EQ(result.error().code, PublishDataTrackErrorCode::INTERNAL); } +TEST_F(FfiClientTest, NotInitialized_DefineSchemaAsyncFails) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + const DataTrackSchemaId schema_id{"schema", DataTrackSchemaEncoding::JsonSchema}; + auto result = FfiClient::instance().defineSchemaAsync(1, schema_id, "{}").get(); + EXPECT_FALSE(result); + EXPECT_FALSE(result.error().empty()); +} + +TEST_F(FfiClientTest, NotInitialized_GetSchemaAsyncFails) { + ASSERT_FALSE(FfiClient::instance().isInitialized()); + + const DataTrackSchemaId schema_id{"schema", DataTrackSchemaEncoding::JsonSchema}; + auto result = FfiClient::instance().getSchemaAsync(1, schema_id, "participant").get(); + EXPECT_FALSE(result); + EXPECT_FALSE(result.error().empty()); +} + TEST_F(FfiClientTest, NotInitialized_SubscribeDataTrackFails) { ASSERT_FALSE(FfiClient::instance().isInitialized());