diff --git a/endpoint/stream/stream/include/privmx/endpoint/stream/ServerTypes.hpp b/endpoint/stream/stream/include/privmx/endpoint/stream/ServerTypes.hpp index 638c12ee..f89dd575 100644 --- a/endpoint/stream/stream/include/privmx/endpoint/stream/ServerTypes.hpp +++ b/endpoint/stream/stream/include/privmx/endpoint/stream/ServerTypes.hpp @@ -29,7 +29,8 @@ JSON_STRUCT_EXT(StreamRoomDataEntry, core::server::ContainerDataEntry, STREAM_RO #define STREAM_ROOM_INFO_EXTRA_FIELDS(F) \ F(data, std::vector) \ - F(state, std::optional) + F(state, std::optional) \ + F(streamRoomTtl, std::optional) JSON_STRUCT_EXT(StreamRoomInfo, core::server::ContainerInfoBase, STREAM_ROOM_INFO_EXTRA_FIELDS); #define SESSION_DESCRIPTION_FIELDS(F) \ @@ -37,7 +38,7 @@ JSON_STRUCT_EXT(StreamRoomInfo, core::server::ContainerInfoBase, STREAM_ROOM_INF F(type, std::string) JSON_STRUCT(SessionDescription, SESSION_DESCRIPTION_FIELDS); -#define STREAM_ROOM_CREATE_MODEL_EXTRA_FIELDS(F) +#define STREAM_ROOM_CREATE_MODEL_EXTRA_FIELDS(F) F(streamRoomTtl, std::optional) JSON_STRUCT_EXT(StreamRoomCreateModel, core::server::ContainerCreateModelBase, STREAM_ROOM_CREATE_MODEL_EXTRA_FIELDS); #define STREAM_ROOM_CREATE_RESULT_FIELDS(F) F(streamRoomId, std::string) diff --git a/endpoint/stream/stream/include/privmx/endpoint/stream/StreamApiLowImpl.hpp b/endpoint/stream/stream/include/privmx/endpoint/stream/StreamApiLowImpl.hpp index 6b08370b..f66bf6e8 100644 --- a/endpoint/stream/stream/include/privmx/endpoint/stream/StreamApiLowImpl.hpp +++ b/endpoint/stream/stream/include/privmx/endpoint/stream/StreamApiLowImpl.hpp @@ -56,6 +56,7 @@ class StreamApiLowImpl : public privmx::utils::ManualManagedClass& policies, + const std::optional& streamRoomTtl, const std::string& type = STREAM_TYPE_FILTER_FLAG ); diff --git a/endpoint/stream/stream/include_pub/privmx/endpoint/stream/StreamApiLow.hpp b/endpoint/stream/stream/include_pub/privmx/endpoint/stream/StreamApiLow.hpp index 01053265..f5850161 100644 --- a/endpoint/stream/stream/include_pub/privmx/endpoint/stream/StreamApiLow.hpp +++ b/endpoint/stream/stream/include_pub/privmx/endpoint/stream/StreamApiLow.hpp @@ -45,7 +45,8 @@ class StreamApiLow : public privmx::endpoint::core::ExtendedPointer& managers, const core::Buffer& publicMeta, const core::Buffer& privateMeta, - const std::optional& policies + const std::optional& policies, + const std::optional& streamRoomTtl = std::nullopt ); void updateStreamRoom( diff --git a/endpoint/stream/stream/include_pub/privmx/endpoint/stream/Types.hpp b/endpoint/stream/stream/include_pub/privmx/endpoint/stream/Types.hpp index e92f990c..f3c558f2 100644 --- a/endpoint/stream/stream/include_pub/privmx/endpoint/stream/Types.hpp +++ b/endpoint/stream/stream/include_pub/privmx/endpoint/stream/Types.hpp @@ -49,6 +49,7 @@ struct StreamRoom { int64_t statusCode; int64_t schemaVersion; std::string state; // "created" | "open" | "closed" + int64_t streamRoomTtl; }; struct Stream { diff --git a/endpoint/stream/stream/src/StreamApiLow.cpp b/endpoint/stream/stream/src/StreamApiLow.cpp index 694b3b40..46bae38a 100644 --- a/endpoint/stream/stream/src/StreamApiLow.cpp +++ b/endpoint/stream/stream/src/StreamApiLow.cpp @@ -67,14 +67,15 @@ std::string StreamApiLow::createStreamRoom( const std::vector& managers, const core::Buffer& publicMeta, const core::Buffer& privateMeta, - const std::optional& policies + const std::optional& policies, + const std::optional& streamRoomTtl ) { auto impl = getImpl(); core::Validator::validateId(contextId, "field:contextId "); core::Validator::validateClass>(users, "field:users "); core::Validator::validateClass>(managers, "field:managers "); try { - return impl->createStreamRoom(contextId, users, managers, publicMeta, privateMeta, policies); + return impl->createStreamRoom(contextId, users, managers, publicMeta, privateMeta, policies, streamRoomTtl); } catch (const privmx::utils::PrivmxException& e) { core::ExceptionConverter::rethrowAsCoreException(e); throw core::Exception("ExceptionConverter rethrow error"); diff --git a/endpoint/stream/stream/src/StreamApiLowImpl.cpp b/endpoint/stream/stream/src/StreamApiLowImpl.cpp index d2c13b05..c88d0e34 100644 --- a/endpoint/stream/stream/src/StreamApiLowImpl.cpp +++ b/endpoint/stream/stream/src/StreamApiLowImpl.cpp @@ -576,6 +576,7 @@ std::string StreamApiLowImpl::createStreamRoom( const core::Buffer& publicMeta, const core::Buffer& privateMeta, const std::optional& policies, + const std::optional& streamRoomTtl, const std::string& type ) { auto ctx = prepareContainerCreate(contextId, users, managers); @@ -595,6 +596,7 @@ std::string StreamApiLowImpl::createStreamRoom( if (policies.has_value()) { createStreamRoomModel.policy = privmx::endpoint::core::Factory::createPolicyServerObject(policies.value()); } + createStreamRoomModel.streamRoomTtl = streamRoomTtl; auto result = _serverApi->streamRoomCreate(createStreamRoomModel); return result.streamRoomId; } diff --git a/endpoint/stream/stream/src/StreamVarSerializer.cpp b/endpoint/stream/stream/src/StreamVarSerializer.cpp index 6abac554..74609fca 100644 --- a/endpoint/stream/stream/src/StreamVarSerializer.cpp +++ b/endpoint/stream/stream/src/StreamVarSerializer.cpp @@ -75,6 +75,7 @@ Poco::Dynamic::Var VarSerializer::serialize(const stream::St obj->set("statusCode", serialize(val.statusCode)); obj->set("schemaVersion", serialize(val.schemaVersion)); obj->set("state", serialize(val.state)); + obj->set("streamRoomTtl", serialize(val.streamRoomTtl)); return obj; } diff --git a/endpoint/stream/stream/src/encryptors/streamRoom/StreamRoomDataSchemaMapper.cpp b/endpoint/stream/stream/src/encryptors/streamRoom/StreamRoomDataSchemaMapper.cpp index 944d5277..a2309ff3 100644 --- a/endpoint/stream/stream/src/encryptors/streamRoom/StreamRoomDataSchemaMapper.cpp +++ b/endpoint/stream/stream/src/encryptors/streamRoom/StreamRoomDataSchemaMapper.cpp @@ -125,6 +125,7 @@ StreamRoom StreamRoomDataSchemaMapper::toLibStreamRoom( .policy = core::Factory::parsePolicyServerObject(info.policy), .statusCode = statusCode, .schemaVersion = schemaVersion, - .state = info.state.value_or("closed") + .state = info.state.value_or("closed"), + .streamRoomTtl = info.streamRoomTtl.value_or(0) }; } \ No newline at end of file diff --git a/endpoint/stream/stream/src/varinterface/StreamApiLowVarInterface.cpp b/endpoint/stream/stream/src/varinterface/StreamApiLowVarInterface.cpp index 523c9ab5..8690c1c7 100644 --- a/endpoint/stream/stream/src/varinterface/StreamApiLowVarInterface.cpp +++ b/endpoint/stream/stream/src/varinterface/StreamApiLowVarInterface.cpp @@ -89,14 +89,15 @@ Poco::Dynamic::Var StreamApiLowVarInterface::buildSubscriptionQuery(const Poco:: } Poco::Dynamic::Var StreamApiLowVarInterface::createStreamRoom(const Poco::Dynamic::Var& args) { - auto argsArr = core::VarInterfaceUtil::validateAndExtractArray(args, 6); + auto argsArr = core::VarInterfaceUtil::validateAndExtractArray(args, 7); auto contextId = _deserializer.deserialize(argsArr->get(0), "contextId"); auto users = _deserializer.deserializeVector(argsArr->get(1), "users"); auto managers = _deserializer.deserializeVector(argsArr->get(2), "managers"); auto publicMeta = _deserializer.deserialize(argsArr->get(3), "publicMeta"); auto privateMeta = _deserializer.deserialize(argsArr->get(4), "privateMeta"); auto policies = _deserializer.deserializeOptional(argsArr->get(5), "policies"); - auto result = _streamApi.createStreamRoom(contextId, users, managers, publicMeta, privateMeta, policies); + auto streamRoomTtl = _deserializer.deserializeOptional(argsArr->get(6), "streamRoomTtl"); + auto result = _streamApi.createStreamRoom(contextId, users, managers, publicMeta, privateMeta, policies, streamRoomTtl); return _serializer.serialize(result); } diff --git a/endpoint/stream/webrtc/include/privmx/endpoint/stream/StreamApiImpl.hpp b/endpoint/stream/webrtc/include/privmx/endpoint/stream/StreamApiImpl.hpp index a8603a8e..c159ae8d 100644 --- a/endpoint/stream/webrtc/include/privmx/endpoint/stream/StreamApiImpl.hpp +++ b/endpoint/stream/webrtc/include/privmx/endpoint/stream/StreamApiImpl.hpp @@ -53,7 +53,8 @@ class StreamApiImpl { const std::vector& managers, const core::Buffer& publicMeta, const core::Buffer& privateMeta, - const std::optional& policies + const std::optional& policies, + const std::optional& streamRoomTtl ); void updateStreamRoom( diff --git a/endpoint/stream/webrtc/include_pub/privmx/endpoint/stream/StreamApi.hpp b/endpoint/stream/webrtc/include_pub/privmx/endpoint/stream/StreamApi.hpp index bef04694..e2b22c7c 100644 --- a/endpoint/stream/webrtc/include_pub/privmx/endpoint/stream/StreamApi.hpp +++ b/endpoint/stream/webrtc/include_pub/privmx/endpoint/stream/StreamApi.hpp @@ -58,6 +58,8 @@ class StreamApi { * @param publicMeta public (unencrypted) metadata * @param privateMeta private (encrypted) metadata * @param policies Stream Room's policies (pass std::nullopt to use defaults) + * @param streamRoomTtl grace period (ms) the room stays open after the last participant leaves before being + * closed; 0 closes it immediately (pass std::nullopt to use the server default) * * @return created Stream Room ID */ @@ -67,7 +69,8 @@ class StreamApi { const std::vector& managers, const core::Buffer& publicMeta, const core::Buffer& privateMeta, - const std::optional& policies + const std::optional& policies, + const std::optional& streamRoomTtl = std::nullopt ); /** diff --git a/endpoint/stream/webrtc/src/StreamApi.cpp b/endpoint/stream/webrtc/src/StreamApi.cpp index 4107c104..2eafd95f 100644 --- a/endpoint/stream/webrtc/src/StreamApi.cpp +++ b/endpoint/stream/webrtc/src/StreamApi.cpp @@ -41,14 +41,15 @@ std::string StreamApi::createStreamRoom( const std::vector& managers, const core::Buffer& publicMeta, const core::Buffer& privateMeta, - const std::optional& policies + const std::optional& policies, + const std::optional& streamRoomTtl ) { validateEndpoint(); core::Validator::validateId(contextId, "field:contextId "); core::Validator::validateClass>(users, "field:users "); core::Validator::validateClass>(managers, "field:managers "); try { - return _impl->createStreamRoom(contextId, users, managers, publicMeta, privateMeta, policies); + return _impl->createStreamRoom(contextId, users, managers, publicMeta, privateMeta, policies, streamRoomTtl); } catch (const privmx::utils::PrivmxException& e) { core::ExceptionConverter::rethrowAsCoreException(e); throw core::Exception("ExceptionConverter rethrow error"); diff --git a/endpoint/stream/webrtc/src/StreamApiImpl.cpp b/endpoint/stream/webrtc/src/StreamApiImpl.cpp index c94f1a9d..828cb06d 100644 --- a/endpoint/stream/webrtc/src/StreamApiImpl.cpp +++ b/endpoint/stream/webrtc/src/StreamApiImpl.cpp @@ -560,9 +560,10 @@ std::string StreamApiImpl::createStreamRoom( const std::vector& managers, const core::Buffer& publicMeta, const core::Buffer& privateMeta, - const std::optional& policies + const std::optional& policies, + const std::optional& streamRoomTtl ) { - return _api->createStreamRoom(contextId, users, managers, publicMeta, privateMeta, policies); + return _api->createStreamRoom(contextId, users, managers, publicMeta, privateMeta, policies, streamRoomTtl); } void StreamApiImpl::updateStreamRoom( diff --git a/test/tests/StreamTest.cpp b/test/tests/StreamTest.cpp index 454a10c1..7ac15114 100644 --- a/test/tests/StreamTest.cpp +++ b/test/tests/StreamTest.cpp @@ -279,7 +279,8 @@ TEST_F(StreamTest, createStreamRoom) { .updatePolicy="all", .updaterCanBeRemovedFromManagers="yes", .ownerCanBeRemovedFromManagers="yes" - } + }, + 60000 ); }); if(streamRoomId.empty()) { @@ -308,6 +309,7 @@ TEST_F(StreamTest, createStreamRoom) { EXPECT_EQ(streamRoom.policy.updatePolicy, std::optional("all")); EXPECT_EQ(streamRoom.policy.updaterCanBeRemovedFromManagers, std::optional("yes")); EXPECT_EQ(streamRoom.policy.ownerCanBeRemovedFromManagers, std::optional("yes")); + EXPECT_EQ(streamRoom.streamRoomTtl, 60000); // same users and managers EXPECT_NO_THROW({ streamRoomId = streamApi->createStreamRoom( @@ -345,6 +347,62 @@ TEST_F(StreamTest, createStreamRoom) { if(streamRoom.managers.size() == 1) { EXPECT_EQ(streamRoom.managers[0], reader->getString("Login.user_1_id")); } + EXPECT_EQ(streamRoom.streamRoomTtl, 0); +} + +TEST_F(StreamTest, streamRoomTtl_allowsRejoinAndRepublishAfterLeave) { + std::string streamRoomId; + EXPECT_NO_THROW({ + streamRoomId = streamApi->createStreamRoom( + reader->getString("Context_1.contextId"), + std::vector{core::UserWithPubKey{ + .userId=reader->getString("Login.user_1_id"), + .pubKey=reader->getString("Login.user_1_pubKey") + }}, + std::vector{core::UserWithPubKey{ + .userId=reader->getString("Login.user_1_id"), + .pubKey=reader->getString("Login.user_1_pubKey") + }}, + core::Buffer::from("public"), + core::Buffer::from("private"), + std::nullopt, + 60000 + ); + }); + ASSERT_FALSE(streamRoomId.empty()); + + stream::StreamHandle handle; + + // first session + EXPECT_NO_THROW({ + streamApi->joinStreamRoom(streamRoomId); + }); + EXPECT_NO_THROW({ + handle = streamApi->createStream(streamRoomId); + streamApi->getImpl()->addFakeVideoTrack(handle); + streamApi->publishStream(handle); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + EXPECT_NO_THROW({ + streamApi->removeStream(handle); + }); + EXPECT_NO_THROW({ + streamApi->leaveStreamRoom(streamRoomId); + }); + + // second session + EXPECT_NO_THROW({ + streamApi->joinStreamRoom(streamRoomId); + }); + EXPECT_NO_THROW({ + handle = streamApi->createStream(streamRoomId); + streamApi->getImpl()->addFakeVideoTrack(handle); + streamApi->publishStream(handle); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + EXPECT_NO_THROW({ + streamApi->leaveStreamRoom(streamRoomId); + }); } TEST_F(StreamTest, listStreamRooms_incorrect_input_data) {