diff --git a/examples/all-clusters-app/all-clusters-common/all-clusters-app.matter b/examples/all-clusters-app/all-clusters-common/all-clusters-app.matter index 8ec0e5bfb43517..90688ca28048e7 100644 --- a/examples/all-clusters-app/all-clusters-common/all-clusters-app.matter +++ b/examples/all-clusters-app/all-clusters-common/all-clusters-app.matter @@ -6570,6 +6570,176 @@ provisional cluster CameraAvSettingsUserLevelManagement = 1362 { command DPTZRelativeMove(DPTZRelativeMoveRequest): DefaultSuccess = 6; } +/** This cluster implements the upload of Audio and Video streams from the Push AV Stream Transport Cluster using suitable push-based transports. */ +provisional cluster PushAvStreamTransport = 1365 { + revision 1; + + enum ContainerFormatEnum : enum8 { + kCMAF = 0 [spec_name = "CMAF"]; + } + + enum IngestMethodsEnum : enum8 { + kCMAFIngest = 0; + } + + enum StatusCodeEnum : enum8 { + kInvalidTLSEndpoint = 2; + kInvalidStream = 3; + kInvalidURL = 4; + kInvalidZone = 5; + kInvalidCombination = 6; + kInvalidTriggerType = 7; + kInvalidTransportStatus = 8; + } + + enum TransportStatusEnum : enum8 { + kActive = 0; + kInactive = 1; + } + + enum TransportTriggerTypeEnum : enum8 { + kCommand = 0; + kMotion = 1; + kContinuous = 2; + } + + enum TriggerActivationReasonEnum : enum8 { + kUserInitiated = 0; + kAutomation = 1; + kEmergency = 2; + } + + bitmap Feature : bitmap32 { + kPerZoneSensitivity = 0x1; + kMetadata = 0x2; + } + + struct TransportMotionTriggerTimeControlStruct { + int16u initialDuration = 0; + int16u augmentationDuration = 1; + elapsed_s maxDuration = 2; + int16u blindDuration = 3; + } + + struct TransportZoneOptionsStruct { + nullable int16u zone = 0; + optional int8u sensitivity = 1; + } + + struct TransportTriggerOptionsStruct { + TransportTriggerTypeEnum triggerType = 0; + optional nullable TransportZoneOptionsStruct motionZones[] = 1; + optional nullable int8u motionSensitivity = 2; + optional TransportMotionTriggerTimeControlStruct motionTimeControl = 3; + optional int16u maxPreRollLen = 4; + } + + struct CMAFContainerOptionsStruct { + int16u chunkDuration = 0; + optional octet_string<16> CENCKey = 1; + optional boolean metadataEnabled = 2; + optional octet_string<16> CENCKeyID = 3; + } + + struct ContainerOptionsStruct { + ContainerFormatEnum containerType = 0; + optional CMAFContainerOptionsStruct CMAFContainerOptions = 1; + } + + struct TransportOptionsStruct { + StreamUsageEnum streamUsage = 0; + optional nullable int16u videoStreamID = 1; + optional nullable int16u audioStreamID = 2; + int16u endpointID = 3; + long_char_string<2000> url = 4; + TransportTriggerOptionsStruct triggerOptions = 5; + IngestMethodsEnum ingestMethod = 6; + ContainerOptionsStruct containerOptions = 7; + optional epoch_s expiryTime = 8; + } + + fabric_scoped struct TransportConfigurationStruct { + int16u connectionID = 0; + TransportStatusEnum transportStatus = 1; + optional TransportOptionsStruct transportOptions = 2; + fabric_idx fabricIndex = 254; + } + + struct SupportedFormatStruct { + ContainerFormatEnum containerFormat = 0; + IngestMethodsEnum ingestMethod = 1; + } + + info event PushTransportBegin = 0 { + int16u connectionID = 0; + TransportTriggerTypeEnum triggerType = 1; + optional TriggerActivationReasonEnum activationReason = 2; + } + + info event PushTransportEnd = 1 { + int16u connectionID = 0; + TransportTriggerTypeEnum triggerType = 1; + optional TriggerActivationReasonEnum activationReason = 2; + } + + readonly attribute SupportedFormatStruct supportedFormats[] = 0; + readonly attribute TransportConfigurationStruct currentConnections[] = 1; + readonly attribute command_id generatedCommandList[] = 65528; + readonly attribute command_id acceptedCommandList[] = 65529; + readonly attribute attrib_id attributeList[] = 65531; + readonly attribute bitmap32 featureMap = 65532; + readonly attribute int16u clusterRevision = 65533; + + request struct AllocatePushTransportRequest { + TransportOptionsStruct transportOptions = 0; + } + + response struct AllocatePushTransportResponse = 1 { + TransportConfigurationStruct transportConfiguration = 0; + } + + request struct DeallocatePushTransportRequest { + int16u connectionID = 0; + } + + request struct ModifyPushTransportRequest { + int16u connectionID = 0; + TransportOptionsStruct transportOptions = 1; + } + + request struct SetTransportStatusRequest { + nullable int16u connectionID = 0; + TransportStatusEnum transportStatus = 1; + } + + request struct ManuallyTriggerTransportRequest { + int16u connectionID = 0; + TriggerActivationReasonEnum activationReason = 1; + optional TransportMotionTriggerTimeControlStruct timeControl = 2; + } + + request struct FindTransportRequest { + optional nullable int16u connectionID = 0; + } + + response struct FindTransportResponse = 7 { + TransportConfigurationStruct transportConfigurations[] = 0; + } + + /** This command SHALL allocate a transport and return a PushTransportConnectionID. */ + fabric command access(invoke: manage) AllocatePushTransport(AllocatePushTransportRequest): AllocatePushTransportResponse = 0; + /** This command SHALL be generated to request the Node deallocates the specified transport. */ + fabric command access(invoke: manage) DeallocatePushTransport(DeallocatePushTransportRequest): DefaultSuccess = 2; + /** This command is used to request the Node modifies the configuration of the specified push transport. */ + fabric command access(invoke: manage) ModifyPushTransport(ModifyPushTransportRequest): DefaultSuccess = 3; + /** This command SHALL be generated to request the Node modifies the Transport Status of a specified transport or all transports. */ + fabric command access(invoke: manage) SetTransportStatus(SetTransportStatusRequest): DefaultSuccess = 4; + /** This command SHALL be generated to request the Node to manually start the specified push transport. */ + fabric command ManuallyTriggerTransport(ManuallyTriggerTransportRequest): DefaultSuccess = 5; + /** This command SHALL return the Transport Configuration for the specified push transport or all allocated transports for the fabric if null. */ + fabric command FindTransport(FindTransportRequest): FindTransportResponse = 6; +} + /** This cluster provides facilities to configure and play Chime sounds, such as those used in a doorbell. */ provisional cluster Chime = 1366 { revision 1; @@ -8843,6 +9013,27 @@ endpoint 1 { handle command DPTZRelativeMove; } + server cluster PushAvStreamTransport { + emits event PushTransportBegin; + emits event PushTransportEnd; + callback attribute supportedFormats; + callback attribute currentConnections; + callback attribute generatedCommandList; + callback attribute acceptedCommandList; + callback attribute attributeList; + ram attribute featureMap default = 0; + ram attribute clusterRevision default = 1; + + handle command AllocatePushTransport; + handle command AllocatePushTransportResponse; + handle command DeallocatePushTransport; + handle command ModifyPushTransport; + handle command SetTransportStatus; + handle command ManuallyTriggerTransport; + handle command FindTransport; + handle command FindTransportResponse; + } + server cluster Chime { callback attribute installedChimeSounds; callback attribute selectedChime; diff --git a/examples/all-clusters-app/all-clusters-common/all-clusters-app.zap b/examples/all-clusters-app/all-clusters-common/all-clusters-app.zap index 2f481881237faa..0eee822d9e4126 100644 --- a/examples/all-clusters-app/all-clusters-common/all-clusters-app.zap +++ b/examples/all-clusters-app/all-clusters-common/all-clusters-app.zap @@ -19959,6 +19959,211 @@ } ] }, + { + "name": "Push AV Stream Transport", + "code": 1365, + "mfgCode": null, + "define": "PUSH_AV_STREAM_TRANSPORT_CLUSTER", + "side": "server", + "enabled": 1, + "apiMaturity": "provisional", + "commands": [ + { + "name": "AllocatePushTransport", + "code": 0, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "AllocatePushTransportResponse", + "code": 1, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "DeallocatePushTransport", + "code": 2, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "ModifyPushTransport", + "code": 3, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "SetTransportStatus", + "code": 4, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "ManuallyTriggerTransport", + "code": 5, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "FindTransport", + "code": 6, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "FindTransportResponse", + "code": 7, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 + } + ], + "attributes": [ + { + "name": "SupportedFormats", + "code": 0, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "CurrentConnections", + "code": 1, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "GeneratedCommandList", + "code": 65528, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "AcceptedCommandList", + "code": 65529, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "AttributeList", + "code": 65531, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "FeatureMap", + "code": 65532, + "mfgCode": null, + "side": "server", + "type": "bitmap32", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "0", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "ClusterRevision", + "code": 65533, + "mfgCode": null, + "side": "server", + "type": "int16u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "1", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + } + ], + "events": [ + { + "name": "PushTransportBegin", + "code": 0, + "mfgCode": null, + "side": "server", + "included": 1 + }, + { + "name": "PushTransportEnd", + "code": 1, + "mfgCode": null, + "side": "server", + "included": 1 + } + ] + }, { "name": "Chime", "code": 1366, diff --git a/examples/all-clusters-app/all-clusters-common/include/push-av-stream-transport-delegate-impl.h b/examples/all-clusters-app/all-clusters-common/include/push-av-stream-transport-delegate-impl.h new file mode 100644 index 00000000000000..fbe97b1364873b --- /dev/null +++ b/examples/all-clusters-app/all-clusters-common/include/push-av-stream-transport-delegate-impl.h @@ -0,0 +1,87 @@ +/* + * + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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 chip { +namespace app { +namespace Clusters { +namespace PushAvStreamTransport { + +struct PushAvStream +{ + uint16_t id; + TransportOptionsStruct transportOptions; + TransportStatusEnum transportStatus; + PushAvStreamTransportStatusEnum connectionStatus; +}; + +/** + * The application delegate to define the options & implement commands. + */ +class PushAvStreamTransportManager : public PushAvStreamTransportDelegate +{ +public: + Protocols::InteractionModel::Status AllocatePushTransport(const TransportOptionsStruct & transportOptions, + const uint16_t connectionID); + Protocols::InteractionModel::Status DeallocatePushTransport(const uint16_t connectionID); + Protocols::InteractionModel::Status ModifyPushTransport(const uint16_t connectionID, + const TransportOptionsStorage transportOptions); + Protocols::InteractionModel::Status SetTransportStatus(const std::vector connectionIDList, + TransportStatusEnum transportStatus); + + Protocols::InteractionModel::Status + ManuallyTriggerTransport(const uint16_t connectionID, TriggerActivationReasonEnum activationReason, + const Optional & timeControl); + + bool ValidateUrl(std::string url); + + Protocols::InteractionModel::Status ValidateBandwidthLimit(StreamUsageEnum streamUsage, + const Optional> & videoStreamId, + const Optional> & audioStreamId); + Protocols::InteractionModel::Status SelectVideoStream(StreamUsageEnum streamUsage, uint16_t & videoStreamId); + + Protocols::InteractionModel::Status SelectAudioStream(StreamUsageEnum streamUsage, uint16_t & audioStreamId); + + Protocols::InteractionModel::Status ValidateVideoStream(uint16_t videoStreamId); + + Protocols::InteractionModel::Status ValidateAudioStream(uint16_t audioStreamId); + + PushAvStreamTransportStatusEnum GetTransportBusyStatus(const uint16_t connectionID); + + void OnAttributeChanged(AttributeId attributeId); + CHIP_ERROR LoadCurrentConnections(std::vector & currentConnections); + CHIP_ERROR PersistentAttributesLoadedCallback(); + + void Init(); + PushAvStreamTransportManager() = default; + + ~PushAvStreamTransportManager() = default; + +private: + std::vector pushavStreams; +}; + +} // namespace PushAvStreamTransport +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/examples/all-clusters-app/all-clusters-common/src/push-av-stream-transport-delegate-impl.cpp b/examples/all-clusters-app/all-clusters-common/src/push-av-stream-transport-delegate-impl.cpp new file mode 100644 index 00000000000000..4ea0a7c103c864 --- /dev/null +++ b/examples/all-clusters-app/all-clusters-common/src/push-av-stream-transport-delegate-impl.cpp @@ -0,0 +1,182 @@ +/* + * + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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 +#include +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::DataModel; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::PushAvStreamTransport; +using chip::Protocols::InteractionModel::Status; + +Protocols::InteractionModel::Status +PushAvStreamTransportManager::AllocatePushTransport(const TransportOptionsStruct & transportOptions, const uint16_t connectionID) +{ + PushAvStream stream{ connectionID, transportOptions, TransportStatusEnum::kInactive, PushAvStreamTransportStatusEnum::kIdle }; + + /*Store the allocated stream persistently*/ + pushavStreams.push_back(stream); + + return Status::Success; +} + +Protocols::InteractionModel::Status PushAvStreamTransportManager::DeallocatePushTransport(const uint16_t connectionID) +{ + pushavStreams.erase(std::remove_if(pushavStreams.begin(), pushavStreams.end(), + [connectionID](const PushAvStream & stream) { return stream.id == connectionID; }), + pushavStreams.end()); + return Status::Success; +} + +Protocols::InteractionModel::Status +PushAvStreamTransportManager::ModifyPushTransport(const uint16_t connectionID, const TransportOptionsStorage transportOptions) +{ + for (PushAvStream & stream : pushavStreams) + { + if (stream.id == connectionID) + { + ChipLogProgress(Zcl, "Modified Push AV Stream with ID: %d", connectionID); + return Status::Success; + } + } + ChipLogError(Zcl, "Allocated Push AV Stream with ID: %d not found", connectionID); + return Status::NotFound; +} + +Protocols::InteractionModel::Status PushAvStreamTransportManager::SetTransportStatus(const std::vector connectionIDList, + TransportStatusEnum transportStatus) +{ + for (PushAvStream & stream : pushavStreams) + { + for (uint16_t connectionID : connectionIDList) + { + if (stream.id == connectionID) + { + stream.transportStatus = transportStatus; + ChipLogProgress(Zcl, "Set Transport Status for Push AV Stream with ID: %d", connectionID); + } + } + } + return Status::Success; +} + +Protocols::InteractionModel::Status PushAvStreamTransportManager::ManuallyTriggerTransport( + const uint16_t connectionID, TriggerActivationReasonEnum activationReason, + const Optional & timeControl) +{ + // TODO: Validates the requested stream usage against the camera's resource management and stream priority policies. + for (PushAvStream & stream : pushavStreams) + { + if (stream.id == connectionID) + { + stream.connectionStatus = PushAvStreamTransportStatusEnum::kBusy; + ChipLogProgress(Zcl, "Transport triggered for Push AV Stream with ID: %d", connectionID); + } + } + return Status::Success; +} + +Protocols::InteractionModel::Status +PushAvStreamTransportManager::ValidateBandwidthLimit(StreamUsageEnum streamUsage, + const Optional> & videoStreamId, + const Optional> & audioStreamId) +{ + // TODO: Validates the requested stream usage against the camera's resource management. + // Returning Status::Success to pass through checks in the Server Implementation. + return Status::Success; +} + +bool PushAvStreamTransportManager::ValidateUrl(std::string url) +{ + return true; +} + +Protocols::InteractionModel::Status PushAvStreamTransportManager::SelectVideoStream(StreamUsageEnum streamUsage, + uint16_t & videoStreamId) +{ + // TODO: Select and Assign videoStreamID from the allocated videoStreams + // Returning Status::Success to pass through checks in the Server Implementation. + return Status::Success; +} + +Protocols::InteractionModel::Status PushAvStreamTransportManager::SelectAudioStream(StreamUsageEnum streamUsage, + uint16_t & audioStreamId) +{ + // TODO: Select and Assign audioStreamID from the allocated audioStreams + // Returning Status::Success to pass through checks in the Server Implementation. + return Status::Success; +} + +Protocols::InteractionModel::Status PushAvStreamTransportManager::ValidateVideoStream(uint16_t videoStreamId) +{ + // TODO: Validate videoStreamID from the allocated videoStreams + // Returning Status::Success to pass through checks in the Server Implementation. + return Status::Success; +} + +Protocols::InteractionModel::Status PushAvStreamTransportManager::ValidateAudioStream(uint16_t audioStreamId) +{ + // TODO: Validate audioStreamID from the allocated audioStreams + // Returning Status::Success to pass through checks in the Server Implementation. + return Status::Success; +} + +PushAvStreamTransportStatusEnum PushAvStreamTransportManager::GetTransportBusyStatus(const uint16_t connectionID) +{ + for (PushAvStream & stream : pushavStreams) + { + if (stream.id == connectionID) + { + return stream.connectionStatus; + } + } + return PushAvStreamTransportStatusEnum::kUnknown; +} + +void PushAvStreamTransportManager::OnAttributeChanged(AttributeId attributeId) +{ + ChipLogProgress(Zcl, "Attribute changed for AttributeId = " ChipLogFormatMEI, ChipLogValueMEI(attributeId)); +} + +void PushAvStreamTransportManager::Init() +{ + ChipLogProgress(Zcl, "Push AV Stream Transport Initialized"); +} +CHIP_ERROR +PushAvStreamTransportManager::LoadCurrentConnections(std::vector & currentConnections) +{ + ChipLogProgress(Zcl, "Push AV Current Connections loaded"); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR +PushAvStreamTransportManager::PersistentAttributesLoadedCallback() +{ + ChipLogProgress(Zcl, "Push AV Stream Transport Persistent attributes loaded"); + + return CHIP_NO_ERROR; +} diff --git a/examples/all-clusters-app/esp32/sdkconfig.defaults b/examples/all-clusters-app/esp32/sdkconfig.defaults index 4dcff1583e4f87..3f1e85dedb135e 100644 --- a/examples/all-clusters-app/esp32/sdkconfig.defaults +++ b/examples/all-clusters-app/esp32/sdkconfig.defaults @@ -69,3 +69,6 @@ CONFIG_MAX_GROUP_ENDPOINTS_PER_FABRIC=3 # Enable OTA Requestor CONFIG_ENABLE_OTA_REQUESTOR=y + +# Optimize Flash +CONFIG_NEWLIB_NANO_FORMAT=y diff --git a/examples/all-clusters-app/esp32/sdkconfig_m5stack.defaults b/examples/all-clusters-app/esp32/sdkconfig_m5stack.defaults index 03691548ea91db..7a3e15a9f48a4a 100644 --- a/examples/all-clusters-app/esp32/sdkconfig_m5stack.defaults +++ b/examples/all-clusters-app/esp32/sdkconfig_m5stack.defaults @@ -79,3 +79,6 @@ CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y CONFIG_EVENT_LOGGING_CRIT_BUFFER_SIZE=512 CONFIG_EVENT_LOGGING_INFO_BUFFER_SIZE=512 CONFIG_EVENT_LOGGING_DEBUG_BUFFER_SIZE=512 + +# Optimize Flash +CONFIG_NEWLIB_NANO_FORMAT=y diff --git a/examples/all-clusters-app/esp32/sdkconfig_m5stack_rpc.defaults b/examples/all-clusters-app/esp32/sdkconfig_m5stack_rpc.defaults index 0a426dffd0066e..62efd0b551a07f 100644 --- a/examples/all-clusters-app/esp32/sdkconfig_m5stack_rpc.defaults +++ b/examples/all-clusters-app/esp32/sdkconfig_m5stack_rpc.defaults @@ -91,3 +91,6 @@ CONFIG_BT_NIMBLE_ROLE_OBSERVER=n CONFIG_EVENT_LOGGING_CRIT_BUFFER_SIZE=512 CONFIG_EVENT_LOGGING_INFO_BUFFER_SIZE=512 CONFIG_EVENT_LOGGING_DEBUG_BUFFER_SIZE=512 + +# Optimize Flash +CONFIG_NEWLIB_NANO_FORMAT=y \ No newline at end of file diff --git a/examples/all-clusters-app/esp32/sdkconfig_rpc.defaults b/examples/all-clusters-app/esp32/sdkconfig_rpc.defaults index 3faf5754c82a99..c04c0021a59247 100644 --- a/examples/all-clusters-app/esp32/sdkconfig_rpc.defaults +++ b/examples/all-clusters-app/esp32/sdkconfig_rpc.defaults @@ -90,3 +90,6 @@ CONFIG_BT_NIMBLE_ROLE_OBSERVER=n CONFIG_EVENT_LOGGING_CRIT_BUFFER_SIZE=512 CONFIG_EVENT_LOGGING_INFO_BUFFER_SIZE=512 CONFIG_EVENT_LOGGING_DEBUG_BUFFER_SIZE=512 + +# Optimize Flash +CONFIG_NEWLIB_NANO_FORMAT=y \ No newline at end of file diff --git a/examples/all-clusters-app/linux/BUILD.gn b/examples/all-clusters-app/linux/BUILD.gn index 0b5853bae9aeab..293b2fdbaaba43 100644 --- a/examples/all-clusters-app/linux/BUILD.gn +++ b/examples/all-clusters-app/linux/BUILD.gn @@ -50,6 +50,7 @@ source_set("chip-all-clusters-common") { "${chip_root}/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/oven-modes.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/oven-operational-state-delegate.cpp", + "${chip_root}/examples/all-clusters-app/all-clusters-common/src/push-av-stream-transport-delegate-impl.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/resource-monitoring-delegates.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/rvc-modes.cpp", "${chip_root}/examples/all-clusters-app/all-clusters-common/src/rvc-operational-state-delegate-impl.cpp", diff --git a/examples/all-clusters-app/linux/main-common.cpp b/examples/all-clusters-app/linux/main-common.cpp index 764ee316cde01e..6b0c7b64eda51d 100644 --- a/examples/all-clusters-app/linux/main-common.cpp +++ b/examples/all-clusters-app/linux/main-common.cpp @@ -33,6 +33,7 @@ #include "operational-state-delegate-impl.h" #include "oven-modes.h" #include "oven-operational-state-delegate.h" +#include "push-av-stream-transport-delegate-impl.h" #include "resource-monitoring-delegates.h" #include "rvc-modes.h" #include "rvc-operational-state-delegate-impl.h" @@ -47,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -82,6 +84,7 @@ Clusters::TemperatureControl::AppSupportedTemperatureLevelsDelegate sAppSupporte Clusters::ModeSelect::StaticSupportedModesManager sStaticSupportedModesManager; Clusters::ValveConfigurationAndControl::ValveControlDelegate sValveDelegate; Clusters::TimeSynchronization::ExtendedTimeSyncDelegate sTimeSyncDelegate; +Clusters::PushAvStreamTransport::PushAvStreamTransportManager gPushAvStreamTransportManager; // Please refer to https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/namespaces constexpr const uint8_t kNamespaceCommon = 7; @@ -194,6 +197,8 @@ void ApplicationInit() VerifyOrDie(Clusters::UnitLocalization::UnitLocalizationServer::Instance().SetTemperatureUnit( Clusters::UnitLocalization::TempUnitEnum::kFahrenheit) == CHIP_NO_ERROR); + Clusters::PushAvStreamTransport::SetDelegate(chip::EndpointId(1), &gPushAvStreamTransportManager); + SetTagList(/* endpoint= */ 0, Span(gEp0TagList)); SetTagList(/* endpoint= */ 1, Span(gEp1TagList)); SetTagList(/* endpoint= */ 2, Span(gEp2TagList)); diff --git a/scripts/tools/check_includes_config.py b/scripts/tools/check_includes_config.py index 4b13ed238151bf..203e2f49836d9d 100644 --- a/scripts/tools/check_includes_config.py +++ b/scripts/tools/check_includes_config.py @@ -144,6 +144,10 @@ 'src/app/clusters/camera-av-stream-management-server/camera-av-stream-management-server.cpp': {'set'}, 'src/app/clusters/camera-av-settings-user-level-management-server/camera-av-settings-user-level-management-server.h': {'string', 'vector'}, 'src/app/clusters/webrtc-transport-requestor-server/webrtc-transport-requestor-server.h': {'string', 'vector'}, + 'src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-server.h': {'vector'}, + 'src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-delegate.h': {'vector'}, + 'src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-storage.h': {'vector'}, + 'src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.h': {'vector'}, 'src/credentials/attestation_verifier/FileAttestationTrustStore.h': {'vector'}, 'src/credentials/attestation_verifier/FileAttestationTrustStore.cpp': {'string'}, 'src/credentials/attestation_verifier/TestDACRevocationDelegateImpl.cpp': {'fstream'}, diff --git a/src/BUILD.gn b/src/BUILD.gn index eeaea33287de0e..b4a629abcb6bbd 100644 --- a/src/BUILD.gn +++ b/src/BUILD.gn @@ -78,6 +78,7 @@ if (chip_build_tests) { "${chip_root}/src/transport/retransmit/tests", "${chip_root}/src/transport/tests", "${chip_root}/src/tracing/esp32_diagnostic_trace/tests", + "${chip_root}/src/app/clusters/push-av-stream-transport-server/tests", ] # Skip DNSSD tests for Mbed platform due to flash memory size limitations diff --git a/src/app/clusters/push-av-stream-transport-server/BUILD.gn b/src/app/clusters/push-av-stream-transport-server/BUILD.gn new file mode 100644 index 00000000000000..630841805ad110 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/BUILD.gn @@ -0,0 +1,34 @@ +# Copyright (c) 2025 Project CHIP Authors +# +# 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. +import("//build_overrides/build.gni") +import("//build_overrides/chip.gni") + +source_set("push-av-stream-transport-server") { + sources = [ + "push-av-stream-transport-cluster.cpp", + "push-av-stream-transport-cluster.h", + "push-av-stream-transport-delegate.h", + "push-av-stream-transport-logic.cpp", + "push-av-stream-transport-logic.h", + "push-av-stream-transport-storage.h", + ] + + public_deps = [ + "${chip_root}/src/app/", + "${chip_root}/src/app/server-cluster", + "${chip_root}/src/lib/core:types", + "${chip_root}/zzz_generated/app-common/clusters/PushAvStreamTransport", + ] + public_configs = [ "${chip_root}/src:includes" ] +} diff --git a/src/app/clusters/push-av-stream-transport-server/CodegenIntegration.cpp b/src/app/clusters/push-av-stream-transport-server/CodegenIntegration.cpp new file mode 100644 index 00000000000000..4a57d4126c3c5c --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/CodegenIntegration.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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 +#include +#include +#include + +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::PushAvStreamTransport::Attributes; +using chip::Protocols::InteractionModel::Status; + +namespace { + +static constexpr size_t kPushAvStreamTransportFixedClusterCount = + PushAvStreamTransport::StaticApplicationConfig::kFixedClusterConfig.size(); +static constexpr size_t kPushAvStreamTransportMaxClusterCount = + kPushAvStreamTransportFixedClusterCount + CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT; + +LazyRegisteredServerCluster gServers[kPushAvStreamTransportMaxClusterCount]; + +// Find the 0-based array index corresponding to the given endpoint id. +// Log an error if not found. +bool findEndpointWithLog(EndpointId endpointId, uint16_t & outArrayIndex) +{ + uint16_t arrayIndex = + emberAfGetClusterServerEndpointIndex(endpointId, PushAvStreamTransport::Id, kPushAvStreamTransportFixedClusterCount); + + if (arrayIndex >= kPushAvStreamTransportMaxClusterCount) + { + ChipLogError(AppServer, "Cound not find endpoint index for endpoint %u", endpointId); + return false; + } + return true; +} + +} // namespace +void emberAfPushAvStreamTransportClusterInitCallback(EndpointId endpointId) +{ + + uint16_t arrayIndex = 0; + if (!findEndpointWithLog(endpointId, arrayIndex)) + { + return; + } + uint32_t rawFeatureMap; + if (FeatureMap::Get(endpointId, &rawFeatureMap) != Status::Success) + { + ChipLogError(AppServer, "Failed to get feature map for endpoint %u", endpointId); + rawFeatureMap = 0; + } + ChipLogProgress(AppServer, "Registering Push AV Stream Transport on endpoint %u, %d", endpointId, arrayIndex); + gServers[arrayIndex].Create(endpointId, BitFlags(rawFeatureMap)); + CHIP_ERROR err = CodegenDataModelProvider::Instance().Registry().Register(gServers[arrayIndex].Registration()); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Failed to register OTA on endpoint %u: %" CHIP_ERROR_FORMAT, endpointId, err.Format()); + } +} + +void emberAfPushAvStreamTransportClusterShutdownCallback(EndpointId endpointId) +{ + uint16_t arrayIndex = 0; + if (!findEndpointWithLog(endpointId, arrayIndex)) + { + return; + } + + CHIP_ERROR err = CodegenDataModelProvider::Instance().Registry().Unregister(&gServers[arrayIndex].Cluster()); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Failed to unregister OTA on endpoint %u: %" CHIP_ERROR_FORMAT, endpointId, err.Format()); + } + gServers[arrayIndex].Cluster().Deinit(); + gServers[arrayIndex].Destroy(); +} + +void MatterPushAvStreamTransportPluginServerInitCallback() {} + +void MatterPushAvStreamTransportPluginServerShutdownCallback() {} + +namespace chip { +namespace app { +namespace Clusters { +namespace PushAvStreamTransport { + +void SetDelegate(EndpointId endpointId, PushAvStreamTransportDelegate * delegate) +{ + ChipLogProgress(AppServer, "Setting Push AV Stream Transport delegate on endpoint %u", endpointId); + uint16_t arrayIndex = 0; + if (!findEndpointWithLog(endpointId, arrayIndex)) + { + return; + } + gServers[arrayIndex].Cluster().SetDelegate(endpointId, delegate); + gServers[arrayIndex].Cluster().Init(); +} + +} // namespace PushAvStreamTransport +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/push-av-stream-transport-server/CodegenIntegration.h b/src/app/clusters/push-av-stream-transport-server/CodegenIntegration.h new file mode 100644 index 00000000000000..3e8ee0554f3f1f --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/CodegenIntegration.h @@ -0,0 +1,34 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * All rights reserved. + * + * 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 "push-av-stream-transport-delegate.h" + +namespace chip { +namespace app { +namespace Clusters { +namespace PushAvStreamTransport { + +/// Sets the given delegate on an endpoint configured via code-generation +void SetDelegate(chip::EndpointId endpointId, PushAvStreamTransportDelegate * delegate); + +} // namespace PushAvStreamTransport +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/push-av-stream-transport-server/app_config_dependent_sources.cmake b/src/app/clusters/push-av-stream-transport-server/app_config_dependent_sources.cmake new file mode 100644 index 00000000000000..0053a93e48257a --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/app_config_dependent_sources.cmake @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Project CHIP Authors +# +# 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. + +# This is the equivalent to app_config_dependent_sources.gni +TARGET_SOURCES( + ${APP_TARGET} + PRIVATE + "${CLUSTER_DIR}/push-av-stream-transport-cluster.cpp" + "${CLUSTER_DIR}/push-av-stream-transport-cluster.h" + "${CLUSTER_DIR}/push-av-stream-transport-logic.cpp" + "${CLUSTER_DIR}/push-av-stream-transport-logic.h" + "${CLUSTER_DIR}/push-av-stream-transport-delegate.h" + "${CLUSTER_DIR}/CodegenIntegration.cpp" + "${CLUSTER_DIR}/CodegenIntegration.h" +) \ No newline at end of file diff --git a/src/app/clusters/push-av-stream-transport-server/app_config_dependent_sources.gni b/src/app/clusters/push-av-stream-transport-server/app_config_dependent_sources.gni new file mode 100644 index 00000000000000..be7224f0bbcf9e --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/app_config_dependent_sources.gni @@ -0,0 +1,17 @@ +# Copyright (c) 2025 Project CHIP Authors +# +# 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. +app_config_dependent_sources = [ + "CodegenIntegration.cpp", + "CodegenIntegration.h", +] diff --git a/src/app/clusters/push-av-stream-transport-server/constants.h b/src/app/clusters/push-av-stream-transport-server/constants.h new file mode 100644 index 00000000000000..bd714db04bc444 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/constants.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2021-2025 Project CHIP Authors + * All rights reserved. + * + * 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 chip { +namespace app { +namespace Clusters { +namespace PushAvStreamTransport { + +using SupportedFormatStruct = Structs::SupportedFormatStruct::Type; +using CMAFContainerOptionsStruct = Structs::CMAFContainerOptionsStruct::Type; +using ContainerOptionsStruct = Structs::ContainerOptionsStruct::Type; +using TransportZoneOptionsStruct = Structs::TransportZoneOptionsStruct::Type; +using TransportTriggerOptionsStruct = Structs::TransportTriggerOptionsStruct::Type; +using TransportMotionTriggerTimeControlStruct = Structs::TransportMotionTriggerTimeControlStruct::Type; +using TransportOptionsStruct = Structs::TransportOptionsStruct::Type; +using TransportConfigurationStruct = Structs::TransportConfigurationStruct::Type; +using StreamUsageEnum = chip::app::Clusters::Globals::StreamUsageEnum; + +static constexpr size_t kMinUrlLength = 13u; +static constexpr size_t kMaxUrlLength = 2000u; +static constexpr size_t kMaxCENCKeyLength = 16u; +static constexpr size_t kMaxCENCKeyIDLength = 16u; + +enum class PushAvStreamTransportStatusEnum : uint8_t +{ + kBusy = 0x00, + kIdle = 0x01, + kUnknown = 0x02 +}; + +} // namespace PushAvStreamTransport +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-cluster.cpp b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-cluster.cpp new file mode 100644 index 00000000000000..32cd102694f241 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-cluster.cpp @@ -0,0 +1,192 @@ +/** + * + * Copyright (c) 2025 Project CHIP Authors + * + * 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 "constants.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace Clusters { +namespace { + +using namespace chip::app::Clusters::PushAvStreamTransport::Commands; + +constexpr DataModel::AcceptedCommandEntry kAcceptedCommands[] = { + AllocatePushTransport::kMetadataEntry, DeallocatePushTransport::kMetadataEntry, ModifyPushTransport::kMetadataEntry, + SetTransportStatus::kMetadataEntry, ManuallyTriggerTransport::kMetadataEntry, FindTransport::kMetadataEntry, +}; + +constexpr CommandId kGeneratedCommands[] = { + AllocatePushTransportResponse::Id, + FindTransportResponse::Id, +}; + +constexpr DataModel::AttributeEntry kAttributes[] = { + PushAvStreamTransport::Attributes::SupportedFormats::kMetadataEntry, + PushAvStreamTransport::Attributes::CurrentConnections::kMetadataEntry, +}; + +} // namespace + +using Protocols::InteractionModel::Status; +using namespace PushAvStreamTransport::Commands; + +CHIP_ERROR PushAvStreamTransportServer::Attributes(const ConcreteClusterPath & path, + ReadOnlyBufferBuilder & builder) +{ + ReturnErrorOnFailure(builder.ReferenceExisting(kAttributes)); + return builder.AppendElements(DefaultServerCluster::GlobalAttributes()); +} + +CHIP_ERROR PushAvStreamTransportServer::ReadAndEncodeSupportedFormats(const AttributeValueEncoder::ListEncodeHelper & encoder) +{ + for (const auto & supportsFormat : mLogic.mSupportedFormats) + { + ReturnErrorOnFailure(encoder.Encode(supportsFormat)); + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR +PushAvStreamTransportServer::ReadAndEncodeCurrentConnections(const AttributeValueEncoder::ListEncodeHelper & encoder, + FabricIndex fabricIndex) +{ + for (const auto & currentConnections : mLogic.mCurrentConnections) + { + if (currentConnections.fabricIndex == fabricIndex) + { + ReturnErrorOnFailure(encoder.Encode(currentConnections)); + } + } + + return CHIP_NO_ERROR; +} + +DataModel::ActionReturnStatus PushAvStreamTransportServer::ReadAttribute(const DataModel::ReadAttributeRequest & request, + AttributeValueEncoder & aEncoder) +{ + VerifyOrDie(request.path.mClusterId == PushAvStreamTransport::Id); + + ChipLogProgress(Zcl, "Push AV Stream Transport[ep=%d]: Reading Attribute %" PRIu32, request.path.mEndpointId, + request.path.mAttributeId); + + switch (request.path.mAttributeId) + { + case PushAvStreamTransport::Attributes::FeatureMap::Id: + return aEncoder.Encode(mLogic.mFeatures.Raw()); + + case PushAvStreamTransport::Attributes::ClusterRevision::Id: + return aEncoder.Encode(PushAvStreamTransport::kRevision); + + case PushAvStreamTransport::Attributes::SupportedFormats::Id: + return aEncoder.EncodeList([this](const auto & encoder) -> CHIP_ERROR { return ReadAndEncodeSupportedFormats(encoder); }); + + case PushAvStreamTransport::Attributes::CurrentConnections::Id: + return aEncoder.EncodeList([this, &aEncoder](const auto & encoder) -> CHIP_ERROR { + CHIP_ERROR err = ReadAndEncodeCurrentConnections(encoder, aEncoder.AccessingFabricIndex()); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "Push AV Stream Transport: Error reading CurrentConnections %s", err.AsString()); + } + return err; + }); + } + + return Status::UnsupportedAttribute; +} + +CHIP_ERROR PushAvStreamTransportServer::AcceptedCommands(const ConcreteClusterPath & path, + ReadOnlyBufferBuilder & builder) +{ + return builder.ReferenceExisting(kAcceptedCommands); +} + +CHIP_ERROR PushAvStreamTransportServer::GeneratedCommands(const ConcreteClusterPath & path, + ReadOnlyBufferBuilder & builder) +{ + return builder.ReferenceExisting(kGeneratedCommands); +} + +std::optional PushAvStreamTransportServer::InvokeCommand(const DataModel::InvokeRequest & request, + TLV::TLVReader & input_arguments, + CommandHandler * handler) +{ + using namespace PushAvStreamTransport::Commands; + + FabricIndex fabricIndex = request.GetAccessingFabricIndex(); + + switch (request.path.mCommandId) + { + case AllocatePushTransport::Id: { + AllocatePushTransport::DecodableType data; + ReturnErrorOnFailure(data.Decode(input_arguments, fabricIndex)); + return mLogic.HandleAllocatePushTransport(*handler, request.path, data); + } + case DeallocatePushTransport::Id: { + DeallocatePushTransport::DecodableType data; + ReturnErrorOnFailure(data.Decode(input_arguments, fabricIndex)); + return mLogic.HandleDeallocatePushTransport(*handler, request.path, data); + } + case ModifyPushTransport::Id: { + ModifyPushTransport::DecodableType data; + ReturnErrorOnFailure(data.Decode(input_arguments, fabricIndex)); + return mLogic.HandleModifyPushTransport(*handler, request.path, data); + } + case SetTransportStatus::Id: { + SetTransportStatus::DecodableType data; + ReturnErrorOnFailure(data.Decode(input_arguments, fabricIndex)); + return mLogic.HandleSetTransportStatus(*handler, request.path, data); + } + case ManuallyTriggerTransport::Id: { + ManuallyTriggerTransport::DecodableType data; + ReturnErrorOnFailure(data.Decode(input_arguments, fabricIndex)); + return mLogic.HandleManuallyTriggerTransport(*handler, request.path, data); + } + case FindTransport::Id: { + FindTransport::DecodableType data; + ReturnErrorOnFailure(data.Decode(input_arguments, fabricIndex)); + return mLogic.HandleFindTransport(*handler, request.path, data); + } + } + + return Status::UnsupportedCommand; +} + +} // namespace Clusters +} // namespace app +} // namespace chip + +void MatterPushAvStreamTransportClusterServerShutdownCallback(chip::EndpointId endpoint) +{ + ChipLogProgress(Zcl, "Shuting Push AV Stream Transport server cluster on endpoint %d", endpoint); +} diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-cluster.h b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-cluster.h new file mode 100644 index 00000000000000..7ed7ceee73bc9f --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-cluster.h @@ -0,0 +1,81 @@ +/* + * + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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 + +namespace chip { +namespace app { +namespace Clusters { + +/// Integration of Push AV Stream Transport logic within the matter data model +/// +/// Translates between matter calls and Push AV Stream Transport logic +class PushAvStreamTransportServer : public DefaultServerCluster +{ +public: + /** + * @brief Creates a Push AV Stream Transport server instance + * + * This instance needs to be initialized by calling Init() before it can be registered + * and used by the interaction model. + * + * @param aEndpointId The endpoint on which this cluster exists (must match zap configuration) + * @param aFeatures Bitflags indicating which features are supported by this instance + * + * @note The caller must ensure the delegate lives throughout the instance's lifetime + */ + PushAvStreamTransportServer(EndpointId aEndpointId, BitFlags aFeatures) : + DefaultServerCluster({ aEndpointId, PushAvStreamTransport::Id }), mLogic(aEndpointId, aFeatures) + {} + + PushAvStreamTransportServerLogic & GetLogic() { return mLogic; } + + void SetDelegate(EndpointId aEndpoint, PushAvStreamTransportDelegate * delegate) { mLogic.SetDelegate(aEndpoint, delegate); } + + CHIP_ERROR Init() { return mLogic.Init(); } + + void Deinit() { mLogic.Shutdown(); } + + DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, + AttributeValueEncoder & encoder) override; + CHIP_ERROR AcceptedCommands(const ConcreteClusterPath & path, + ReadOnlyBufferBuilder & builder) override; + CHIP_ERROR GeneratedCommands(const ConcreteClusterPath & path, ReadOnlyBufferBuilder & builder) override; + + std::optional InvokeCommand(const DataModel::InvokeRequest & request, + chip::TLV::TLVReader & input_arguments, + CommandHandler * handler) override; + + CHIP_ERROR Attributes(const ConcreteClusterPath & path, ReadOnlyBufferBuilder & builder) override; + +private: + PushAvStreamTransportServerLogic mLogic; + + // Helpers to read list items + CHIP_ERROR ReadAndEncodeCurrentConnections(const AttributeValueEncoder::ListEncodeHelper & encoder, FabricIndex fabricIndex); + CHIP_ERROR ReadAndEncodeSupportedFormats(const AttributeValueEncoder::ListEncodeHelper & encoder); +}; + +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-delegate.h b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-delegate.h new file mode 100644 index 00000000000000..e65804f670a181 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-delegate.h @@ -0,0 +1,237 @@ +/* + * + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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 + +namespace chip { +namespace app { +namespace Clusters { + +/** + * @brief Defines interfaces for implementing application-specific logic for the PushAvStreamTransport Delegate. + * + * This class provides interfaces for command handling and loading of allocated streams. + */ +class PushAvStreamTransportDelegate +{ +public: + PushAvStreamTransportDelegate() = default; + + virtual ~PushAvStreamTransportDelegate() = default; + + void SetEndpointId(EndpointId aEndpoint) { mEndpointId = aEndpoint; } + + /** + * @brief Handles stream transport allocation with the provided transport configuration option. + * + * @param transportOptions The configuration options of the transport to be allocated + * @param connectionID The connectionID to allocate + * @return Success if allocation is successful and a PushTransportConnectionID was produced; + * otherwise, the command is rejected with Failure + * + * The delegate processes the transport following options: + * - URL: Validates the URL + * - StreamUsage, VideoStreamID, AudioStreamID: For selection of Stream + * + * Allocates the transport and maps it to the connectionID. + * On Success, TransportConfigurationStruct is sent as response by the server. + */ + virtual Protocols::InteractionModel::Status + AllocatePushTransport(const PushAvStreamTransport::Structs::TransportOptionsStruct::Type & transportOptions, + const uint16_t connectionID) = 0; + + /** + * @brief Handles stream transport deallocation for the provided connectionID. + * + * @param connectionID The connectionID to deallocate + * @return Success if transport deallocation is successful; + * BUSY if the transport is currently uploading + */ + virtual Protocols::InteractionModel::Status DeallocatePushTransport(const uint16_t connectionID) = 0; + + /** + * @brief Handles stream transport modification. + * + * @param connectionID The connectionID of the stream transport to modify + * @param transportOptions The Transport Options to modify + * @return Success if stream transport modification is successful; + * Failure if modification fails + * + * @note The buffers storing URL, Trigger Options, Motion Zones, Container Options are owned by + * the PushAVStreamTransport Server. The allocated buffers are cleared and reassigned to modified + * transportOptions on success of ModifyPushTransport and deallocated on success of DeallocatePushTransport. + */ + virtual Protocols::InteractionModel::Status + ModifyPushTransport(const uint16_t connectionID, const PushAvStreamTransport::TransportOptionsStorage transportOptions) = 0; + + /** + * @brief Handles stream transport status modification. + * + * @param connectionIDList List of connectionIDs for which new transport status to apply + * @param transportStatus Updated status of the connection(s) + * @return Success if stream transport status is successfully set; + * Failure if status modification fails + * + * Behavior based on transportStatus: + * - Inactive(1): + * - Disables transmissions + * - Removes queued items + * - Cancels active uploads + * - Emits GeneratePushTransportEndEvent + * - Active(0): + * - Enables transmissions + * - For Continuous trigger type: begins transmission immediately + * - For Command/Motion trigger: begins if trigger is active and within Time Control Bounds + * - Emits GeneratePushTransportBeginEvent + */ + virtual Protocols::InteractionModel::Status SetTransportStatus(const std::vector connectionIDList, + PushAvStreamTransport::TransportStatusEnum transportStatus) = 0; + + /** + * @brief Requests manual start of the specified push transport. + * + * @param connectionID The connectionID of the stream transport to trigger + * @param activationReason Information about why the transport was started/stopped + * @param timeControl Configuration to control triggered transport lifecycle + * @return Success if stream transport trigger is successful; + * Failure if trigger fails + * + * @note The server handles PushTransportBegin event emission on success. + * The delegate should emit PushTransportEnd Event using GeneratePushTransportEndEvent() + * when timeControl values indicate end of transmission. + */ + virtual Protocols::InteractionModel::Status ManuallyTriggerTransport( + const uint16_t connectionID, PushAvStreamTransport::TriggerActivationReasonEnum activationReason, + const Optional & timeControl) = 0; + + /** + * @brief Validates the provided URL. + * + * @param url The URL to validate + * @return true if URL is valid, false otherwise + */ + virtual bool ValidateUrl(std::string url) = 0; + + /** + * @brief Validates bandwidth requirements against camera's resource management. + * + * @param streamUsage The desired usage type for the stream (e.g. live view, recording) + * @param videoStreamId Optional identifier for the requested video stream + * @param audioStreamId Optional identifier for the requested audio stream + * @return Status::Success if stream usage is valid; + * Status::ResourceExhausted if resources are insufficient + * + * Ensures the requested stream usage is allowed given current allocation of + * camera resources (CPU, memory, network bandwidth). + */ + virtual Protocols::InteractionModel::Status + ValidateBandwidthLimit(PushAvStreamTransport::StreamUsageEnum streamUsage, + const Optional> & videoStreamId, + const Optional> & audioStreamId) = 0; + + /** + * @brief Assigns existing Video Stream based on camera's resource management and stream priority policies. + * + * @param streamUsage The desired usage type for the stream + * @param videoStreamId Identifier for the requested video stream + * @return Status::Success and selected videoStreamID if successful; + * Status::InvalidStream if no allocated VideoStream exists + */ + virtual Protocols::InteractionModel::Status SelectVideoStream(PushAvStreamTransport::StreamUsageEnum streamUsage, + uint16_t & videoStreamId) = 0; + + /** + * @brief Assigns existing Audio Stream based on camera's resource management and stream priority policies. + * + * @param streamUsage The desired usage type for the stream + * @param audioStreamId Identifier for the requested audio stream + * @return Status::Success and selected audioStreamID if successful; + * Status::InvalidStream if no allocated AudioStream exists + */ + virtual Protocols::InteractionModel::Status SelectAudioStream(PushAvStreamTransport::StreamUsageEnum streamUsage, + uint16_t & audioStreamId) = 0; + + /** + * @brief Validates that the video stream corresponding to videoStreamID is allocated. + * + * @param videoStreamId Identifier for the requested video stream + * @return Status::Success if allocated video stream exists; + * Status::InvalidStream if no allocated video stream with videoStreamID exists + */ + virtual Protocols::InteractionModel::Status ValidateVideoStream(uint16_t videoStreamId) = 0; + + /** + * @brief Validates that the audio stream corresponding to audioStreamID is allocated. + * + * @param audioStreamId Identifier for the requested audio stream + * @return Status::Success if allocated audio stream exists; + * Status::InvalidStream if no allocated audio stream with audioStreamID exists + */ + virtual Protocols::InteractionModel::Status ValidateAudioStream(uint16_t audioStreamId) = 0; + + /** + * @brief Gets the status of the transport. + * + * @param connectionID The connectionID of the stream transport to check status + * @return busy if transport is uploading, idle otherwise + */ + virtual PushAvStreamTransport::PushAvStreamTransportStatusEnum GetTransportBusyStatus(const uint16_t connectionID) = 0; + + /** + * @brief Delegate callback for notifying change in an attribute. + * + * @param attributeId The ID of the attribute that changed + */ + virtual void OnAttributeChanged(AttributeId attributeId) = 0; + + /** + * @brief Loads pre-allocated transport connections into the cluster server list. + * + * @param currentConnections Vector to store loaded transport configurations + * @return CHIP_ERROR indicating success or failure + * + * The delegate application is responsible for creating and persisting connections + * based on Allocation commands. These connections' context information is loaded + * into the cluster server list at initialization for serving attribute reads. + * The list can be updated via Add/Remove functions for respective transport connections. + * + * @note Required buffers are managed by TransportConfigurationStorage; + * the delegate function must populate the vector correctly. + */ + virtual CHIP_ERROR + LoadCurrentConnections(std::vector & currentConnections) = 0; + + /** + * @brief Callback after persistent attributes managed by the Cluster are loaded from Storage. + * + * @return CHIP_ERROR indicating success or failure + */ + virtual CHIP_ERROR PersistentAttributesLoadedCallback() = 0; + +protected: + EndpointId mEndpointId = kInvalidEndpointId; +}; +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.cpp b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.cpp new file mode 100644 index 00000000000000..db2562e7a46ac0 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.cpp @@ -0,0 +1,1048 @@ +/** + * + * Copyright (c) 2025 Project CHIP Authors + * + * 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 "constants.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr uint16_t kMaxConnectionId = 65535; // This is also invalid connectionID + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::PushAvStreamTransport; +using namespace chip::app::Clusters::PushAvStreamTransport::Structs; +using namespace chip::app::Clusters::PushAvStreamTransport::Attributes; +using namespace Protocols::InteractionModel; +using chip::Protocols::InteractionModel::Status; + +namespace chip { +namespace app { +namespace Clusters { + +PushAvStreamTransportServerLogic::PushAvStreamTransportServerLogic(EndpointId aEndpoint, BitFlags aFeatures) : + mEndpointId(aEndpoint), mFeatures(aFeatures), + mSupportedFormats{ PushAvStreamTransport::SupportedFormatStruct{ PushAvStreamTransport::ContainerFormatEnum::kCmaf, + PushAvStreamTransport::IngestMethodsEnum::kCMAFIngest } } +{} + +PushAvStreamTransportServerLogic::~PushAvStreamTransportServerLogic() +{ + for (const auto & timerContext : mTimerContexts) + { + DeviceLayer::SystemLayer().CancelTimer(PushAVStreamTransportDeallocateCallback, static_cast(timerContext.get())); + } + Shutdown(); +} + +CHIP_ERROR PushAvStreamTransportServerLogic::Init() +{ + LoadPersistentAttributes(); + return CHIP_NO_ERROR; +} + +void PushAvStreamTransportServerLogic::Shutdown() {} + +bool PushAvStreamTransportServerLogic::HasFeature(Feature feature) const +{ + return mFeatures.Has(feature); +} + +bool PushAvStreamTransportServerLogic::IsNullDelegateWithLogging(EndpointId endpointIdForLogging) +{ + + if (mDelegate == nullptr) + { + ChipLogError(Zcl, "No PushAvStreamTransportDelegate set for ep:%u", endpointIdForLogging); + return true; + } + return false; +} + +PushAvStreamTransportServerLogic::UpsertResultEnum +PushAvStreamTransportServerLogic::UpsertStreamTransportConnection(const TransportConfigurationStorage & transportConfiguration) +{ + UpsertResultEnum result; + auto it = + std::find_if(mCurrentConnections.begin(), mCurrentConnections.end(), + [id = transportConfiguration.connectionID](const auto & existing) { return existing.connectionID == id; }); + + if (it != mCurrentConnections.end()) + { + *it = transportConfiguration; + result = UpsertResultEnum::kUpdated; + } + else + { + mCurrentConnections.push_back(transportConfiguration); + result = UpsertResultEnum::kInserted; + } + + MatterReportingAttributeChangeCallback(mEndpointId, PushAvStreamTransport::Id, + PushAvStreamTransport::Attributes::CurrentConnections::Id); + + return result; +} + +PushAvStreamTransportServerLogic::UpsertResultEnum +PushAvStreamTransportServerLogic::UpsertTimerAppState(std::shared_ptr timerAppState) +{ + auto it = std::find_if(mTimerContexts.begin(), mTimerContexts.end(), + [id = timerAppState->connectionID](const auto & existing) { return existing->connectionID == id; }); + + assert(it == mTimerContexts.end()); + + mTimerContexts.push_back(timerAppState); + return UpsertResultEnum::kInserted; +} + +void PushAvStreamTransportServerLogic::RemoveStreamTransportConnection(const uint16_t transportConnectionId) +{ + size_t originalSize = mCurrentConnections.size(); + + // Erase-Remove idiom + mCurrentConnections.erase(std::remove_if(mCurrentConnections.begin(), mCurrentConnections.end(), + [transportConnectionId](const TransportConfigurationStorage & s) { + return s.connectionID == transportConnectionId; + }), + mCurrentConnections.end()); + + // If a connection was removed, the size will be smaller. + if (mCurrentConnections.size() < originalSize) + { + // Notify the stack that the CurrentConnections attribute has changed. + MatterReportingAttributeChangeCallback(mEndpointId, PushAvStreamTransport::Id, + PushAvStreamTransport::Attributes::CurrentConnections::Id); + } +} + +void PushAvStreamTransportServerLogic::RemoveTimerAppState(const uint16_t connectionID) +{ + // Erase-Remove idiom + auto it = std::remove_if(mTimerContexts.begin(), mTimerContexts.end(), + [connectionID](const std::shared_ptr & s) { + if (s->connectionID == connectionID) + { + DeviceLayer::SystemLayer().CancelTimer(PushAVStreamTransportDeallocateCallback, + static_cast(s.get())); + return true; // Remove from vector + } + return false; // Keep in vector + }); + + mTimerContexts.erase(it, mTimerContexts.end()); +} + +void PushAvStreamTransportServerLogic::LoadPersistentAttributes() +{ + CHIP_ERROR err = CHIP_NO_ERROR; + + // Load currentConnections + err = mDelegate->LoadCurrentConnections(mCurrentConnections); + if (err != CHIP_NO_ERROR) + { + ChipLogDetail(Zcl, "PushAVStreamTransport: Unable to load allocated connections from the KVS."); + } + + // Signal delegate that all persistent configuration attributes have been loaded. + mDelegate->PersistentAttributesLoadedCallback(); +} + +TransportConfigurationStorage * PushAvStreamTransportServerLogic::FindStreamTransportConnection(const uint16_t connectionID) +{ + for (auto & transportConnection : mCurrentConnections) + { + if (transportConnection.connectionID == connectionID) + { + return &transportConnection; + } + } + return nullptr; +} + +TransportConfigurationStorage * +PushAvStreamTransportServerLogic::FindStreamTransportConnectionWithinFabric(const uint16_t connectionID, FabricIndex fabricIndex) +{ + for (auto & transportConnection : mCurrentConnections) + { + if (transportConnection.connectionID == connectionID) + { + if (transportConnection.GetFabricIndex() == fabricIndex) + { + return &transportConnection; + } + } + } + return nullptr; +} + +uint16_t PushAvStreamTransportServerLogic::GenerateConnectionID() +{ + static uint16_t lastID = 0; + + for (uint16_t i = 0; i < kMaxConnectionId; ++i) + { + uint16_t candidateID = static_cast((lastID + i + 1) % kMaxConnectionId); // Wrap from 0 to 65534 + if (FindStreamTransportConnection(candidateID) == nullptr) + { + lastID = candidateID; + return candidateID; + } + } + + return kMaxConnectionId; // All 0 to 65534 IDs are in use +} + +void PushAvStreamTransportServerLogic::PushAVStreamTransportDeallocateCallback(System::Layer *, void * callbackContext) +{ + PushAVStreamTransportDeallocateCallbackContext * transportDeallocateContext = + static_cast((callbackContext)); + + uint16_t connectionID = transportDeallocateContext->connectionID; + + // Call the delegate + auto delegateStatus = Protocols::InteractionModel::ClusterStatusCode( + transportDeallocateContext->instance->mDelegate->DeallocatePushTransport(connectionID)); + + if (delegateStatus.IsSuccess()) + { + ChipLogProgress(Zcl, "Push AV Stream Transport Deallocate timer expired. %s", "Deallocating"); + + // Remove connection from CurrentConnections + transportDeallocateContext->instance->RemoveStreamTransportConnection(connectionID); + transportDeallocateContext->instance->RemoveTimerAppState(connectionID); + } + else + { + ChipLogError(Zcl, "Push AV Stream Transport Deallocate timer expired. %s", "Deallocation Failed"); + } +} + +CHIP_ERROR PushAvStreamTransportServerLogic::ScheduleTransportDeallocate(uint16_t connectionID, uint32_t timeoutSec) +{ + uint32_t timeoutMs = timeoutSec * MILLISECOND_TICKS_PER_SECOND; + + std::shared_ptr transportDeallocateContext{ new ( + std::nothrow) PushAVStreamTransportDeallocateCallbackContext{ this, connectionID } }; + + if (transportDeallocateContext == nullptr) + { + ChipLogError(Zcl, "Failed to allocate memory for deallocate context"); + return CHIP_ERROR_NO_MEMORY; + } + + CHIP_ERROR err = DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Milliseconds32(timeoutMs), + PushAVStreamTransportDeallocateCallback, + static_cast(transportDeallocateContext.get())); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "Failed to schedule deallocate: timeout=%" PRIu32 ", status=%" CHIP_ERROR_FORMAT, timeoutSec, + err.Format()); + } + else + { + UpsertTimerAppState(transportDeallocateContext); + } + + return err; +} + +Status PushAvStreamTransportServerLogic::ValidateIncomingTransportOptions( + const Structs::TransportOptionsStruct::DecodableType & transportOptions) +{ + // Contraints check on incoming transport Options + VerifyOrReturnValue( + transportOptions.streamUsage != StreamUsageEnum::kUnknownEnumValue, Status::ConstraintError, + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Invalid streamUsage ", mEndpointId)); + + VerifyOrReturnValue( + transportOptions.videoStreamID.HasValue() || transportOptions.audioStreamID.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Missing videoStreamID and audioStreamID", + mEndpointId)); + + VerifyOrReturnValue(transportOptions.url.size() >= kMinUrlLength && transportOptions.url.size() <= kMaxUrlLength, + Status::ConstraintError, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: URL length: %" PRIu32 + " not in allowed length range of 13 to 2000", + mEndpointId, static_cast(transportOptions.url.size()))); + + auto & triggerOptions = transportOptions.triggerOptions; + + VerifyOrReturnValue( + triggerOptions.triggerType != TransportTriggerTypeEnum::kUnknownEnumValue, Status::ConstraintError, + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Invalid triggerType ", mEndpointId)); + + if (triggerOptions.triggerType == TransportTriggerTypeEnum::kMotion) + { + VerifyOrReturnValue( + triggerOptions.motionZones.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Missing motion zones ", mEndpointId)); + + if (!triggerOptions.motionZones.Value().IsNull()) + { + auto & motionZonesList = triggerOptions.motionZones; + auto iter = motionZonesList.Value().Value().begin(); + + while (iter.Next()) + { + auto & transportZoneOption = iter.GetValue(); + + if (mFeatures.Has(Feature::kPerZoneSensitivity)) + { + VerifyOrReturnValue( + transportZoneOption.sensitivity.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Missing Zone Sensitivity ", + mEndpointId)); + + VerifyOrReturnValue( + transportZoneOption.sensitivity.Value() >= 1 && transportZoneOption.sensitivity.Value() <= 10, + Status::ConstraintError, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: Zone Sensitivity Constraint Error", + mEndpointId)); + } + else + { + VerifyOrReturnValue(!transportZoneOption.sensitivity.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: Found Zone " + "Sensitivity which is not expected.", + mEndpointId)); + } + } + + if (iter.GetStatus() != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Motion Zones TLV Validation failed", + mEndpointId); + return Status::InvalidCommand; + } + } + + VerifyOrReturnValue(triggerOptions.motionTimeControl.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: Missing Motion Time Control ", + mEndpointId)); + + VerifyOrReturnValue( + triggerOptions.motionTimeControl.Value().initialDuration >= 1, Status::ConstraintError, + ChipLogError( + Zcl, + "Transport Options verification from command data[ep=%d]: Motion Time Control (InitialDuration) Constraint Error", + mEndpointId)); + + VerifyOrReturnValue( + triggerOptions.motionTimeControl.Value().maxDuration >= 1, Status::ConstraintError, + ChipLogError( + Zcl, "Transport Options verification from command data[ep=%d]: Motion Time Control (MaxDuration) Constraint Error", + mEndpointId)); + } + else + { + + VerifyOrReturnValue( + !triggerOptions.motionZones.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Found motion zones which is not expected", + mEndpointId)); + + VerifyOrReturnValue( + !triggerOptions.motionTimeControl.HasValue(), Status::InvalidCommand, + ChipLogError( + Zcl, "Transport Options verification from command data[ep=%d]: Found Motion Time Control which is not expected ", + mEndpointId)); + } + + if (triggerOptions.triggerType == TransportTriggerTypeEnum::kMotion && !mFeatures.Has(Feature::kPerZoneSensitivity)) + { + VerifyOrReturnValue( + triggerOptions.motionSensitivity.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Missing Motion Sensitivity ", mEndpointId)); + + VerifyOrReturnValue( + triggerOptions.motionSensitivity.Value().Value() >= 1 && triggerOptions.motionSensitivity.Value().Value() <= 10, + Status::ConstraintError, + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Motion Sensitivity Constraint Error", + mEndpointId)); + } + else + { + VerifyOrReturnValue( + !triggerOptions.motionSensitivity.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: Found Motion Sensitivity which is not expected ", + mEndpointId)); + } + + if (triggerOptions.triggerType == TransportTriggerTypeEnum::kMotion || + triggerOptions.triggerType == TransportTriggerTypeEnum::kCommand) + { + VerifyOrReturnValue(triggerOptions.maxPreRollLen.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: Missing Max Pre Roll Len field ", + mEndpointId)); + } + else + { + VerifyOrReturnValue( + !triggerOptions.maxPreRollLen.HasValue(), Status::InvalidCommand, + ChipLogError( + Zcl, "Transport Options verification from command data[ep=%d]: Found Max Pre Roll Len field which is not expected.", + mEndpointId)); + } + + IngestMethodsEnum ingestMethod = transportOptions.ingestMethod; + + VerifyOrReturnValue( + ingestMethod != IngestMethodsEnum::kUnknownEnumValue, Status::ConstraintError, + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Invalid Ingest Method ", mEndpointId)); + + const Structs::ContainerOptionsStruct::Type & containerOptions = transportOptions.containerOptions; + + VerifyOrReturnValue( + containerOptions.containerType != ContainerFormatEnum::kUnknownEnumValue, Status::ConstraintError, + ChipLogError(Zcl, "Transport Options verification from command data[ep=%d]: Invalid Container Format ", mEndpointId)); + + if (containerOptions.containerType == ContainerFormatEnum::kCmaf) + { + VerifyOrReturnValue(containerOptions.CMAFContainerOptions.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: Missing CMAF Container Options ", + mEndpointId)); + + if (containerOptions.CMAFContainerOptions.Value().CENCKey.HasValue()) + { + VerifyOrReturnValue( + containerOptions.CMAFContainerOptions.Value().CENCKey.Value().size() == kMaxCENCKeyLength, Status::ConstraintError, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: CMAF Container Options CENC Key constraint " + "Error, actual length: %" PRIu32 " not " + "equal to expected length of 16", + mEndpointId, + static_cast(containerOptions.CMAFContainerOptions.Value().CENCKey.Value().size()))); + } + + if (!mFeatures.Has(Feature::kMetadata)) + { + VerifyOrReturnValue(!containerOptions.CMAFContainerOptions.Value().metadataEnabled.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: Found CMAF Container " + "Options MetadataEnabled which is not expected.", + mEndpointId)); + } + + if (containerOptions.CMAFContainerOptions.Value().CENCKey.HasValue()) + { + VerifyOrReturnValue( + containerOptions.CMAFContainerOptions.Value().CENCKeyID.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: Missing CMAF Container Options CENC Key ID ", + mEndpointId)); + + VerifyOrReturnValue( + containerOptions.CMAFContainerOptions.Value().CENCKeyID.Value().size() == kMaxCENCKeyIDLength, + Status::ConstraintError, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: CMAF Container Options CENC Key ID " + "constraint Error, actual " + "length: %" PRIu32 " not equal to expected length of 16", + mEndpointId, + static_cast(containerOptions.CMAFContainerOptions.Value().CENCKeyID.Value().size()))); + } + else + { + VerifyOrReturnValue(!containerOptions.CMAFContainerOptions.Value().CENCKeyID.HasValue(), Status::InvalidCommand, + ChipLogError(Zcl, + "Transport Options verification from command data[ep=%d]: Found CMAF Container " + "Options CENC Key ID which is not expected", + mEndpointId)); + } + } + else + { + VerifyOrReturnValue( + !containerOptions.CMAFContainerOptions.HasValue(), Status::InvalidCommand, + ChipLogError( + Zcl, "Transport Options verification from command data[ep=%d]: Found CMAF Container Options which is not expected ", + mEndpointId)); + } + + return Status::Success; +} + +std::optional +PushAvStreamTransportServerLogic::HandleAllocatePushTransport(CommandHandler & handler, const ConcreteCommandPath & commandPath, + const Commands::AllocatePushTransport::DecodableType & commandData) +{ + if (IsNullDelegateWithLogging(commandPath.mEndpointId)) + { + return Status::UnsupportedCommand; + } + + Commands::AllocatePushTransportResponse::Type response; + auto & transportOptions = commandData.transportOptions; + + Status transportOptionsValidityStatus = ValidateIncomingTransportOptions(transportOptions); + + VerifyOrDo(transportOptionsValidityStatus == Status::Success, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: TransportOptions of command data is not Valid", mEndpointId); + handler.AddStatus(commandPath, transportOptionsValidityStatus); + return transportOptionsValidityStatus; + }); + + // Todo: TLSEndpointID Validation + + IngestMethodsEnum ingestMethod = commandData.transportOptions.ingestMethod; + + Structs::ContainerOptionsStruct::Type containerOptions = commandData.transportOptions.containerOptions; + + bool isFormatSupported = false; + + for (auto & supportsFormat : mSupportedFormats) + { + if ((supportsFormat.ingestMethod == ingestMethod) && (supportsFormat.containerFormat == containerOptions.containerType)) + { + isFormatSupported = true; + } + } + + if (isFormatSupported == false) + { + auto status = to_underlying(StatusCodeEnum::kInvalidCombination); + ChipLogError(Zcl, + "HandleAllocatePushTransport[ep=%d]: Invalid Ingest Method and Container Format Combination : (Ingest Method: " + "%02X and Container Format: %02X)", + mEndpointId, to_underlying(ingestMethod), to_underlying(containerOptions.containerType)); + handler.AddClusterSpecificFailure(commandPath, status); + return std::nullopt; + } + + bool isValidUrl = mDelegate->ValidateUrl(std::string(transportOptions.url.data(), transportOptions.url.size())); + + if (isValidUrl == false) + { + auto status = to_underlying(StatusCodeEnum::kInvalidURL); + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid Url", mEndpointId); + handler.AddClusterSpecificFailure(commandPath, status); + return std::nullopt; + } + + /*Spec issue for invalid Trigger Type: https://github.com/CHIP-Specifications/connectedhomeip-spec/issues/11701*/ + if (transportOptions.triggerOptions.triggerType == TransportTriggerTypeEnum::kUnknownEnumValue) + { + auto status = to_underlying(StatusCodeEnum::kInvalidTriggerType); + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid Trigger type", mEndpointId); + handler.AddClusterSpecificFailure(commandPath, status); + return std::nullopt; + } + + // Todo:Validate ZoneId + + // Validate Bandwidth Requirement + Status status = mDelegate->ValidateBandwidthLimit(transportOptions.streamUsage, transportOptions.videoStreamID, + transportOptions.audioStreamID); + if (status != Status::Success) + { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Resource Exhausted", mEndpointId); + handler.AddStatus(commandPath, status); + return std::nullopt; + } + + std::shared_ptr transportOptionsPtr{ new (std::nothrow) TransportOptionsStorage(transportOptions) }; + + if (transportOptionsPtr == nullptr) + { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Memory Allocation failed for transportOptions", mEndpointId); + handler.AddStatus(commandPath, Status::ResourceExhausted); + return std::nullopt; + } + + if (transportOptions.videoStreamID.HasValue()) + { + if (transportOptions.videoStreamID.Value().IsNull()) + { + uint16_t videoStreamID; + + auto delegateStatus = Protocols::InteractionModel::ClusterStatusCode( + mDelegate->SelectVideoStream(transportOptions.streamUsage, videoStreamID)); + + if (!delegateStatus.IsSuccess()) + { + handler.AddStatus(commandPath, delegateStatus); + return std::nullopt; + } + + transportOptionsPtr->videoStreamID.SetValue(videoStreamID); + } + else + { + auto delegateStatus = Protocols::InteractionModel::ClusterStatusCode( + mDelegate->ValidateVideoStream(transportOptions.videoStreamID.Value().Value())); + + if (!delegateStatus.IsSuccess()) + { + handler.AddStatus(commandPath, delegateStatus); + return std::nullopt; + } + } + } + + if (transportOptions.audioStreamID.HasValue()) + { + if (transportOptions.audioStreamID.Value().IsNull()) + { + uint16_t audioStreamID; + + auto delegateStatus = Protocols::InteractionModel::ClusterStatusCode( + mDelegate->SelectAudioStream(transportOptions.streamUsage, audioStreamID)); + + if (!delegateStatus.IsSuccess()) + { + handler.AddStatus(commandPath, delegateStatus); + return std::nullopt; + } + + transportOptionsPtr->audioStreamID.SetValue(audioStreamID); + } + else + { + auto delegateStatus = Protocols::InteractionModel::ClusterStatusCode( + mDelegate->ValidateAudioStream(transportOptions.videoStreamID.Value().Value())); + + if (!delegateStatus.IsSuccess()) + { + handler.AddStatus(commandPath, delegateStatus); + return std::nullopt; + } + } + } + + uint16_t connectionID = GenerateConnectionID(); + + if (connectionID == kMaxConnectionId) + { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Max Connections Exhausted", mEndpointId); + handler.AddStatus(commandPath, Status::ResourceExhausted); + return std::nullopt; + } + + status = mDelegate->AllocatePushTransport(*transportOptionsPtr, connectionID); + + if (status == Status::Success) + { + // add connection to CurrentConnections + FabricIndex peerFabricIndex = handler.GetAccessingFabricIndex(); + + TransportConfigurationStorage outTransportConfiguration(connectionID, transportOptionsPtr); + + outTransportConfiguration.transportStatus = TransportStatusEnum::kInactive; + + outTransportConfiguration.SetFabricIndex(peerFabricIndex); + + UpsertStreamTransportConnection(outTransportConfiguration); + + response.transportConfiguration = outTransportConfiguration; + + // ExpiryTime Handling + if (transportOptions.expiryTime.HasValue()) + { + ScheduleTransportDeallocate(connectionID, transportOptions.expiryTime.Value()); + } + + handler.AddResponse(commandPath, response); + } + else + { + handler.AddStatus(commandPath, status); + } + + return std::nullopt; +} + +std::optional PushAvStreamTransportServerLogic::HandleDeallocatePushTransport( + CommandHandler & handler, const ConcreteCommandPath & commandPath, + const Commands::DeallocatePushTransport::DecodableType & commandData) +{ + if (IsNullDelegateWithLogging(commandPath.mEndpointId)) + { + return Status::UnsupportedCommand; + } + + uint16_t connectionID = commandData.connectionID; + FabricIndex FabricIndex = handler.GetAccessingFabricIndex(); + TransportConfigurationStorage * transportConfiguration = FindStreamTransportConnectionWithinFabric(connectionID, FabricIndex); + if (transportConfiguration == nullptr) + { + ChipLogError(Zcl, "HandleDeallocatePushTransport[ep=%d]: ConnectionID Not Found.", mEndpointId); + handler.AddStatus(commandPath, Status::NotFound); + return std::nullopt; + } + + // Call the delegate + auto delegateStatus = Protocols::InteractionModel::ClusterStatusCode(mDelegate->DeallocatePushTransport(connectionID)); + + if (delegateStatus.IsSuccess()) + { + // Remove connection from CurrentConnections + RemoveStreamTransportConnection(connectionID); + RemoveTimerAppState(connectionID); + } + + handler.AddStatus(commandPath, delegateStatus); + + return std::nullopt; +} + +std::optional +PushAvStreamTransportServerLogic::HandleModifyPushTransport(CommandHandler & handler, const ConcreteCommandPath & commandPath, + const Commands::ModifyPushTransport::DecodableType & commandData) +{ + if (IsNullDelegateWithLogging(commandPath.mEndpointId)) + { + return Status::UnsupportedCommand; + } + + Status status = Status::Success; + uint16_t connectionID = commandData.connectionID; + auto & transportOptions = commandData.transportOptions; + + // Contraints check on incoming transport Options + Status transportOptionsValidityStatus = ValidateIncomingTransportOptions(transportOptions); + + VerifyOrDo(transportOptionsValidityStatus == Status::Success, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: TransportOptions of command data is not Valid", mEndpointId); + handler.AddStatus(commandPath, transportOptionsValidityStatus); + }); + + FabricIndex fabricIndex = handler.GetAccessingFabricIndex(); + + TransportConfigurationStorage * transportConfiguration = FindStreamTransportConnectionWithinFabric(connectionID, fabricIndex); + + if (transportConfiguration == nullptr) + { + ChipLogError(Zcl, "HandleModifyPushTransport[ep=%d]: ConnectionID Not Found.", mEndpointId); + handler.AddStatus(commandPath, Status::NotFound); + return std::nullopt; + } + + if (mDelegate->GetTransportBusyStatus(connectionID) == PushAvStreamTransportStatusEnum::kBusy) + { + ChipLogError(Zcl, "HandleModifyPushTransport[ep=%d]: Connection is Busy", mEndpointId); + handler.AddStatus(commandPath, Status::Busy); + return std::nullopt; + } + + std::shared_ptr transportOptionsPtr{ new (std::nothrow) TransportOptionsStorage(transportOptions) }; + + if (transportOptionsPtr == nullptr) + { + ChipLogError(Zcl, "HandleModifyPushTransport[ep=%d]: Memory Allocation failed for transportOptions", mEndpointId); + handler.AddStatus(commandPath, Status::ResourceExhausted); + return std::nullopt; + } + // Call the delegate + status = mDelegate->ModifyPushTransport(connectionID, *transportOptionsPtr); + + if (status == Status::Success) + { + transportConfiguration->SetTransportOptionsPtr(transportOptionsPtr); + } + + handler.AddStatus(commandPath, status); + + return std::nullopt; +} + +std::optional +PushAvStreamTransportServerLogic::HandleSetTransportStatus(CommandHandler & handler, const ConcreteCommandPath & commandPath, + const Commands::SetTransportStatus::DecodableType & commandData) +{ + if (IsNullDelegateWithLogging(commandPath.mEndpointId)) + { + return Status::UnsupportedCommand; + } + + Status status = Status::Success; + DataModel::Nullable connectionID = commandData.connectionID; + auto & transportStatus = commandData.transportStatus; + + VerifyOrDo(transportStatus != TransportStatusEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Motion Time Control (MaxDuration) Constraint Error", mEndpointId); + handler.AddStatus(commandPath, Status::ConstraintError); + }); + + std::vector connectionIDList; + + if (connectionID.IsNull()) + { + for (auto & transportConnection : mCurrentConnections) + { + if (transportConnection.fabricIndex == handler.GetAccessingFabricIndex()) + { + connectionIDList.push_back(transportConnection.connectionID); + } + } + } + else + { + FabricIndex fabricIndex = handler.GetAccessingFabricIndex(); + TransportConfigurationStorage * transportConfiguration = + FindStreamTransportConnectionWithinFabric(connectionID.Value(), fabricIndex); + if (transportConfiguration == nullptr) + { + ChipLogError(Zcl, "HandleSetTransportStatus[ep=%d]: ConnectionID Not Found.", mEndpointId); + handler.AddStatus(commandPath, Status::NotFound); + return std::nullopt; + } + connectionIDList.push_back(connectionID.Value()); + } + // Call the delegate + status = mDelegate->SetTransportStatus(connectionIDList, transportStatus); + if (status == Status::Success) + { + for (auto & connID : connectionIDList) + { + for (auto & transportConnection : mCurrentConnections) + { + if (transportConnection.connectionID == connID) + { + transportConnection.transportStatus = transportStatus; + } + } + } + } + handler.AddStatus(commandPath, status); + + return std::nullopt; +} + +std::optional PushAvStreamTransportServerLogic::HandleManuallyTriggerTransport( + CommandHandler & handler, const ConcreteCommandPath & commandPath, + const Commands::ManuallyTriggerTransport::DecodableType & commandData) +{ + if (IsNullDelegateWithLogging(commandPath.mEndpointId)) + { + return Status::UnsupportedCommand; + } + + Status status = Status::Success; + uint16_t connectionID = commandData.connectionID; + auto & activationReason = commandData.activationReason; + + VerifyOrDo(activationReason != TriggerActivationReasonEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Invalid Activation Reason ", mEndpointId); + handler.AddStatus(commandPath, Status::ConstraintError); + }); + + Optional timeControl = commandData.timeControl; + + if (timeControl.HasValue()) + { + VerifyOrDo(timeControl.Value().initialDuration >= 1, { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Motion Time Control (InitialDuration) Constraint Error", + mEndpointId); + handler.AddStatus(commandPath, Status::ConstraintError); + }); + + VerifyOrDo(timeControl.Value().maxDuration >= 1, { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Motion Time Control (MaxDuration) Constraint Error", + mEndpointId); + handler.AddStatus(commandPath, Status::ConstraintError); + }); + } + + FabricIndex fabricIndex = handler.GetAccessingFabricIndex(); + TransportConfigurationStorage * transportConfiguration = FindStreamTransportConnectionWithinFabric(connectionID, fabricIndex); + + if (transportConfiguration == nullptr) + { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: ConnectionID Not Found.", mEndpointId); + handler.AddStatus(commandPath, Status::NotFound); + return std::nullopt; + } + + if (mDelegate->GetTransportBusyStatus(connectionID) == PushAvStreamTransportStatusEnum::kBusy) + { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Connection is Busy", mEndpointId); + handler.AddStatus(commandPath, Status::Busy); + return std::nullopt; + } + + if (transportConfiguration->transportStatus == TransportStatusEnum::kInactive) + { + auto clusterStatus = to_underlying(StatusCodeEnum::kInvalidTransportStatus); + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Invalid Transport status", mEndpointId); + handler.AddClusterSpecificFailure(commandPath, clusterStatus); + return std::nullopt; + } + if (transportConfiguration->transportOptions.HasValue()) + { + if (transportConfiguration->transportOptions.Value().triggerOptions.triggerType == TransportTriggerTypeEnum::kContinuous) + { + + auto clusterStatus = to_underlying(StatusCodeEnum::kInvalidTriggerType); + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Invalid Trigger type", mEndpointId); + handler.AddClusterSpecificFailure(commandPath, clusterStatus); + return std::nullopt; + } + if (transportConfiguration->transportOptions.Value().triggerOptions.triggerType == TransportTriggerTypeEnum::kCommand && + !timeControl.HasValue()) + { + + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Time control field not present", mEndpointId); + handler.AddStatus(commandPath, Status::DynamicConstraintError); + return std::nullopt; + } + } + + // When trigger type is motion in the allocated transport but triggering it manually + if (!timeControl.HasValue()) + { + timeControl = transportConfiguration->transportOptions.Value().triggerOptions.motionTimeControl; + } + + // Call the delegate + status = mDelegate->ManuallyTriggerTransport(connectionID, activationReason, timeControl); + + if (status == Status::Success) + { + GeneratePushTransportBeginEvent(connectionID, TransportTriggerTypeEnum::kCommand, MakeOptional(activationReason)); + } + + handler.AddStatus(commandPath, status); + + return std::nullopt; +} + +std::optional +PushAvStreamTransportServerLogic::HandleFindTransport(CommandHandler & handler, const ConcreteCommandPath & commandPath, + const Commands::FindTransport::DecodableType & commandData) +{ + if (IsNullDelegateWithLogging(commandPath.mEndpointId)) + { + return Status::UnsupportedCommand; + } + + Commands::FindTransportResponse::Type response; + + Optional> connectionID = commandData.connectionID; + + std::vector transportConfigurations; + + if (!connectionID.HasValue() || connectionID.Value().IsNull()) + { + if (mCurrentConnections.size() == 0) + { + ChipLogError(Zcl, "HandleFindTransport[ep=%d]: ConnectionID not found", mEndpointId); + handler.AddStatus(commandPath, Status::NotFound); + return std::nullopt; + } + + for (auto & connection : mCurrentConnections) + { + if (connection.fabricIndex == handler.GetAccessingFabricIndex()) + { + transportConfigurations.push_back(connection); + } + } + } + else + { + FabricIndex fabricIndex = handler.GetAccessingFabricIndex(); + TransportConfigurationStorage * transportConfiguration = + FindStreamTransportConnectionWithinFabric(connectionID.Value().Value(), fabricIndex); + if (transportConfiguration == nullptr) + { + ChipLogError(Zcl, "HandleFindTransport[ep=%d]: ConnectionID not found", mEndpointId); + handler.AddStatus(commandPath, Status::NotFound); + return std::nullopt; + } + + transportConfigurations.push_back(*transportConfiguration); + } + + if (transportConfigurations.size() == 0) + { + handler.AddStatus(commandPath, Status::NotFound); + } + + response.transportConfigurations = DataModel::List( + transportConfigurations.data(), transportConfigurations.size()); + + handler.AddResponse(commandPath, response); + + return std::nullopt; +} + +Status +PushAvStreamTransportServerLogic::GeneratePushTransportBeginEvent(const uint16_t connectionID, + const TransportTriggerTypeEnum triggerType, + const Optional activationReason) +{ + Events::PushTransportBegin::Type event; + EventNumber eventNumber; + + event.connectionID = connectionID; + event.triggerType = triggerType; + event.activationReason = activationReason; + + CHIP_ERROR err = LogEvent(event, mEndpointId, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Endpoint %d - Unable to generate PushAVTransportBegin event: %" CHIP_ERROR_FORMAT, mEndpointId, + err.Format()); + return Status::Failure; + } + return Status::Success; +} + +Status PushAvStreamTransportServerLogic::GeneratePushTransportEndEvent(const uint16_t connectionID, + const TransportTriggerTypeEnum triggerType, + const Optional activationReason) +{ + Events::PushTransportEnd::Type event; + EventNumber eventNumber; + + event.connectionID = connectionID; + event.triggerType = triggerType; + event.activationReason = activationReason; + + CHIP_ERROR err = LogEvent(event, mEndpointId, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Endpoint %d - Unable to generate PushAVTransportEnd event: %" CHIP_ERROR_FORMAT, mEndpointId, + err.Format()); + return Status::Failure; + } + return Status::Success; +} + +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.h b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.h new file mode 100644 index 00000000000000..88d60638f80bfd --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.h @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace Clusters { + +class PushAvStreamTransportServerLogic +{ +public: + PushAvStreamTransportServerLogic(EndpointId aEndpoint, BitFlags aFeatures); + ~PushAvStreamTransportServerLogic(); + + void SetDelegate(EndpointId aEndpoint, PushAvStreamTransportDelegate * delegate) + { + mDelegate = delegate; + if (mDelegate == nullptr) + { + ChipLogError(Zcl, "Push AV Stream Transport: Delegate is null"); + return; + } + mDelegate->SetEndpointId(aEndpoint); + } + + enum class UpsertResultEnum : uint8_t + { + kInserted = 0x00, + kUpdated = 0x01, + }; + + struct PushAVStreamTransportDeallocateCallbackContext + { + PushAvStreamTransportServerLogic * instance; + uint16_t connectionID; + }; + + EndpointId mEndpointId = kInvalidEndpointId; + + std::vector> mTimerContexts; + + BitFlags mFeatures; + + std::vector mSupportedFormats; + + std::vector mCurrentConnections; + + CHIP_ERROR Init(); + + void Shutdown(); + + bool HasFeature(PushAvStreamTransport::Feature feature) const; + + Protocols::InteractionModel::Status ValidateIncomingTransportOptions( + const PushAvStreamTransport::Structs::TransportOptionsStruct::DecodableType & transportOptions); + + std::optional + HandleAllocatePushTransport(CommandHandler & handler, const ConcreteCommandPath & commandPath, + const PushAvStreamTransport::Commands::AllocatePushTransport::DecodableType & commandData); + + std::optional + HandleDeallocatePushTransport(CommandHandler & handler, const ConcreteCommandPath & commandPath, + const PushAvStreamTransport::Commands::DeallocatePushTransport::DecodableType & commandData); + + std::optional + HandleModifyPushTransport(CommandHandler & handler, const ConcreteCommandPath & commandPath, + const PushAvStreamTransport::Commands::ModifyPushTransport::DecodableType & commandData); + + std::optional + HandleSetTransportStatus(CommandHandler & handler, const ConcreteCommandPath & commandPath, + const PushAvStreamTransport::Commands::SetTransportStatus::DecodableType & commandData); + + std::optional + HandleManuallyTriggerTransport(CommandHandler & handler, const ConcreteCommandPath & commandPath, + const PushAvStreamTransport::Commands::ManuallyTriggerTransport::DecodableType & commandData); + + std::optional + HandleFindTransport(CommandHandler & handler, const ConcreteCommandPath & commandPath, + const PushAvStreamTransport::Commands::FindTransport::DecodableType & commandData); + + // Send Push AV Stream Transport events + Protocols::InteractionModel::Status + GeneratePushTransportBeginEvent(const uint16_t connectionID, const PushAvStreamTransport::TransportTriggerTypeEnum triggerType, + const Optional activationReason); + Protocols::InteractionModel::Status + GeneratePushTransportEndEvent(const uint16_t connectionID, const PushAvStreamTransport::TransportTriggerTypeEnum triggerType, + const Optional activationReason); + +private: + PushAvStreamTransportDelegate * mDelegate = nullptr; + + /// Convenience method that returns if the internal delegate is null and will log + /// an error if the check returns true + bool IsNullDelegateWithLogging(EndpointId endpointIdForLogging); + + /** + * Helper function that loads all the persistent attributes from the KVS. + */ + void LoadPersistentAttributes(); + + // Helper functions + uint16_t GenerateConnectionID(); + + PushAvStreamTransport::TransportConfigurationStorage * FindStreamTransportConnection(const uint16_t connectionID); + + PushAvStreamTransport::TransportConfigurationStorage * FindStreamTransportConnectionWithinFabric(const uint16_t connectionID, + FabricIndex fabricIndex); + + // Add/Remove Management functions for transport + UpsertResultEnum + UpsertStreamTransportConnection(const PushAvStreamTransport::TransportConfigurationStorage & transportConfiguration); + + void RemoveStreamTransportConnection(const uint16_t connectionID); + + static void PushAVStreamTransportDeallocateCallback(chip::System::Layer *, void * callbackContext); + + UpsertResultEnum UpsertTimerAppState(std::shared_ptr timerAppState); + + void RemoveTimerAppState(const uint16_t connectionID); + + /** + * @brief Schedule deallocate with a given timeout + * + * @param endpointId endpoint where DoorLockServer is running + * @param timeoutSec timeout in seconds + */ + CHIP_ERROR ScheduleTransportDeallocate(uint16_t connectionID, uint32_t timeoutSec); +}; + +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-storage.h b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-storage.h new file mode 100644 index 00000000000000..d2465ee384da06 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-storage.h @@ -0,0 +1,389 @@ +/* + * + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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 chip { +namespace app { +namespace Clusters { +namespace PushAvStreamTransport { + +/** + * @brief Storage implementation for transport trigger options. + * Provides deep copy functionality and internal storage for motion zones. + * Must be used when trigger options need to persist beyond the original data scope. + */ +struct TransportTriggerOptionsStorage : public TransportTriggerOptionsStruct +{ + TransportTriggerOptionsStorage() {} + + TransportTriggerOptionsStorage(const TransportTriggerOptionsStorage & aTransportTriggerOptionsStorage) + { + *this = aTransportTriggerOptionsStorage; + } + + TransportTriggerOptionsStorage & operator=(const TransportTriggerOptionsStorage & aTransportTriggerOptionsStorage) + { + if (this == &aTransportTriggerOptionsStorage) + { + return *this; + } + + triggerType = aTransportTriggerOptionsStorage.triggerType; + + mTransportZoneOptions = aTransportTriggerOptionsStorage.mTransportZoneOptions; + + motionZones = aTransportTriggerOptionsStorage.motionZones; + + // Rebind motionZones to point to the copied vector if it was set + if (motionZones.HasValue() && !motionZones.Value().IsNull()) + { + motionZones.SetValue(DataModel::MakeNullable( + DataModel::List(mTransportZoneOptions.data(), mTransportZoneOptions.size()))); + } + + motionSensitivity = aTransportTriggerOptionsStorage.motionSensitivity; + motionTimeControl = aTransportTriggerOptionsStorage.motionTimeControl; + maxPreRollLen = aTransportTriggerOptionsStorage.maxPreRollLen; + + return *this; + } + + TransportTriggerOptionsStorage & + operator=(const Structs::TransportTriggerOptionsStruct::DecodableType & aTransportTriggerOptions) + { + + triggerType = aTransportTriggerOptions.triggerType; + + auto & motionZonesList = aTransportTriggerOptions.motionZones; + + if (triggerType == TransportTriggerTypeEnum::kMotion && motionZonesList.HasValue()) + { + if (!motionZonesList.Value().IsNull()) + { + auto iter = motionZonesList.Value().Value().begin(); + + while (iter.Next()) + { + auto & transportZoneOption = iter.GetValue(); + mTransportZoneOptions.push_back(transportZoneOption); + } + // GetStatus() check is not needed here, because the motionZonesList is already checked in the + // ValidateIncomingTransportOptions() function + + motionZones.SetValue(DataModel::MakeNullable( + DataModel::List(mTransportZoneOptions.data(), mTransportZoneOptions.size()))); + } + else + { + motionZones.Value().SetNull(); + } + } + else + { + motionZones.ClearValue(); + } + + motionSensitivity = aTransportTriggerOptions.motionSensitivity; + motionTimeControl = aTransportTriggerOptions.motionTimeControl; + maxPreRollLen = aTransportTriggerOptions.maxPreRollLen; + + return *this; + } + + TransportTriggerOptionsStorage(const Structs::TransportTriggerOptionsStruct::DecodableType & triggerOptions) + { + *this = triggerOptions; + } + +private: + std::vector mTransportZoneOptions; +}; + +/** + * @brief Storage implementation for CMAF container options. + * Manages fixed-size buffers for CENC keys and IDs with bounds checking. + * Must be used when CMAF container configurations need persistent key storage. + */ +struct CMAFContainerOptionsStorage : public CMAFContainerOptionsStruct +{ + CMAFContainerOptionsStorage() {} + + CMAFContainerOptionsStorage(const CMAFContainerOptionsStorage & aCMAFContainerOptionsStorage) + { + *this = aCMAFContainerOptionsStorage; + } + + CMAFContainerOptionsStorage & operator=(const CMAFContainerOptionsStorage & aCMAFContainerOptionsStorage) + { + if (this == &aCMAFContainerOptionsStorage) + { + return *this; + } + + *this = static_cast(aCMAFContainerOptionsStorage); + + return *this; + } + + CMAFContainerOptionsStorage & operator=(const Structs::CMAFContainerOptionsStruct::Type & aCMAFContainerOptions) + { + chunkDuration = aCMAFContainerOptions.chunkDuration; + + CENCKey = aCMAFContainerOptions.CENCKey; + + CENCKeyID = aCMAFContainerOptions.CENCKeyID; + + if (CENCKey.HasValue()) + { + MutableByteSpan CENCKeyBuffer(mCENCKeyBuffer); + // ValidateIncomingTransportOptions() function already checked the CENCKey length + CopySpanToMutableSpan(aCMAFContainerOptions.CENCKey.Value(), CENCKeyBuffer); + CENCKey.SetValue(CENCKeyBuffer); + } + else + { + CENCKey.ClearValue(); + } + + metadataEnabled = aCMAFContainerOptions.metadataEnabled; + + if (CENCKeyID.HasValue()) + { + MutableByteSpan CENCKeyIDBuffer(mCENCKeyIDBuffer); + // ValidateIncomingTransportOptions() function already checked the CENCKeyID length + CopySpanToMutableSpan(aCMAFContainerOptions.CENCKeyID.Value(), CENCKeyIDBuffer); + CENCKeyID.SetValue(CENCKeyIDBuffer); + } + else + { + CENCKeyID.ClearValue(); + } + + return *this; + } + + CMAFContainerOptionsStorage(const Structs::CMAFContainerOptionsStruct::Type & CMAFContainerOptions) + { + *this = CMAFContainerOptions; + } + +private: + uint8_t mCENCKeyBuffer[kMaxCENCKeyLength]; + uint8_t mCENCKeyIDBuffer[kMaxCENCKeyIDLength]; +}; + +/** + * @brief Storage implementation for container format options. + * Manages different container formats with type-specific storage handling. + * Currently supports CMAF containers. + */ +struct ContainerOptionsStorage : public ContainerOptionsStruct +{ + ContainerOptionsStorage() {} + + ContainerOptionsStorage(const ContainerOptionsStorage & aContainerOptionsStorage) { *this = aContainerOptionsStorage; } + + ContainerOptionsStorage & operator=(const ContainerOptionsStorage & aContainerOptionsStorage) + { + if (this == &aContainerOptionsStorage) + { + return *this; + } + + *this = static_cast(aContainerOptionsStorage); + + return *this; + } + + ContainerOptionsStorage & operator=(const Structs::ContainerOptionsStruct::DecodableType & aContainerOptions) + { + containerType = aContainerOptions.containerType; + + if (containerType == ContainerFormatEnum::kCmaf) + { + mCMAFContainerStorage = aContainerOptions.CMAFContainerOptions.Value(); + CMAFContainerOptions.SetValue(mCMAFContainerStorage); + } + else + { + CMAFContainerOptions.ClearValue(); + } + + return *this; + } + + ContainerOptionsStorage(const Structs::ContainerOptionsStruct::DecodableType & containerOptions) { *this = containerOptions; } + +private: + CMAFContainerOptionsStorage mCMAFContainerStorage; +}; + +/** + * @brief Storage implementation for transport options. + * Manages URL storage, trigger options, container options, and stream configurations. + * Must be used when transport configurations need to persist beyond request scope. + */ +struct TransportOptionsStorage : public TransportOptionsStruct +{ + TransportOptionsStorage() {} + + TransportOptionsStorage(const TransportOptionsStorage & aTransportOptionsStorage) { *this = aTransportOptionsStorage; } + + TransportOptionsStorage & operator=(const TransportOptionsStorage & aTransportOptionsStorage) + { + if (this == &aTransportOptionsStorage) + { + return *this; + } + streamUsage = aTransportOptionsStorage.streamUsage; + videoStreamID = aTransportOptionsStorage.videoStreamID; + audioStreamID = aTransportOptionsStorage.audioStreamID; + endpointID = aTransportOptionsStorage.endpointID; + + // Deep copy the URL buffer + std::memcpy(mUrlBuffer, aTransportOptionsStorage.mUrlBuffer, kMaxUrlLength); + url = MutableCharSpan(mUrlBuffer, aTransportOptionsStorage.url.size()); + + // Copy internal storage objects + mTriggerOptionsStorage = aTransportOptionsStorage.mTriggerOptionsStorage; + triggerOptions = mTriggerOptionsStorage; + + ingestMethod = aTransportOptionsStorage.ingestMethod; + + mContainerOptionsStorage = aTransportOptionsStorage.mContainerOptionsStorage; + containerOptions = mContainerOptionsStorage; + + expiryTime = aTransportOptionsStorage.expiryTime; + + return *this; + } + + TransportOptionsStorage & operator=(const Structs::TransportOptionsStruct::DecodableType & transportOptions) + { + streamUsage = transportOptions.streamUsage; + videoStreamID = transportOptions.videoStreamID; + audioStreamID = transportOptions.audioStreamID; + endpointID = transportOptions.endpointID; + + MutableCharSpan urlBuffer(mUrlBuffer); + // ValidateIncomingTransportOptions() function already checked the url length + CopyCharSpanToMutableCharSpanWithTruncation(transportOptions.url, urlBuffer); + url = urlBuffer; + + mTriggerOptionsStorage = transportOptions.triggerOptions; + triggerOptions = mTriggerOptionsStorage; + + ingestMethod = transportOptions.ingestMethod; + + mContainerOptionsStorage = transportOptions.containerOptions; + containerOptions = mContainerOptionsStorage; + + expiryTime = transportOptions.expiryTime; + + return *this; + } + + TransportOptionsStorage(const Structs::TransportOptionsStruct::DecodableType & transportOptions) { *this = transportOptions; } + +private: + char mUrlBuffer[kMaxUrlLength]; + TransportTriggerOptionsStorage mTriggerOptionsStorage; + ContainerOptionsStorage mContainerOptionsStorage; +}; + +/** + * @brief Storage implementation for transport configurations. + * Manages connection ID, status, fabric index and transport options. + * Uses shared_ptr for transport options to ensure proper lifetime management. + */ +struct TransportConfigurationStorage : public TransportConfigurationStruct +{ + TransportConfigurationStorage() {} + + TransportConfigurationStorage(const TransportConfigurationStorage & aTransportConfigurationStorage) + { + *this = aTransportConfigurationStorage; + } + + TransportConfigurationStorage & operator=(const TransportConfigurationStorage & aTransportConfigurationStorage) + { + if (this == &aTransportConfigurationStorage) + { + return *this; + } + connectionID = aTransportConfigurationStorage.connectionID; + transportStatus = aTransportConfigurationStorage.transportStatus; + fabricIndex = aTransportConfigurationStorage.fabricIndex; + + mTransportOptionsPtr = aTransportConfigurationStorage.mTransportOptionsPtr; + + if (mTransportOptionsPtr) + { + transportOptions.SetValue(*mTransportOptionsPtr); + } + else + { + transportOptions.ClearValue(); + } + + return *this; + } + + TransportConfigurationStorage(const uint16_t aConnectionID, std::shared_ptr aTransportOptionsPtr) + { + connectionID = aConnectionID; + transportStatus = TransportStatusEnum::kInactive; + /*Store the pointer to keep buffer alive*/ + mTransportOptionsPtr = aTransportOptionsPtr; + /*Convert Storage type to base type*/ + if (mTransportOptionsPtr) + { + transportOptions.SetValue(*mTransportOptionsPtr); + } + else + { + transportOptions.ClearValue(); + } + } + std::shared_ptr GetTransportOptionsPtr() const { return mTransportOptionsPtr; } + void SetTransportOptionsPtr(std::shared_ptr aTransportOptionsPtr) + { + mTransportOptionsPtr = aTransportOptionsPtr; + if (mTransportOptionsPtr) + { + transportOptions.SetValue(*mTransportOptionsPtr); + } + else + { + transportOptions.ClearValue(); + } + } + +private: + std::shared_ptr mTransportOptionsPtr; +}; + +} // namespace PushAvStreamTransport +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/push-av-stream-transport-server/tests/BUILD.gn b/src/app/clusters/push-av-stream-transport-server/tests/BUILD.gn new file mode 100644 index 00000000000000..0a0dead1e33048 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/tests/BUILD.gn @@ -0,0 +1,40 @@ +# Copyright (c) 2025 Project CHIP Authors +# +# 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. + +import("//build_overrides/build.gni") +import("//build_overrides/chip.gni") +import("//build_overrides/pigweed.gni") + +import("${chip_root}/build/chip/chip_test_suite.gni") + +chip_test_suite("tests") { + output_name = "libTestPushAVStreamTransport" + + test_sources = [ + "TestPushAVStreamTransportCluster.cpp", + "TestPushAVStreamTransportStorage.cpp", + ] + + sources = [] + + cflags = [ "-Wconversion" ] + + public_deps = [ + "${chip_root}/src/app/clusters/push-av-stream-transport-server", + "${chip_root}/src/app/clusters/testing", + "${chip_root}/src/app/tests:helpers", + "${chip_root}/src/lib/core:string-builder-adapters", + "${chip_root}/src/lib/support", + ] +} diff --git a/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp b/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp new file mode 100644 index 00000000000000..fb5a334af36f41 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp @@ -0,0 +1,1332 @@ +/* + * + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { + +class MockCommandHandler : public CommandHandler +{ +public: + ~MockCommandHandler() override {} + + struct ResponseRecord + { + ConcreteCommandPath path; + CommandId commandId; + chip::System::PacketBufferHandle encodedData; + }; + + CHIP_ERROR FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath, + const Protocols::InteractionModel::ClusterStatusCode & aStatus, + const char * context = nullptr) override + { + mStatuses.push_back({ aRequestCommandPath, aStatus }); + return CHIP_NO_ERROR; + } + + void AddStatus(const ConcreteCommandPath & aRequestCommandPath, const Protocols::InteractionModel::ClusterStatusCode & aStatus, + const char * context = nullptr) override + { + CHIP_ERROR err = FallibleAddStatus(aRequestCommandPath, aStatus, context); + VerifyOrDie(err == CHIP_NO_ERROR); + } + + CHIP_ERROR AddClusterSpecificSuccess(const ConcreteCommandPath & aRequestCommandPath, ClusterStatus aClusterStatus) override + { + return FallibleAddStatus(aRequestCommandPath, + Protocols::InteractionModel::ClusterStatusCode::ClusterSpecificSuccess(aClusterStatus)); + } + + CHIP_ERROR AddClusterSpecificFailure(const ConcreteCommandPath & aRequestCommandPath, ClusterStatus aClusterStatus) override + { + return FallibleAddStatus(aRequestCommandPath, + Protocols::InteractionModel::ClusterStatusCode::ClusterSpecificFailure(aClusterStatus)); + } + + FabricIndex GetAccessingFabricIndex() const override { return mFabricIndex; } + + const std::vector & GetResponses() const { return mResponses; } + + CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + const DataModel::EncodableToTLV & aEncodable) override + { + chip::System::PacketBufferHandle handle = chip::MessagePacketBuffer::New(1024); + VerifyOrReturnError(!handle.IsNull(), CHIP_ERROR_NO_MEMORY); + + TLV::TLVWriter baseWriter; + baseWriter.Init(handle->Start(), handle->MaxDataLength()); + + FabricIndex fabricIndex = 1; + + DataModel::FabricAwareTLVWriter writer(baseWriter, fabricIndex); + + TLV::TLVType containerType; + ReturnErrorOnFailure( + static_cast(writer).StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, containerType)); + ReturnErrorOnFailure(aEncodable.EncodeTo(writer, TLV::ContextTag(chip::app::CommandDataIB::Tag::kFields))); + ReturnErrorOnFailure(static_cast(writer).EndContainer(containerType)); + + handle->SetDataLength(static_cast(writer).GetLengthWritten()); + + mResponses.push_back({ aRequestCommandPath, aResponseCommandId, std::move(handle) }); + return CHIP_NO_ERROR; + } + + void AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + const DataModel::EncodableToTLV & aEncodable) override + { + AddResponseData(aRequestCommandPath, aResponseCommandId, aEncodable); + } + + bool IsTimedInvoke() const override { return mIsTimedInvoke; } + + void FlushAcksRightAwayOnSlowCommand() override { mAcksFlushed = true; } + + Access::SubjectDescriptor GetSubjectDescriptor() const override { return mSubjectDescriptor; } + + Messaging::ExchangeContext * GetExchangeContext() const override { return mExchangeContext; } + + // Optional for test configuration + void SetFabricIndex(FabricIndex index) { mFabricIndex = index; } + void SetTimedInvoke(bool isTimed) { mIsTimedInvoke = isTimed; } + void SetExchangeContext(Messaging::ExchangeContext * context) { mExchangeContext = context; } + +private: + struct StatusRecord + { + ConcreteCommandPath path; + Protocols::InteractionModel::ClusterStatusCode status; + }; + + std::vector mResponses; + std::vector mStatuses; + + FabricIndex mFabricIndex = 0; + bool mIsTimedInvoke = false; + bool mAcksFlushed = false; + Messaging::ExchangeContext * mExchangeContext = nullptr; + Access::SubjectDescriptor mSubjectDescriptor; +}; + +static uint8_t gDebugEventBuffer[120]; +static uint8_t gInfoEventBuffer[120]; +static uint8_t gCritEventBuffer[120]; +static chip::app::CircularEventBuffer gCircularEventBuffer[3]; + +class MockEventLogging : public chip::Test::AppContext +{ +public: + void SetUp() override + { + const chip::app::LogStorageResources logStorageResources[] = { + { &gDebugEventBuffer[0], sizeof(gDebugEventBuffer), chip::app::PriorityLevel::Debug }, + { &gInfoEventBuffer[0], sizeof(gInfoEventBuffer), chip::app::PriorityLevel::Info }, + { &gCritEventBuffer[0], sizeof(gCritEventBuffer), chip::app::PriorityLevel::Critical }, + }; + + AppContext::SetUp(); + + ASSERT_EQ(mEventCounter.Init(0), CHIP_NO_ERROR); + + chip::app::EventManagement::CreateEventManagement(&GetExchangeManager(), std::size(logStorageResources), + gCircularEventBuffer, logStorageResources, &mEventCounter); + } + + void TearDown() override + { + chip::app::EventManagement::DestroyEventManagement(); + AppContext::TearDown(); + } + +private: + chip::MonotonicallyIncreasingCounter mEventCounter; +}; + +static void CheckLogState(chip::app::EventManagement & aLogMgmt, size_t expectedNumEvents, chip::app::PriorityLevel aPriority) +{ + chip::TLV::TLVReader reader; + size_t elementCount; + chip::app::CircularEventBufferWrapper bufWrapper; + EXPECT_EQ(aLogMgmt.GetEventReader(reader, aPriority, &bufWrapper), CHIP_NO_ERROR); + + EXPECT_EQ(chip::TLV::Utilities::Count(reader, elementCount, false), CHIP_NO_ERROR); + + EXPECT_EQ(elementCount, expectedNumEvents); +} +} // namespace app +} // namespace chip + +namespace chip { +namespace app { +namespace Clusters { +namespace PushAvStreamTransport { + +using TransportZoneOptionsDecodableStruct = Structs::TransportZoneOptionsStruct::DecodableType; +using TransportTriggerOptionsDecodableStruct = Structs::TransportTriggerOptionsStruct::DecodableType; +using TransportMotionTriggerTimeControlDecodableStruct = Structs::TransportMotionTriggerTimeControlStruct::DecodableType; +using TransportOptionsDecodableStruct = Structs::TransportOptionsStruct::DecodableType; + +using namespace chip::Protocols::InteractionModel; + +struct PushAvStream +{ + uint16_t id; + TransportOptionsStruct transportOptions; + TransportStatusEnum transportStatus; + PushAvStreamTransportStatusEnum connectionStatus; +}; +class TestPushAVStreamTransportDelegateImpl : public PushAvStreamTransportDelegate +{ +public: + Protocols::InteractionModel::Status AllocatePushTransport(const TransportOptionsStruct & transportOptions, + const uint16_t connectionID) + { + PushAvStream stream{ connectionID, transportOptions, TransportStatusEnum::kInactive, + PushAvStreamTransportStatusEnum::kIdle }; + + /*Store the allocated stream persistently*/ + pushavStreams.push_back(stream); + + return Status::Success; + } + + Protocols::InteractionModel::Status DeallocatePushTransport(const uint16_t connectionID) + { + pushavStreams.erase(std::remove_if(pushavStreams.begin(), pushavStreams.end(), + [connectionID](const PushAvStream & stream) { return stream.id == connectionID; }), + pushavStreams.end()); + return Status::Success; + } + + Protocols::InteractionModel::Status ModifyPushTransport(const uint16_t connectionID, + const TransportOptionsStorage transportOptions) + { + for (PushAvStream & stream : pushavStreams) + { + if (stream.id == connectionID) + { + ChipLogProgress(Zcl, "Modified Push AV Stream with ID: %d", connectionID); + return Status::Success; + } + } + ChipLogError(Zcl, "Allocated Push AV Stream with ID: %d not found", connectionID); + return Status::NotFound; + } + + Protocols::InteractionModel::Status SetTransportStatus(const std::vector connectionIDList, + TransportStatusEnum transportStatus) + { + for (PushAvStream & stream : pushavStreams) + { + for (uint16_t connectionID : connectionIDList) + { + if (stream.id == connectionID) + { + stream.transportStatus = transportStatus; + ChipLogProgress(Zcl, "Set Transport Status for Push AV Stream with ID: %d", connectionID); + } + } + } + return Status::Success; + } + + Protocols::InteractionModel::Status + ManuallyTriggerTransport(const uint16_t connectionID, TriggerActivationReasonEnum activationReason, + const Optional & timeControl) + { + // TODO: Validates the requested stream usage against the camera's resource management and stream priority policies. + for (PushAvStream & stream : pushavStreams) + { + if (stream.id == connectionID) + { + stream.connectionStatus = PushAvStreamTransportStatusEnum::kBusy; + ChipLogProgress(Zcl, "Transport triggered for Push AV Stream with ID: %d", connectionID); + } + } + return Status::Success; + } + + Protocols::InteractionModel::Status ValidateBandwidthLimit(StreamUsageEnum streamUsage, + const Optional> & videoStreamId, + const Optional> & audioStreamId) + { + // TODO: Validates the requested stream usage against the camera's resource management. + // Returning Status::Success to pass through checks in the Server Implementation. + return Status::Success; + } + + bool ValidateUrl(std::string url) { return true; } + + Protocols::InteractionModel::Status SelectVideoStream(StreamUsageEnum streamUsage, uint16_t & videoStreamId) + { + // TODO: Select and Assign videoStreamID from the allocated videoStreams + // Returning Status::Success to pass through checks in the Server Implementation. + return Status::Success; + } + + Protocols::InteractionModel::Status SelectAudioStream(StreamUsageEnum streamUsage, uint16_t & audioStreamId) + { + // TODO: Select and Assign audioStreamID from the allocated audioStreams + // Returning Status::Success to pass through checks in the Server Implementation. + return Status::Success; + } + + Protocols::InteractionModel::Status ValidateVideoStream(uint16_t videoStreamId) + { + // TODO: Validate videoStreamID from the allocated videoStreams + // Returning Status::Success to pass through checks in the Server Implementation. + return Status::Success; + } + + Protocols::InteractionModel::Status ValidateAudioStream(uint16_t audioStreamId) + { + // TODO: Validate audioStreamID from the allocated audioStreams + // Returning Status::Success to pass through checks in the Server Implementation. + return Status::Success; + } + + PushAvStreamTransportStatusEnum GetTransportBusyStatus(const uint16_t connectionID) + { + for (PushAvStream & stream : pushavStreams) + { + if (stream.id == connectionID) + { + return stream.connectionStatus; + } + } + return PushAvStreamTransportStatusEnum::kUnknown; + } + + void OnAttributeChanged(AttributeId attributeId) + { + ChipLogProgress(Zcl, "Attribute changed for AttributeId = " ChipLogFormatMEI, ChipLogValueMEI(attributeId)); + } + + void Init() { ChipLogProgress(Zcl, "Push AV Stream Transport Initialized"); } + CHIP_ERROR + LoadCurrentConnections(std::vector & currentConnections) + { + ChipLogProgress(Zcl, "Push AV Current Connections loaded"); + + return CHIP_NO_ERROR; + } + + CHIP_ERROR + PersistentAttributesLoadedCallback() + { + ChipLogProgress(Zcl, "Persistent attributes loaded"); + + return CHIP_NO_ERROR; + } + +private: + std::vector pushavStreams; +}; + +class TestPushAVStreamTransportServerLogic : public ::testing::Test +{ +public: + static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } + static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } +}; + +TEST_F(TestPushAVStreamTransportServerLogic, TestTransportOptionsConstraints) +{ + std::string cencKey = "1234567890ABCDEF"; + std::string cencKeyID = "1234567890ABCDEF"; + + CMAFContainerOptionsStruct cmafContainerOptions; + ContainerOptionsStruct containerOptions; + TransportMotionTriggerTimeControlDecodableStruct motionTimeControl; + std::vector mTransportZoneOptions; + TransportTriggerOptionsDecodableStruct triggerOptions; + + std::string url = "rtsp://192.168.1.100:554/stream"; + TransportOptionsDecodableStruct transportOptions; + + uint8_t tlvBuffer[512]; + Structs::TransportZoneOptionsStruct::Type zone1; + Structs::TransportZoneOptionsStruct::Type zone2; + DataModel::DecodableList decodedList; + + PushAvStreamTransportServerLogic logic(1, BitFlags(1)); + + // Create CMAFContainerOptionsStruct object + cmafContainerOptions.chunkDuration = 1000; + cmafContainerOptions.metadataEnabled.ClearValue(); + + cmafContainerOptions.CENCKey.SetValue(ByteSpan(reinterpret_cast(cencKey.c_str()), cencKey.size())); + cmafContainerOptions.CENCKeyID.SetValue(ByteSpan(reinterpret_cast(cencKeyID.c_str()), cencKeyID.size())); + + // Create ContainerOptionsStruct object + containerOptions.containerType = ContainerFormatEnum::kCmaf; + containerOptions.CMAFContainerOptions.SetValue(cmafContainerOptions); + + // Create a TransportMotionTriggerTimeControlStruct object + motionTimeControl.initialDuration = 5000; + motionTimeControl.augmentationDuration = 2000; + motionTimeControl.maxDuration = 30000; + motionTimeControl.blindDuration = 1000; + + triggerOptions.triggerType = TransportTriggerTypeEnum::kMotion; + + triggerOptions.motionZones.ClearValue(); + + triggerOptions.motionSensitivity.ClearValue(); + triggerOptions.motionTimeControl.SetValue(motionTimeControl); + triggerOptions.maxPreRollLen.SetValue(1000); + + // Create TransportOptionsStruct object + transportOptions.streamUsage = StreamUsageEnum::kAnalysis; + transportOptions.videoStreamID.SetValue(1); + transportOptions.audioStreamID.SetValue(2); + transportOptions.endpointID = 1; + transportOptions.url = Span(url.data(), url.size()); + transportOptions.triggerOptions = triggerOptions; + transportOptions.containerOptions = containerOptions; + transportOptions.expiryTime.ClearValue(); + + // Invalid command because the motion zones are missing + EXPECT_EQ(logic.ValidateIncomingTransportOptions(transportOptions), Status::InvalidCommand); + + // Create transport zone options structs + zone1.zone.SetNonNull(1); + zone1.sensitivity.SetValue(5); + + zone2.zone.SetNonNull(2); + zone2.sensitivity.SetValue(10); + + // Encode them into a TLV buffer + TLV::TLVWriter writer; + writer.Init(tlvBuffer, sizeof(tlvBuffer)); + + TLV::TLVWriter containerWriter; + CHIP_ERROR err; + + err = writer.OpenContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone1); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone2); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = writer.CloseContainer(containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + size_t encodedLen = writer.GetLengthWritten(); + + // Decode the TLV into a DecodableList + TLV::TLVReader motionZonesReader; + motionZonesReader.Init(tlvBuffer, static_cast(encodedLen)); + err = motionZonesReader.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = decodedList.Decode(motionZonesReader); + EXPECT_EQ(err, CHIP_NO_ERROR); + + triggerOptions.motionZones.SetValue(DataModel::MakeNullable(decodedList)); + + // Upadate the trigger options in the transport options + transportOptions.triggerOptions = triggerOptions; + EXPECT_EQ(logic.ValidateIncomingTransportOptions(transportOptions), Status::Success); +} + +void PrintBufHex(const uint8_t * buf, size_t size) +{ + for (size_t i = 0; i < size; ++i) + { + printf("%02X ", buf[i]); + + if ((i + 1) % 16 == 0) + printf("\n"); + } + + if (size % 16 != 0) + printf("\n"); +} + +TEST_F(TestPushAVStreamTransportServerLogic, Test_AllocateTransport_AllocateTransportResponse_ReadAttribute_DeallocateTransport) +{ + /* + * Test AllocatePushTransport + */ + std::string cencKey = "1234567890ABCDEF"; + std::string cencKeyID = "1234567890ABCDEF"; + + CMAFContainerOptionsStruct cmafContainerOptions; + ContainerOptionsStruct containerOptions; + TransportMotionTriggerTimeControlDecodableStruct motionTimeControl; + std::vector mTransportZoneOptions; + TransportTriggerOptionsDecodableStruct triggerOptions; + + std::string url = "rtsp://192.168.1.100:554/stream"; + TransportOptionsDecodableStruct transportOptions; + + uint8_t tlvBuffer[512]; + Structs::TransportZoneOptionsStruct::Type zone1; + Structs::TransportZoneOptionsStruct::Type zone2; + DataModel::DecodableList decodedList; + + // Create CMAFContainerOptionsStruct object + cmafContainerOptions.chunkDuration = 1000; + cmafContainerOptions.metadataEnabled.ClearValue(); + + cmafContainerOptions.CENCKey.SetValue(ByteSpan(reinterpret_cast(cencKey.c_str()), cencKey.size())); + cmafContainerOptions.CENCKeyID.SetValue(ByteSpan(reinterpret_cast(cencKeyID.c_str()), cencKeyID.size())); + + // Create ContainerOptionsStruct object + containerOptions.containerType = ContainerFormatEnum::kCmaf; + containerOptions.CMAFContainerOptions.SetValue(cmafContainerOptions); + + // Create a TransportMotionTriggerTimeControlStruct object + motionTimeControl.initialDuration = 5000; + motionTimeControl.augmentationDuration = 2000; + motionTimeControl.maxDuration = 30000; + motionTimeControl.blindDuration = 1000; + + triggerOptions.triggerType = TransportTriggerTypeEnum::kMotion; + + // Create transport zone options structs + zone1.zone.SetNonNull(1); + zone1.sensitivity.SetValue(5); + + zone2.zone.SetNonNull(2); + zone2.sensitivity.SetValue(10); + + // Encode them into a TLV buffer + TLV::TLVWriter writer; + writer.Init(tlvBuffer, sizeof(tlvBuffer)); + + TLV::TLVWriter containerWriter; + CHIP_ERROR err; + + err = writer.OpenContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone1); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone2); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = writer.CloseContainer(containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + size_t encodedLen = writer.GetLengthWritten(); + + // Decode the TLV into a DecodableList + TLV::TLVReader motionZonesReader; + motionZonesReader.Init(tlvBuffer, static_cast(encodedLen)); + err = motionZonesReader.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = decodedList.Decode(motionZonesReader); + EXPECT_EQ(err, CHIP_NO_ERROR); + + triggerOptions.motionZones.SetValue(DataModel::MakeNullable(decodedList)); + + triggerOptions.motionSensitivity.ClearValue(); + triggerOptions.motionTimeControl.SetValue(motionTimeControl); + triggerOptions.maxPreRollLen.SetValue(1000); + + // Create TransportOptionsStruct object + transportOptions.streamUsage = StreamUsageEnum::kAnalysis; + transportOptions.videoStreamID.SetValue(1); + transportOptions.audioStreamID.SetValue(2); + transportOptions.endpointID = 1; + transportOptions.url = Span(url.data(), url.size()); + transportOptions.triggerOptions = triggerOptions; + transportOptions.containerOptions = containerOptions; + transportOptions.expiryTime.ClearValue(); + + PushAvStreamTransportServer server(1, BitFlags(1)); + TestPushAVStreamTransportDelegateImpl mockDelegate; + + MockCommandHandler commandHandler; + commandHandler.SetFabricIndex(1); + ConcreteCommandPath kCommandPath{ 1, Clusters::PushAvStreamTransport::Id, Commands::AllocatePushTransport::Id }; + Commands::AllocatePushTransport::DecodableType commandData; + commandData.transportOptions = transportOptions; + + // Without a delegate, command is unsupported. + EXPECT_EQ(server.GetLogic().HandleAllocatePushTransport(commandHandler, kCommandPath, commandData), Status::UnsupportedCommand); + + // Set the delegate to the server logic + server.GetLogic().SetDelegate(1, &mockDelegate); + EXPECT_EQ(server.GetLogic().HandleAllocatePushTransport(commandHandler, kCommandPath, commandData), std::nullopt); + + EXPECT_EQ(server.GetLogic().mCurrentConnections.size(), (size_t) 1); + uint16_t allocatedConnectionID = server.GetLogic().mCurrentConnections[0].connectionID; + + /* + * Test AllocatePushTransportResponse + */ + + // Check the response + const auto & responses = commandHandler.GetResponses(); + EXPECT_EQ(responses.size(), (size_t) 1); + + // Get the encoded buffer + const auto & encodedBuffer = responses[0].encodedData; + + PrintBufHex(encodedBuffer->Start(), encodedBuffer->DataLength()); + + EXPECT_FALSE(encodedBuffer.IsNull()); + + // Set up TLV reader + TLV::TLVReader responseReader; + responseReader.Init(encodedBuffer->Start(), static_cast(encodedBuffer->DataLength())); + + // Enter the top-level anonymous structure (CommandDataIB wrapper) + err = responseReader.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + TLV::TLVReader outerContainer; + err = responseReader.OpenContainer(outerContainer); + EXPECT_EQ(err, CHIP_NO_ERROR); + + // Read the next element inside the container: should be kFields + err = outerContainer.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + EXPECT_TRUE(IsContextTag(outerContainer.GetTag())); + EXPECT_EQ(TagNumFromTag(outerContainer.GetTag()), chip::to_underlying(CommandDataIB::Tag::kFields)); + + // Decode into response object + Commands::AllocatePushTransportResponse::DecodableType decodedResponse; + err = decodedResponse.Decode(outerContainer); + EXPECT_EQ(err, CHIP_NO_ERROR); + + // Validate decoded fields + EXPECT_EQ(decodedResponse.transportConfiguration.connectionID, allocatedConnectionID); + EXPECT_EQ(decodedResponse.transportConfiguration.GetFabricIndex(), 1); + EXPECT_EQ(decodedResponse.transportConfiguration.transportStatus, TransportStatusEnum::kInactive); + + EXPECT_TRUE(decodedResponse.transportConfiguration.transportOptions.HasValue()); + + Structs::TransportOptionsStruct::DecodableType respTransportOptions = + decodedResponse.transportConfiguration.transportOptions.Value(); + + EXPECT_EQ(respTransportOptions.streamUsage, StreamUsageEnum::kAnalysis); + EXPECT_EQ(respTransportOptions.videoStreamID, 1); + EXPECT_EQ(respTransportOptions.audioStreamID, 2); + EXPECT_EQ(respTransportOptions.endpointID, 1); + std::string respUrlStr(respTransportOptions.url.data(), respTransportOptions.url.size()); + EXPECT_EQ(respUrlStr, "rtsp://192.168.1.100:554/stream"); + + Structs::TransportTriggerOptionsStruct::DecodableType respTriggerOptions = respTransportOptions.triggerOptions; + EXPECT_EQ(respTriggerOptions.triggerType, TransportTriggerTypeEnum::kMotion); + EXPECT_TRUE(respTriggerOptions.motionZones.HasValue()); + EXPECT_FALSE(respTriggerOptions.motionZones.Value().IsNull()); + + DataModel::DecodableList & respMotionZonesList = + respTriggerOptions.motionZones.Value().Value(); + + auto respMotionZonesIter = respMotionZonesList.begin(); + + EXPECT_TRUE(respMotionZonesIter.Next()); + const auto & respDecodedZone1 = respMotionZonesIter.GetValue(); + EXPECT_TRUE(!respDecodedZone1.zone.IsNull()); + EXPECT_EQ(respDecodedZone1.zone.Value(), 1); + EXPECT_TRUE(respDecodedZone1.sensitivity.HasValue()); + EXPECT_EQ(respDecodedZone1.sensitivity.Value(), 5); + + EXPECT_TRUE(respMotionZonesIter.Next()); + const auto & respDecodedZone2 = respMotionZonesIter.GetValue(); + EXPECT_TRUE(!respDecodedZone2.zone.IsNull()); + EXPECT_EQ(respDecodedZone2.zone.Value(), 2); + EXPECT_TRUE(respDecodedZone2.sensitivity.HasValue()); + EXPECT_EQ(respDecodedZone2.sensitivity.Value(), 10); + + // Should be no more entries + EXPECT_FALSE(respMotionZonesIter.Next()); + + EXPECT_FALSE(respTriggerOptions.motionSensitivity.HasValue()); + EXPECT_TRUE(respTriggerOptions.maxPreRollLen.HasValue()); + EXPECT_EQ(respTriggerOptions.maxPreRollLen.Value(), 1000); + + EXPECT_TRUE(respTriggerOptions.motionTimeControl.HasValue()); + Structs::TransportMotionTriggerTimeControlStruct::DecodableType respMotionTimeControl = + respTriggerOptions.motionTimeControl.Value(); + EXPECT_EQ(respMotionTimeControl.initialDuration, 5000); + EXPECT_EQ(respMotionTimeControl.augmentationDuration, 2000); + EXPECT_EQ(respMotionTimeControl.maxDuration, (uint32_t) 30000); + EXPECT_EQ(respMotionTimeControl.blindDuration, 1000); + + EXPECT_EQ(respTransportOptions.ingestMethod, IngestMethodsEnum::kCMAFIngest); + + Structs::ContainerOptionsStruct::DecodableType respContainerOptions = respTransportOptions.containerOptions; + EXPECT_EQ(respContainerOptions.containerType, ContainerFormatEnum::kCmaf); + EXPECT_TRUE(respContainerOptions.CMAFContainerOptions.HasValue()); + Structs::CMAFContainerOptionsStruct::Type respCMAFContainerOptions = respContainerOptions.CMAFContainerOptions.Value(); + EXPECT_EQ(respCMAFContainerOptions.chunkDuration, 1000); + EXPECT_FALSE(respCMAFContainerOptions.metadataEnabled.HasValue()); + + std::string respCENCKeyStr(respCMAFContainerOptions.CENCKey.Value().data(), + respCMAFContainerOptions.CENCKey.Value().data() + respCMAFContainerOptions.CENCKey.Value().size()); + + EXPECT_EQ(respCENCKeyStr, "1234567890ABCDEF"); + + std::string respCENCKeyIDStr(respCMAFContainerOptions.CENCKeyID.Value().data(), + respCMAFContainerOptions.CENCKeyID.Value().data() + + respCMAFContainerOptions.CENCKeyID.Value().size()); + + EXPECT_EQ(respCENCKeyIDStr, "1234567890ABCDEF"); + + /* + * Test ReadAttribute + */ + // Test reading current connections through attribute reader + uint8_t buf[1024]; + + TLV::TLVWriter tlvWriter; + tlvWriter.Init(buf); + + AttributeReportIBs::Builder builder; + builder.Init(&tlvWriter); + + ConcreteAttributePath path(1, Clusters::PushAvStreamTransport::Id, + Clusters::PushAvStreamTransport::Attributes::CurrentConnections::Id); + + DataModel::ReadAttributeRequest request; + request.path = path; + request.readFlags.Set(DataModel::ReadFlags::kFabricFiltered); + chip::DataVersion dataVersion(0); + Access::SubjectDescriptor subjectDescriptor; + FabricIndex peerFabricIndex = 1; + subjectDescriptor.fabricIndex = peerFabricIndex; + AttributeValueEncoder encoder(builder, subjectDescriptor, path, dataVersion, true); + + // Read the CurrentConnections attribute using the cluster's Read function + DataModel::ActionReturnStatus status = server.ReadAttribute(request, encoder); + EXPECT_TRUE(status.IsSuccess()); + + TLV::TLVReader reader; + reader.Init(buf); + + PrintBufHex(buf, tlvWriter.GetLengthWritten()); + + TLV::TLVReader attrReportsReader; + TLV::TLVReader attrReportReader; + TLV::TLVReader attrDataReader; + + reader.Next(); + reader.OpenContainer(attrReportsReader); + + attrReportsReader.Next(); + attrReportsReader.OpenContainer(attrReportReader); + + attrReportReader.Next(); + attrReportReader.OpenContainer(attrDataReader); + + // We're now in the attribute data IB, skip to the desired tag, we want TagNum = 2 + attrDataReader.Next(); + for (int i = 0; i < 3 && !(IsContextTag(attrDataReader.GetTag()) && TagNumFromTag(attrDataReader.GetTag()) == 2); ++i) + { + attrDataReader.Next(); + } + EXPECT_TRUE(IsContextTag(attrDataReader.GetTag())); + EXPECT_EQ(TagNumFromTag(attrDataReader.GetTag()), 2u); + + Clusters::PushAvStreamTransport::Attributes::CurrentConnections::TypeInfo::DecodableType currentConnections; + err = currentConnections.Decode(attrDataReader); + EXPECT_EQ(err, CHIP_NO_ERROR); + + auto iter = currentConnections.begin(); + EXPECT_TRUE(iter.Next()); + Structs::TransportConfigurationStruct::DecodableType readTransportConfiguration = iter.GetValue(); + EXPECT_EQ(readTransportConfiguration.connectionID, allocatedConnectionID); + EXPECT_EQ(readTransportConfiguration.transportStatus, TransportStatusEnum::kInactive); + EXPECT_TRUE(readTransportConfiguration.transportOptions.HasValue()); + Structs::TransportOptionsStruct::DecodableType readTransportOptions = readTransportConfiguration.transportOptions.Value(); + EXPECT_EQ(readTransportOptions.streamUsage, StreamUsageEnum::kAnalysis); + EXPECT_EQ(readTransportOptions.videoStreamID, 1); + EXPECT_EQ(readTransportOptions.audioStreamID, 2); + EXPECT_EQ(readTransportOptions.endpointID, 1); + + std::string urlStr(readTransportOptions.url.data(), readTransportOptions.url.size()); + EXPECT_EQ(urlStr, "rtsp://192.168.1.100:554/stream"); + + Structs::TransportTriggerOptionsStruct::DecodableType readTriggerOptions = readTransportOptions.triggerOptions; + EXPECT_EQ(readTriggerOptions.triggerType, TransportTriggerTypeEnum::kMotion); + EXPECT_TRUE(readTriggerOptions.motionZones.HasValue()); + EXPECT_FALSE(readTriggerOptions.motionZones.Value().IsNull()); + + DataModel::DecodableList readMotionZonesList = + readTriggerOptions.motionZones.Value().Value(); + + auto readMotionZonesIter = readMotionZonesList.begin(); + + EXPECT_TRUE(readMotionZonesIter.Next()); + const auto & decodedZone1 = readMotionZonesIter.GetValue(); + EXPECT_TRUE(!decodedZone1.zone.IsNull()); + EXPECT_EQ(decodedZone1.zone.Value(), 1); + EXPECT_TRUE(decodedZone1.sensitivity.HasValue()); + EXPECT_EQ(decodedZone1.sensitivity.Value(), 5); + + EXPECT_TRUE(readMotionZonesIter.Next()); + const auto & decodedZone2 = readMotionZonesIter.GetValue(); + EXPECT_TRUE(!decodedZone2.zone.IsNull()); + EXPECT_EQ(decodedZone2.zone.Value(), 2); + EXPECT_TRUE(decodedZone2.sensitivity.HasValue()); + EXPECT_EQ(decodedZone2.sensitivity.Value(), 10); + + // Should be no more entries + EXPECT_FALSE(readMotionZonesIter.Next()); + + EXPECT_FALSE(readTriggerOptions.motionSensitivity.HasValue()); + EXPECT_TRUE(readTriggerOptions.maxPreRollLen.HasValue()); + EXPECT_EQ(readTriggerOptions.maxPreRollLen.Value(), 1000); + + EXPECT_TRUE(readTriggerOptions.motionTimeControl.HasValue()); + Structs::TransportMotionTriggerTimeControlStruct::DecodableType readMotionTimeControl = + readTriggerOptions.motionTimeControl.Value(); + EXPECT_EQ(readMotionTimeControl.initialDuration, 5000); + EXPECT_EQ(readMotionTimeControl.augmentationDuration, 2000); + EXPECT_EQ(readMotionTimeControl.maxDuration, (uint32_t) 30000); + EXPECT_EQ(readMotionTimeControl.blindDuration, 1000); + + EXPECT_EQ(readTransportOptions.ingestMethod, IngestMethodsEnum::kCMAFIngest); + + Structs::ContainerOptionsStruct::DecodableType readContainerOptions = readTransportOptions.containerOptions; + EXPECT_EQ(readContainerOptions.containerType, ContainerFormatEnum::kCmaf); + EXPECT_TRUE(readContainerOptions.CMAFContainerOptions.HasValue()); + Structs::CMAFContainerOptionsStruct::Type readCMAFContainerOptions = readContainerOptions.CMAFContainerOptions.Value(); + EXPECT_EQ(readCMAFContainerOptions.chunkDuration, 1000); + EXPECT_FALSE(readCMAFContainerOptions.metadataEnabled.HasValue()); + + std::string cencKeyStr(readCMAFContainerOptions.CENCKey.Value().data(), + readCMAFContainerOptions.CENCKey.Value().data() + readCMAFContainerOptions.CENCKey.Value().size()); + + EXPECT_EQ(cencKeyStr, "1234567890ABCDEF"); + + std::string cencKeyIDStr(readCMAFContainerOptions.CENCKeyID.Value().data(), + readCMAFContainerOptions.CENCKeyID.Value().data() + readCMAFContainerOptions.CENCKeyID.Value().size()); + + EXPECT_EQ(cencKeyIDStr, "1234567890ABCDEF"); + + /* + * Test DeallocatePushTransport + */ + MockCommandHandler deallocateCommandHandler; + deallocateCommandHandler.SetFabricIndex(1); + ConcreteCommandPath kDeallocateCommandPath{ 1, Clusters::PushAvStreamTransport::Id, Commands::DeallocatePushTransport::Id }; + Commands::DeallocatePushTransport::DecodableType deallocateCommandData; + deallocateCommandData.connectionID = allocatedConnectionID; + + EXPECT_EQ( + server.GetLogic().HandleDeallocatePushTransport(deallocateCommandHandler, kDeallocateCommandPath, deallocateCommandData), + std::nullopt); + + EXPECT_EQ(server.GetLogic().mCurrentConnections.size(), (size_t) 0); +} + +TEST_F(MockEventLogging, Test_AllocateTransport_ModifyTransport_FindTransport_FindTransportResponse) +{ + /* + * Test AllocatePushTransport + */ + std::string cencKey = "1234567890ABCDEF"; + std::string cencKeyID = "1234567890ABCDEF"; + + CMAFContainerOptionsStruct cmafContainerOptions; + ContainerOptionsStruct containerOptions; + TransportMotionTriggerTimeControlDecodableStruct motionTimeControl; + std::vector mTransportZoneOptions; + TransportTriggerOptionsDecodableStruct triggerOptions; + + std::string url = "rtsp://192.168.1.100:554/stream"; + TransportOptionsDecodableStruct transportOptions; + + uint8_t tlvBuffer[512]; + Structs::TransportZoneOptionsStruct::Type zone1; + Structs::TransportZoneOptionsStruct::Type zone2; + DataModel::DecodableList decodedList; + + // Create CMAFContainerOptionsStruct object + cmafContainerOptions.chunkDuration = 1000; + cmafContainerOptions.metadataEnabled.ClearValue(); + + cmafContainerOptions.CENCKey.SetValue(ByteSpan(reinterpret_cast(cencKey.c_str()), cencKey.size())); + cmafContainerOptions.CENCKeyID.SetValue(ByteSpan(reinterpret_cast(cencKeyID.c_str()), cencKeyID.size())); + + // Create ContainerOptionsStruct object + containerOptions.containerType = ContainerFormatEnum::kCmaf; + containerOptions.CMAFContainerOptions.SetValue(cmafContainerOptions); + + // Create a TransportMotionTriggerTimeControlStruct object + motionTimeControl.initialDuration = 5000; + motionTimeControl.augmentationDuration = 2000; + motionTimeControl.maxDuration = 30000; + motionTimeControl.blindDuration = 1000; + + triggerOptions.triggerType = TransportTriggerTypeEnum::kMotion; + + // Create transport zone options structs + zone1.zone.SetNonNull(1); + zone1.sensitivity.SetValue(5); + + zone2.zone.SetNonNull(2); + zone2.sensitivity.SetValue(10); + + // Encode them into a TLV buffer + TLV::TLVWriter writer; + writer.Init(tlvBuffer, sizeof(tlvBuffer)); + + TLV::TLVWriter containerWriter; + CHIP_ERROR err; + + err = writer.OpenContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone1); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone2); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = writer.CloseContainer(containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + size_t encodedLen = writer.GetLengthWritten(); + + // Decode the TLV into a DecodableList + TLV::TLVReader motionZonesReader; + motionZonesReader.Init(tlvBuffer, static_cast(encodedLen)); + err = motionZonesReader.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = decodedList.Decode(motionZonesReader); + EXPECT_EQ(err, CHIP_NO_ERROR); + + triggerOptions.motionZones.SetValue(DataModel::MakeNullable(decodedList)); + + triggerOptions.motionSensitivity.ClearValue(); + triggerOptions.motionTimeControl.SetValue(motionTimeControl); + triggerOptions.maxPreRollLen.SetValue(1000); + + // Create TransportOptionsStruct object + transportOptions.streamUsage = StreamUsageEnum::kAnalysis; + transportOptions.videoStreamID.SetValue(1); + transportOptions.audioStreamID.SetValue(2); + transportOptions.endpointID = 1; + transportOptions.url = Span(url.data(), url.size()); + transportOptions.triggerOptions = triggerOptions; + transportOptions.containerOptions = containerOptions; + transportOptions.expiryTime.ClearValue(); + + PushAvStreamTransportServer server(1, BitFlags(1)); + TestPushAVStreamTransportDelegateImpl mockDelegate; + + MockCommandHandler commandHandler; + commandHandler.SetFabricIndex(1); + ConcreteCommandPath kCommandPath{ 1, Clusters::PushAvStreamTransport::Id, Commands::AllocatePushTransport::Id }; + Commands::AllocatePushTransport::DecodableType commandData; + commandData.transportOptions = transportOptions; + + // Set the delegate to the server logic + server.GetLogic().SetDelegate(1, &mockDelegate); + EXPECT_EQ(server.GetLogic().HandleAllocatePushTransport(commandHandler, kCommandPath, commandData), std::nullopt); + + EXPECT_EQ(server.GetLogic().mCurrentConnections.size(), (size_t) 1); + uint16_t allocatedConnectionID = server.GetLogic().mCurrentConnections[0].connectionID; + + /* + * Test ModifyPushTransport + */ + + // Create CMAFContainerOptionsStruct object + cmafContainerOptions.chunkDuration = 500; + cmafContainerOptions.metadataEnabled.ClearValue(); + + cencKey = "ABCDEF1234567890"; + cencKeyID = "ABCDEF1234567890"; + + cmafContainerOptions.CENCKey.SetValue(ByteSpan(reinterpret_cast(cencKey.c_str()), cencKey.size())); + cmafContainerOptions.CENCKeyID.SetValue(ByteSpan(reinterpret_cast(cencKeyID.c_str()), cencKeyID.size())); + + // Create ContainerOptionsStruct object + containerOptions.containerType = ContainerFormatEnum::kCmaf; + containerOptions.CMAFContainerOptions.SetValue(cmafContainerOptions); + + // Create a TransportMotionTriggerTimeControlStruct object + motionTimeControl.initialDuration = 1000; + motionTimeControl.augmentationDuration = 1000; + motionTimeControl.maxDuration = 10000; + motionTimeControl.blindDuration = 1000; + + triggerOptions.triggerType = TransportTriggerTypeEnum::kMotion; + + // Create transport zone options structs + zone1.zone.SetNonNull(7); + zone1.sensitivity.SetValue(8); + + zone2.zone.SetNonNull(9); + zone2.sensitivity.SetValue(6); + + // Encode them into a TLV buffer + TLV::TLVWriter modifyTLVWriter; + modifyTLVWriter.Init(tlvBuffer, sizeof(tlvBuffer)); + + TLV::TLVWriter modifyContainerWriter; + + err = modifyTLVWriter.OpenContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, modifyContainerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(modifyContainerWriter, TLV::AnonymousTag(), zone1); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(modifyContainerWriter, TLV::AnonymousTag(), zone2); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = modifyTLVWriter.CloseContainer(modifyContainerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + size_t modifyEncodedLen = modifyTLVWriter.GetLengthWritten(); + + // Decode the TLV into a DecodableList + TLV::TLVReader modifyMotionZonesReader; + modifyMotionZonesReader.Init(tlvBuffer, static_cast(modifyEncodedLen)); + err = modifyMotionZonesReader.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + DataModel::DecodableList modifyDecodedList; + + err = modifyDecodedList.Decode(modifyMotionZonesReader); + EXPECT_EQ(err, CHIP_NO_ERROR); + + triggerOptions.motionZones.SetValue(DataModel::MakeNullable(modifyDecodedList)); + + triggerOptions.motionSensitivity.ClearValue(); + triggerOptions.motionTimeControl.SetValue(motionTimeControl); + triggerOptions.maxPreRollLen.SetValue(1000); + + // Create TransportOptionsStruct object + transportOptions.streamUsage = StreamUsageEnum::kAnalysis; + transportOptions.videoStreamID.SetValue(11); + transportOptions.audioStreamID.SetValue(22); + transportOptions.endpointID = 1; + url = "rtsp://192.168.1.100:554/modify-stream"; + transportOptions.url = Span(url.data(), url.size()); + transportOptions.triggerOptions = triggerOptions; + transportOptions.containerOptions = containerOptions; + transportOptions.expiryTime.ClearValue(); + + MockCommandHandler modifyCommandHandler; + modifyCommandHandler.SetFabricIndex(1); + + ConcreteCommandPath kModifyCommandPath{ 1, Clusters::PushAvStreamTransport::Id, Commands::ModifyPushTransport::Id }; + Commands::ModifyPushTransport::DecodableType modifyCommandData; + modifyCommandData.connectionID = allocatedConnectionID; + modifyCommandData.transportOptions = transportOptions; + + server.GetLogic().HandleModifyPushTransport(modifyCommandHandler, kModifyCommandPath, modifyCommandData); + + EXPECT_EQ(server.GetLogic().mCurrentConnections.size(), (size_t) 1); + + /* + * Test FindPushTransport + */ + + MockCommandHandler findCommandHandler; + findCommandHandler.SetFabricIndex(1); + + ConcreteCommandPath kFindCommandPath{ 1, Clusters::PushAvStreamTransport::Id, Commands::FindTransport::Id }; + Commands::FindTransport::DecodableType findCommandData; + // As connectionID is static, the new allocated connectionID will be 2. + findCommandData.connectionID.SetValue(DataModel::MakeNullable(allocatedConnectionID)); + + server.GetLogic().HandleFindTransport(findCommandHandler, kFindCommandPath, findCommandData); + + // Check the response + const auto & responses = findCommandHandler.GetResponses(); + EXPECT_EQ(responses.size(), (size_t) 1); + + // Get the encoded buffer + const auto & encodedBuffer = responses[0].encodedData; + + PrintBufHex(encodedBuffer->Start(), encodedBuffer->DataLength()); + + EXPECT_FALSE(encodedBuffer.IsNull()); + + // Set up TLV reader + TLV::TLVReader responseReader; + responseReader.Init(encodedBuffer->Start(), static_cast(encodedBuffer->DataLength())); + + // Enter the top-level anonymous structure (CommandDataIB wrapper) + err = responseReader.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + TLV::TLVReader outerContainer; + err = responseReader.OpenContainer(outerContainer); + EXPECT_EQ(err, CHIP_NO_ERROR); + + // Read the next element inside the container: should be kFields + err = outerContainer.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + EXPECT_TRUE(IsContextTag(outerContainer.GetTag())); + EXPECT_EQ(TagNumFromTag(outerContainer.GetTag()), chip::to_underlying(CommandDataIB::Tag::kFields)); + + // Decode into response object + Commands::FindTransportResponse::DecodableType decodedResponse; + err = decodedResponse.Decode(outerContainer); + EXPECT_EQ(err, CHIP_NO_ERROR); + + auto iter = decodedResponse.transportConfigurations.begin(); + + EXPECT_TRUE(iter.Next()); + + Structs::TransportConfigurationStruct::DecodableType readTransportConfiguration = iter.GetValue(); + EXPECT_EQ(readTransportConfiguration.connectionID, allocatedConnectionID); + EXPECT_EQ(readTransportConfiguration.transportStatus, TransportStatusEnum::kInactive); + + EXPECT_TRUE(readTransportConfiguration.transportOptions.HasValue()); + Structs::TransportOptionsStruct::DecodableType findTransportOptions = readTransportConfiguration.transportOptions.Value(); + EXPECT_EQ(findTransportOptions.streamUsage, StreamUsageEnum::kAnalysis); + EXPECT_EQ(findTransportOptions.videoStreamID, 11); + EXPECT_EQ(findTransportOptions.audioStreamID, 22); + EXPECT_EQ(findTransportOptions.endpointID, 1); + + std::string findUrlStr(findTransportOptions.url.data(), findTransportOptions.url.size()); + EXPECT_EQ(findUrlStr, "rtsp://192.168.1.100:554/modify-stream"); + + Structs::TransportTriggerOptionsStruct::DecodableType findTriggerOptions = findTransportOptions.triggerOptions; + EXPECT_EQ(findTriggerOptions.triggerType, TransportTriggerTypeEnum::kMotion); + EXPECT_TRUE(findTriggerOptions.motionZones.HasValue()); + EXPECT_FALSE(findTriggerOptions.motionZones.Value().IsNull()); + + DataModel::DecodableList findMotionZonesList = + findTriggerOptions.motionZones.Value().Value(); + + auto findMotionZonesIter = findMotionZonesList.begin(); + + EXPECT_TRUE(findMotionZonesIter.Next()); + const auto & decodedZone1 = findMotionZonesIter.GetValue(); + EXPECT_TRUE(!decodedZone1.zone.IsNull()); + EXPECT_EQ(decodedZone1.zone.Value(), 7); + EXPECT_TRUE(decodedZone1.sensitivity.HasValue()); + EXPECT_EQ(decodedZone1.sensitivity.Value(), 8); + + EXPECT_TRUE(findMotionZonesIter.Next()); + const auto & decodedZone2 = findMotionZonesIter.GetValue(); + EXPECT_TRUE(!decodedZone2.zone.IsNull()); + EXPECT_EQ(decodedZone2.zone.Value(), 9); + EXPECT_TRUE(decodedZone2.sensitivity.HasValue()); + EXPECT_EQ(decodedZone2.sensitivity.Value(), 6); + + // Should be no more entries + EXPECT_FALSE(findMotionZonesIter.Next()); + + EXPECT_FALSE(findTriggerOptions.motionSensitivity.HasValue()); + EXPECT_TRUE(findTriggerOptions.maxPreRollLen.HasValue()); + EXPECT_EQ(findTriggerOptions.maxPreRollLen.Value(), 1000); + + EXPECT_TRUE(findTriggerOptions.motionTimeControl.HasValue()); + Structs::TransportMotionTriggerTimeControlStruct::DecodableType findMotionTimeControl = + findTriggerOptions.motionTimeControl.Value(); + EXPECT_EQ(findMotionTimeControl.initialDuration, 1000); + EXPECT_EQ(findMotionTimeControl.augmentationDuration, 1000); + EXPECT_EQ(findMotionTimeControl.maxDuration, (uint32_t) 10000); + EXPECT_EQ(findMotionTimeControl.blindDuration, 1000); + + EXPECT_EQ(findTransportOptions.ingestMethod, IngestMethodsEnum::kCMAFIngest); + + Structs::ContainerOptionsStruct::DecodableType findContainerOptions = findTransportOptions.containerOptions; + EXPECT_EQ(findContainerOptions.containerType, ContainerFormatEnum::kCmaf); + EXPECT_TRUE(findContainerOptions.CMAFContainerOptions.HasValue()); + Structs::CMAFContainerOptionsStruct::Type findCMAFContainerOptions = findContainerOptions.CMAFContainerOptions.Value(); + EXPECT_EQ(findCMAFContainerOptions.chunkDuration, 500); + EXPECT_FALSE(findCMAFContainerOptions.metadataEnabled.HasValue()); + + std::string cencKeyStr(findCMAFContainerOptions.CENCKey.Value().data(), + findCMAFContainerOptions.CENCKey.Value().data() + findCMAFContainerOptions.CENCKey.Value().size()); + + EXPECT_EQ(cencKeyStr, "ABCDEF1234567890"); + + std::string cencKeyIDStr(findCMAFContainerOptions.CENCKeyID.Value().data(), + findCMAFContainerOptions.CENCKeyID.Value().data() + findCMAFContainerOptions.CENCKeyID.Value().size()); + + EXPECT_EQ(cencKeyIDStr, "ABCDEF1234567890"); + + EXPECT_FALSE(iter.Next()); +} + +TEST_F(MockEventLogging, Test_AllocateTransport_SetTransportStatus_ManuallyTriggerTransport) +{ + std::string cencKey = "1234567890ABCDEF"; + std::string cencKeyID = "1234567890ABCDEF"; + + CMAFContainerOptionsStruct cmafContainerOptions; + ContainerOptionsStruct containerOptions; + TransportMotionTriggerTimeControlDecodableStruct motionTimeControl; + std::vector mTransportZoneOptions; + TransportTriggerOptionsDecodableStruct triggerOptions; + + std::string url = "rtsp://192.168.1.100:554/stream"; + TransportOptionsDecodableStruct transportOptions; + + uint8_t tlvBuffer[512]; + Structs::TransportZoneOptionsStruct::Type zone1; + Structs::TransportZoneOptionsStruct::Type zone2; + DataModel::DecodableList decodedList; + + // Create CMAFContainerOptionsStruct object + cmafContainerOptions.chunkDuration = 1000; + cmafContainerOptions.metadataEnabled.ClearValue(); + + cmafContainerOptions.CENCKey.SetValue(ByteSpan(reinterpret_cast(cencKey.c_str()), cencKey.size())); + cmafContainerOptions.CENCKeyID.SetValue(ByteSpan(reinterpret_cast(cencKeyID.c_str()), cencKeyID.size())); + + // Create ContainerOptionsStruct object + containerOptions.containerType = ContainerFormatEnum::kCmaf; + containerOptions.CMAFContainerOptions.SetValue(cmafContainerOptions); + + // Create a TransportMotionTriggerTimeControlStruct object + motionTimeControl.initialDuration = 5000; + motionTimeControl.augmentationDuration = 2000; + motionTimeControl.maxDuration = 30000; + motionTimeControl.blindDuration = 1000; + + triggerOptions.triggerType = TransportTriggerTypeEnum::kMotion; + + // Create transport zone options structs + zone1.zone.SetNonNull(1); + zone1.sensitivity.SetValue(5); + + zone2.zone.SetNonNull(2); + zone2.sensitivity.SetValue(10); + + // Encode them into a TLV buffer + TLV::TLVWriter writer; + writer.Init(tlvBuffer, sizeof(tlvBuffer)); + + TLV::TLVWriter containerWriter; + CHIP_ERROR err; + + err = writer.OpenContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone1); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone2); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = writer.CloseContainer(containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + size_t encodedLen = writer.GetLengthWritten(); + + // Decode the TLV into a DecodableList + TLV::TLVReader motionZonesReader; + motionZonesReader.Init(tlvBuffer, static_cast(encodedLen)); + err = motionZonesReader.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = decodedList.Decode(motionZonesReader); + EXPECT_EQ(err, CHIP_NO_ERROR); + + triggerOptions.motionZones.SetValue(DataModel::MakeNullable(decodedList)); + + triggerOptions.motionSensitivity.ClearValue(); + triggerOptions.motionTimeControl.SetValue(motionTimeControl); + triggerOptions.maxPreRollLen.SetValue(1000); + + // Create TransportOptionsStruct object + transportOptions.streamUsage = StreamUsageEnum::kAnalysis; + transportOptions.videoStreamID.SetValue(1); + transportOptions.audioStreamID.SetValue(2); + transportOptions.endpointID = 1; + transportOptions.url = Span(url.data(), url.size()); + transportOptions.triggerOptions = triggerOptions; + transportOptions.containerOptions = containerOptions; + transportOptions.expiryTime.ClearValue(); + + PushAvStreamTransportServer server(1, BitFlags(1)); + TestPushAVStreamTransportDelegateImpl mockDelegate; + + MockCommandHandler commandHandler; + commandHandler.SetFabricIndex(1); + ConcreteCommandPath kCommandPath{ 1, Clusters::PushAvStreamTransport::Id, Commands::AllocatePushTransport::Id }; + Commands::AllocatePushTransport::DecodableType commandData; + commandData.transportOptions = transportOptions; + + server.GetLogic().SetDelegate(1, &mockDelegate); + EXPECT_EQ(server.GetLogic().HandleAllocatePushTransport(commandHandler, kCommandPath, commandData), std::nullopt); + EXPECT_EQ(server.GetLogic().mCurrentConnections.size(), (size_t) 1); + + uint16_t allocatedConnectionID = server.GetLogic().mCurrentConnections[0].connectionID; + + /* + * Test SetTransportStatus + */ + + MockCommandHandler setCommandHandler; + setCommandHandler.SetFabricIndex(1); + + ConcreteCommandPath kSetCommandPath{ 1, Clusters::PushAvStreamTransport::Id, Commands::SetTransportStatus::Id }; + Commands::SetTransportStatus::DecodableType setCommandData; + + setCommandData.connectionID.SetNonNull(allocatedConnectionID); + setCommandData.transportStatus = TransportStatusEnum::kActive; + server.GetLogic().HandleSetTransportStatus(setCommandHandler, kSetCommandPath, setCommandData); + + EXPECT_EQ(server.GetLogic().mCurrentConnections[0].transportStatus, TransportStatusEnum::kActive); + + MockCommandHandler triggerCommandHandler; + triggerCommandHandler.SetFabricIndex(1); + + ConcreteCommandPath kTriggerCommandPath{ 1, Clusters::PushAvStreamTransport::Id, Commands::ManuallyTriggerTransport::Id }; + Commands::ManuallyTriggerTransport::DecodableType triggerCommandData; + triggerCommandData.connectionID = allocatedConnectionID; + triggerCommandData.activationReason = TriggerActivationReasonEnum::kUserInitiated; + + server.GetLogic().HandleManuallyTriggerTransport(triggerCommandHandler, kTriggerCommandPath, triggerCommandData); + + chip::app::EventManagement & logMgmt = chip::app::EventManagement::GetInstance(); + + CheckLogState(logMgmt, 1, chip::app::PriorityLevel::Info); +} + +} // namespace PushAvStreamTransport +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportStorage.cpp b/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportStorage.cpp new file mode 100644 index 00000000000000..db384ba0875269 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportStorage.cpp @@ -0,0 +1,638 @@ +/* + * + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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 + +#include +#include + +namespace chip { +namespace app { +namespace Clusters { +namespace PushAvStreamTransport { + +using TransportZoneOptionsDecodableStruct = Structs::TransportZoneOptionsStruct::DecodableType; +using TransportTriggerOptionsDecodableStruct = Structs::TransportTriggerOptionsStruct::DecodableType; +using TransportMotionTriggerTimeControlDecodableStruct = Structs::TransportMotionTriggerTimeControlStruct::DecodableType; +using TransportOptionsDecodableStruct = Structs::TransportOptionsStruct::DecodableType; + +class TestPushAVStreamTransportStorage : public ::testing::Test +{ +public: + static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } + static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } +}; + +TEST_F(TestPushAVStreamTransportStorage, TestTransportTriggerOptionsStorage) +{ + uint8_t tlvBuffer[512]; + TransportMotionTriggerTimeControlDecodableStruct motionTimeControl; + TransportTriggerOptionsDecodableStruct triggerOptions; + DataModel::DecodableList decodedList; + /*Test TransportOptionsStorage*/ + + // Create a TransportMotionTriggerTimeControlStruct object + motionTimeControl.initialDuration = 5000; + motionTimeControl.augmentationDuration = 2000; + motionTimeControl.maxDuration = 30000; + motionTimeControl.blindDuration = 1000; + + triggerOptions.triggerType = TransportTriggerTypeEnum::kMotion; + + // Create transport zone options structs + Structs::TransportZoneOptionsStruct::Type zone1; + zone1.zone.SetNonNull(1); + zone1.sensitivity.SetValue(5); + + Structs::TransportZoneOptionsStruct::Type zone2; + zone2.zone.SetNonNull(2); + zone2.sensitivity.SetValue(10); + + // Encode them into a TLV buffer + TLV::TLVWriter writer; + writer.Init(tlvBuffer, sizeof(tlvBuffer)); + + TLV::TLVWriter containerWriter; + CHIP_ERROR err; + + err = writer.OpenContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone1); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone2); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = writer.CloseContainer(containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + size_t encodedLen = writer.GetLengthWritten(); + + // Decode the TLV into a DecodableList + TLV::TLVReader reader; + reader.Init(tlvBuffer, static_cast(encodedLen)); + err = reader.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = decodedList.Decode(reader); + EXPECT_EQ(err, CHIP_NO_ERROR); + + triggerOptions.motionZones.SetValue(DataModel::MakeNullable(decodedList)); + triggerOptions.motionSensitivity.SetValue(DataModel::MakeNullable((uint8_t) 5)); + triggerOptions.motionTimeControl.SetValue(motionTimeControl); + triggerOptions.maxPreRollLen.SetValue(1000); + + TransportTriggerOptionsStorage transportTriggerOptionsStorage(triggerOptions); + + EXPECT_EQ(transportTriggerOptionsStorage.triggerType, TransportTriggerTypeEnum::kMotion); + + DataModel::List & motionZonesList = + transportTriggerOptionsStorage.motionZones.Value().Value(); + + EXPECT_EQ(motionZonesList.size(), (size_t) 2); + + Structs::TransportZoneOptionsStruct::Type motionZone1 = motionZonesList[0]; + Structs::TransportZoneOptionsStruct::Type motionZone2 = motionZonesList[1]; + + EXPECT_FALSE(motionZone1.zone.IsNull()); + EXPECT_EQ(motionZone1.zone.Value(), 1); + EXPECT_TRUE(motionZone1.sensitivity.HasValue()); + EXPECT_EQ(motionZone1.sensitivity.Value(), 5); + + EXPECT_FALSE(motionZone2.zone.IsNull()); + EXPECT_EQ(motionZone2.zone.Value(), 2); + EXPECT_TRUE(motionZone2.sensitivity.HasValue()); + EXPECT_EQ(motionZone2.sensitivity.Value(), 10); + + EXPECT_EQ(transportTriggerOptionsStorage.motionSensitivity.Value().Value(), (uint8_t) 5); + + EXPECT_EQ(transportTriggerOptionsStorage.motionTimeControl.Value().initialDuration, 5000); + EXPECT_EQ(transportTriggerOptionsStorage.motionTimeControl.Value().augmentationDuration, 2000); + EXPECT_EQ(transportTriggerOptionsStorage.motionTimeControl.Value().maxDuration, (uint32_t) 30000); + EXPECT_EQ(transportTriggerOptionsStorage.motionTimeControl.Value().blindDuration, 1000); + EXPECT_EQ(transportTriggerOptionsStorage.maxPreRollLen.Value(), 1000); + + TransportTriggerOptionsStorage transportTriggerOptionsStorageCopy(transportTriggerOptionsStorage); + + EXPECT_EQ(transportTriggerOptionsStorageCopy.triggerType, TransportTriggerTypeEnum::kMotion); + + DataModel::List & motionZonesListCopy = + transportTriggerOptionsStorageCopy.motionZones.Value().Value(); + + EXPECT_EQ(motionZonesListCopy.size(), (size_t) 2); + + Structs::TransportZoneOptionsStruct::Type motionZone1Copy = motionZonesListCopy[0]; + Structs::TransportZoneOptionsStruct::Type motionZone2Copy = motionZonesListCopy[1]; + + EXPECT_FALSE(motionZone1Copy.zone.IsNull()); + EXPECT_EQ(motionZone1Copy.zone.Value(), 1); + EXPECT_TRUE(motionZone1Copy.sensitivity.HasValue()); + EXPECT_EQ(motionZone1Copy.sensitivity.Value(), 5); + + EXPECT_FALSE(motionZone2Copy.zone.IsNull()); + EXPECT_EQ(motionZone2Copy.zone.Value(), 2); + EXPECT_TRUE(motionZone2Copy.sensitivity.HasValue()); + EXPECT_EQ(motionZone2Copy.sensitivity.Value(), 10); + + EXPECT_EQ(transportTriggerOptionsStorageCopy.motionSensitivity.Value().Value(), (uint8_t) 5); + + EXPECT_EQ(transportTriggerOptionsStorageCopy.motionTimeControl.Value().initialDuration, 5000); + EXPECT_EQ(transportTriggerOptionsStorageCopy.motionTimeControl.Value().augmentationDuration, 2000); + EXPECT_EQ(transportTriggerOptionsStorageCopy.motionTimeControl.Value().maxDuration, (uint32_t) 30000); + EXPECT_EQ(transportTriggerOptionsStorageCopy.motionTimeControl.Value().blindDuration, 1000); + + // Accessing buffer using base struct + TransportTriggerOptionsStruct BaseTransportTriggerOptions = + static_cast(transportTriggerOptionsStorageCopy); + + DataModel::List & motionZonesListBase = + BaseTransportTriggerOptions.motionZones.Value().Value(); + + EXPECT_EQ(motionZonesListBase.size(), (size_t) 2); + + Structs::TransportZoneOptionsStruct::Type motionZone1Base = motionZonesListBase[0]; + Structs::TransportZoneOptionsStruct::Type motionZone2Base = motionZonesListBase[1]; + + EXPECT_FALSE(motionZone1Base.zone.IsNull()); + EXPECT_EQ(motionZone1Base.zone.Value(), 1); + EXPECT_TRUE(motionZone1Base.sensitivity.HasValue()); + EXPECT_EQ(motionZone1Base.sensitivity.Value(), 5); + + EXPECT_FALSE(motionZone2Base.zone.IsNull()); + EXPECT_EQ(motionZone2Base.zone.Value(), 2); + EXPECT_TRUE(motionZone2Base.sensitivity.HasValue()); + EXPECT_EQ(motionZone2Base.sensitivity.Value(), 10); +} + +TEST_F(TestPushAVStreamTransportStorage, TestCMAFContainerOptionsStorage) +{ + CMAFContainerOptionsStruct cmafContainerOptions; + /*Test CMAFContainerOptionsStorage*/ + + cmafContainerOptions.chunkDuration = 1000; + cmafContainerOptions.metadataEnabled.SetValue(true); + + std::string cencKey = "1234567890ABCDEF"; + std::string cencKeyID = "1234567890ABCDEF"; + + cmafContainerOptions.CENCKey.SetValue(ByteSpan(reinterpret_cast(cencKey.c_str()), cencKey.size())); + cmafContainerOptions.CENCKeyID.SetValue(ByteSpan(reinterpret_cast(cencKeyID.c_str()), cencKeyID.size())); + + CMAFContainerOptionsStorage cmafContainerOptionsStorage(cmafContainerOptions); + + // Clear the cencKey and cencKeyID strings to test deep copy of cencKey and cencKeyID + cencKey.clear(); + cencKeyID.clear(); + + EXPECT_EQ(cmafContainerOptionsStorage.chunkDuration, 1000); + EXPECT_TRUE(cmafContainerOptionsStorage.metadataEnabled.HasValue()); + EXPECT_TRUE(cmafContainerOptionsStorage.metadataEnabled.Value()); + + std::string cencKeyStr(cmafContainerOptionsStorage.CENCKey.Value().data(), + cmafContainerOptionsStorage.CENCKey.Value().data() + cmafContainerOptionsStorage.CENCKey.Value().size()); + + EXPECT_EQ(cencKeyStr, "1234567890ABCDEF"); + + std::string cencKeyIDStr(cmafContainerOptionsStorage.CENCKeyID.Value().data(), + cmafContainerOptionsStorage.CENCKeyID.Value().data() + + cmafContainerOptionsStorage.CENCKeyID.Value().size()); + + EXPECT_EQ(cencKeyIDStr, "1234567890ABCDEF"); + + CMAFContainerOptionsStorage cmafContainerOptionsStorageCopy(cmafContainerOptionsStorage); + + EXPECT_EQ(cmafContainerOptionsStorageCopy.chunkDuration, 1000); + EXPECT_TRUE(cmafContainerOptionsStorageCopy.metadataEnabled.HasValue()); + EXPECT_TRUE(cmafContainerOptionsStorageCopy.metadataEnabled.Value()); + + std::string cencKeyStrCopy(cmafContainerOptionsStorageCopy.CENCKey.Value().data(), + cmafContainerOptionsStorageCopy.CENCKey.Value().data() + + cmafContainerOptionsStorageCopy.CENCKey.Value().size()); + + EXPECT_EQ(cencKeyStrCopy, "1234567890ABCDEF"); + + std::string cencKeyIDStrCopy(cmafContainerOptionsStorageCopy.CENCKeyID.Value().data(), + cmafContainerOptionsStorageCopy.CENCKeyID.Value().data() + + cmafContainerOptionsStorageCopy.CENCKeyID.Value().size()); + + EXPECT_EQ(cencKeyIDStrCopy, "1234567890ABCDEF"); + + // Accessing buffer using base struct + + CMAFContainerOptionsStruct BaseCMAFContainerOptions = static_cast(cmafContainerOptionsStorageCopy); + + EXPECT_EQ(BaseCMAFContainerOptions.chunkDuration, 1000); + EXPECT_TRUE(BaseCMAFContainerOptions.metadataEnabled.HasValue()); + EXPECT_TRUE(BaseCMAFContainerOptions.metadataEnabled.Value()); + + std::string cencKeyStrBase(BaseCMAFContainerOptions.CENCKey.Value().data(), + BaseCMAFContainerOptions.CENCKey.Value().data() + BaseCMAFContainerOptions.CENCKey.Value().size()); + + EXPECT_EQ(cencKeyStrBase, "1234567890ABCDEF"); + + std::string cencKeyIDStrBase(BaseCMAFContainerOptions.CENCKeyID.Value().data(), + BaseCMAFContainerOptions.CENCKeyID.Value().data() + + BaseCMAFContainerOptions.CENCKeyID.Value().size()); + + EXPECT_EQ(cencKeyIDStrBase, "1234567890ABCDEF"); +} + +TEST_F(TestPushAVStreamTransportStorage, TestContainerOptionsStorage) +{ + CMAFContainerOptionsStruct cmafContainerOptions; + /*Test CMAFContainerOptionsStorage*/ + + cmafContainerOptions.chunkDuration = 1000; + cmafContainerOptions.metadataEnabled.SetValue(true); + + std::string cencKey = "1234567890ABCDEF"; + std::string cencKeyID = "1234567890ABCDEF"; + + cmafContainerOptions.CENCKey.SetValue(ByteSpan(reinterpret_cast(cencKey.c_str()), cencKey.size())); + cmafContainerOptions.CENCKeyID.SetValue(ByteSpan(reinterpret_cast(cencKeyID.c_str()), cencKeyID.size())); + + /*Test ContainerOptionsStorage*/ + + // Using Type as ContainerOptionsStruct::DecodableType == ContainerOptionsStruct::Type + + ContainerOptionsStruct containerOptions; + + containerOptions.containerType = ContainerFormatEnum::kCmaf; + containerOptions.CMAFContainerOptions.SetValue(cmafContainerOptions); + + ContainerOptionsStorage containerOptionsStorage(containerOptions); + + // Clear the cencKey and cencKeyID strings to test deep copy of cencKey and cencKeyID + cencKey.clear(); + cencKeyID.clear(); + + EXPECT_EQ(containerOptionsStorage.containerType, ContainerFormatEnum::kCmaf); + EXPECT_TRUE(containerOptionsStorage.CMAFContainerOptions.HasValue()); + EXPECT_EQ(containerOptionsStorage.CMAFContainerOptions.Value().chunkDuration, 1000); + EXPECT_TRUE(containerOptionsStorage.CMAFContainerOptions.Value().metadataEnabled.HasValue()); + EXPECT_TRUE(containerOptionsStorage.CMAFContainerOptions.Value().metadataEnabled.Value()); + + std::string cencKeyStrContainer(containerOptionsStorage.CMAFContainerOptions.Value().CENCKey.Value().data(), + containerOptionsStorage.CMAFContainerOptions.Value().CENCKey.Value().data() + + containerOptionsStorage.CMAFContainerOptions.Value().CENCKey.Value().size()); + + EXPECT_EQ(cencKeyStrContainer, "1234567890ABCDEF"); + + std::string cencKeyIDStrContainer(containerOptionsStorage.CMAFContainerOptions.Value().CENCKeyID.Value().data(), + containerOptionsStorage.CMAFContainerOptions.Value().CENCKeyID.Value().data() + + containerOptionsStorage.CMAFContainerOptions.Value().CENCKeyID.Value().size()); + + EXPECT_EQ(cencKeyIDStrContainer, "1234567890ABCDEF"); + + ContainerOptionsStorage containerOptionsStorageCopy(containerOptionsStorage); + + EXPECT_EQ(containerOptionsStorageCopy.containerType, ContainerFormatEnum::kCmaf); + EXPECT_TRUE(containerOptionsStorageCopy.CMAFContainerOptions.HasValue()); + EXPECT_EQ(containerOptionsStorageCopy.CMAFContainerOptions.Value().chunkDuration, 1000); + EXPECT_TRUE(containerOptionsStorageCopy.CMAFContainerOptions.Value().metadataEnabled.HasValue()); + EXPECT_TRUE(containerOptionsStorageCopy.CMAFContainerOptions.Value().metadataEnabled.Value()); + + std::string cencKeyStrContainerCopy(containerOptionsStorageCopy.CMAFContainerOptions.Value().CENCKey.Value().data(), + containerOptionsStorageCopy.CMAFContainerOptions.Value().CENCKey.Value().data() + + containerOptionsStorageCopy.CMAFContainerOptions.Value().CENCKey.Value().size()); + + EXPECT_EQ(cencKeyStrContainerCopy, "1234567890ABCDEF"); + + std::string cencKeyIDStrContainerCopy(containerOptionsStorageCopy.CMAFContainerOptions.Value().CENCKeyID.Value().data(), + containerOptionsStorageCopy.CMAFContainerOptions.Value().CENCKeyID.Value().data() + + containerOptionsStorageCopy.CMAFContainerOptions.Value().CENCKeyID.Value().size()); + + EXPECT_EQ(cencKeyIDStrContainerCopy, "1234567890ABCDEF"); + + // Accessing buffer using base struct + + ContainerOptionsStruct BaseContainerOptions = static_cast(containerOptionsStorage); + + EXPECT_EQ(BaseContainerOptions.containerType, ContainerFormatEnum::kCmaf); + EXPECT_TRUE(BaseContainerOptions.CMAFContainerOptions.HasValue()); + EXPECT_EQ(BaseContainerOptions.CMAFContainerOptions.Value().chunkDuration, 1000); + EXPECT_TRUE(BaseContainerOptions.CMAFContainerOptions.Value().metadataEnabled.HasValue()); + EXPECT_TRUE(BaseContainerOptions.CMAFContainerOptions.Value().metadataEnabled.Value()); + + std::string containerCENCKeyStrBase(BaseContainerOptions.CMAFContainerOptions.Value().CENCKey.Value().data(), + BaseContainerOptions.CMAFContainerOptions.Value().CENCKey.Value().data() + + BaseContainerOptions.CMAFContainerOptions.Value().CENCKey.Value().size()); + + EXPECT_EQ(containerCENCKeyStrBase, "1234567890ABCDEF"); + + std::string containerCENCKeyIDStrBase(BaseContainerOptions.CMAFContainerOptions.Value().CENCKeyID.Value().data(), + BaseContainerOptions.CMAFContainerOptions.Value().CENCKeyID.Value().data() + + BaseContainerOptions.CMAFContainerOptions.Value().CENCKeyID.Value().size()); + + EXPECT_EQ(containerCENCKeyIDStrBase, "1234567890ABCDEF"); +} + +TEST_F(TestPushAVStreamTransportStorage, TestTransportOptionsStorage) +{ + uint8_t tlvBuffer[512]; + TransportMotionTriggerTimeControlDecodableStruct motionTimeControl; + TransportTriggerOptionsDecodableStruct triggerOptions; + DataModel::DecodableList decodedList; + CMAFContainerOptionsStruct cmafContainerOptions; + ContainerOptionsStruct containerOptions; + TransportOptionsDecodableStruct transportOptions; + + /*Test TransportOptionsStorage*/ + + // Create a TransportMotionTriggerTimeControlStruct object + motionTimeControl.initialDuration = 5000; + motionTimeControl.augmentationDuration = 2000; + motionTimeControl.maxDuration = 30000; + motionTimeControl.blindDuration = 1000; + + triggerOptions.triggerType = TransportTriggerTypeEnum::kMotion; + + // Create transport zone options structs + Structs::TransportZoneOptionsStruct::Type zone1; + zone1.zone.SetNonNull(1); + zone1.sensitivity.SetValue(5); + + Structs::TransportZoneOptionsStruct::Type zone2; + zone2.zone.SetNonNull(2); + zone2.sensitivity.SetValue(10); + + // Encode them into a TLV buffer + TLV::TLVWriter writer; + writer.Init(tlvBuffer, sizeof(tlvBuffer)); + + TLV::TLVWriter containerWriter; + CHIP_ERROR err; + + err = writer.OpenContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone1); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone2); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = writer.CloseContainer(containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + size_t encodedLen = writer.GetLengthWritten(); + + // Decode the TLV into a DecodableList + TLV::TLVReader reader; + reader.Init(tlvBuffer, static_cast(encodedLen)); + err = reader.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = decodedList.Decode(reader); + EXPECT_EQ(err, CHIP_NO_ERROR); + + triggerOptions.motionZones.SetValue(DataModel::MakeNullable(decodedList)); + triggerOptions.motionSensitivity.SetValue(DataModel::MakeNullable((uint8_t) 5)); + triggerOptions.motionTimeControl.SetValue(motionTimeControl); + triggerOptions.maxPreRollLen.SetValue(1000); + + /*Test CMAFContainerOptionsStorage*/ + + cmafContainerOptions.chunkDuration = 1000; + cmafContainerOptions.metadataEnabled.SetValue(true); + + std::string cencKey = "1234567890ABCDEF"; + std::string cencKeyID = "1234567890ABCDEF"; + + cmafContainerOptions.CENCKey.SetValue(ByteSpan(reinterpret_cast(cencKey.c_str()), cencKey.size())); + cmafContainerOptions.CENCKeyID.SetValue(ByteSpan(reinterpret_cast(cencKeyID.c_str()), cencKeyID.size())); + + containerOptions.containerType = ContainerFormatEnum::kCmaf; + containerOptions.CMAFContainerOptions.SetValue(cmafContainerOptions); + + /*Test TransportOptionsStorage*/ + triggerOptions.triggerType = TransportTriggerTypeEnum::kMotion; + triggerOptions.motionZones.SetValue(DataModel::MakeNullable(decodedList)); + + triggerOptions.motionSensitivity.SetValue(DataModel::MakeNullable((uint8_t) 5)); + triggerOptions.motionTimeControl.SetValue(motionTimeControl); + triggerOptions.maxPreRollLen.SetValue(1000); + + transportOptions.streamUsage = StreamUsageEnum::kAnalysis; + transportOptions.videoStreamID.SetValue(1); + transportOptions.audioStreamID.SetValue(2); + transportOptions.endpointID = 1; + std::string url = "rtsp://192.168.1.100:554/stream"; + transportOptions.url = Span(url.data(), url.size()); + transportOptions.triggerOptions = triggerOptions; + transportOptions.ingestMethod = IngestMethodsEnum::kCMAFIngest; + transportOptions.containerOptions = containerOptions; + transportOptions.expiryTime.SetValue(1000); + + TransportOptionsStorage transportOptionsStorage(transportOptions); + + url.clear(); // Clear the url string to test deep copy of url + + EXPECT_EQ(transportOptionsStorage.streamUsage, StreamUsageEnum::kAnalysis); + EXPECT_EQ(transportOptionsStorage.videoStreamID.Value(), (uint16_t) 1); + EXPECT_EQ(transportOptionsStorage.audioStreamID.Value(), (uint16_t) 2); + EXPECT_EQ(transportOptionsStorage.endpointID, 1); + + std::string transportOptionsUrlStr(transportOptionsStorage.url.data(), transportOptionsStorage.url.size()); + EXPECT_EQ(transportOptionsUrlStr, "rtsp://192.168.1.100:554/stream"); + + TransportOptionsStorage transportOptionsStorageCopy(transportOptionsStorage); + + EXPECT_EQ(transportOptionsStorageCopy.streamUsage, StreamUsageEnum::kAnalysis); + EXPECT_EQ(transportOptionsStorageCopy.videoStreamID.Value(), (uint16_t) 1); + EXPECT_EQ(transportOptionsStorageCopy.audioStreamID.Value(), (uint16_t) 2); + EXPECT_EQ(transportOptionsStorageCopy.endpointID, 1); + + std::string transportOptionsUrlStrCopy(transportOptionsStorageCopy.url.data(), transportOptionsStorageCopy.url.size()); + EXPECT_EQ(transportOptionsUrlStrCopy, "rtsp://192.168.1.100:554/stream"); + + // Accessing buffer using base struct + + TransportOptionsStruct BaseTransportOptions = static_cast(transportOptionsStorage); + + EXPECT_EQ(BaseTransportOptions.streamUsage, StreamUsageEnum::kAnalysis); + EXPECT_EQ(BaseTransportOptions.videoStreamID.Value(), (uint16_t) 1); + EXPECT_EQ(BaseTransportOptions.audioStreamID.Value(), (uint16_t) 2); + EXPECT_EQ(BaseTransportOptions.endpointID, 1); + + std::string transportOptionsUrlStrBase(BaseTransportOptions.url.data(), BaseTransportOptions.url.size()); + EXPECT_EQ(transportOptionsUrlStrBase, "rtsp://192.168.1.100:554/stream"); +} + +TEST_F(TestPushAVStreamTransportStorage, TestTransportConfigurationStorage) +{ + uint8_t tlvBuffer[512]; + TransportMotionTriggerTimeControlDecodableStruct motionTimeControl; + TransportTriggerOptionsDecodableStruct triggerOptions; + DataModel::DecodableList decodedList; + CMAFContainerOptionsStruct cmafContainerOptions; + ContainerOptionsStruct containerOptions; + TransportOptionsDecodableStruct transportOptions; + + /*Test TransportOptionsStorage*/ + + // Create a TransportMotionTriggerTimeControlStruct object + motionTimeControl.initialDuration = 5000; + motionTimeControl.augmentationDuration = 2000; + motionTimeControl.maxDuration = 30000; + motionTimeControl.blindDuration = 1000; + + triggerOptions.triggerType = TransportTriggerTypeEnum::kMotion; + + // Create transport zone options structs + Structs::TransportZoneOptionsStruct::Type zone1; + zone1.zone.SetNonNull(1); + zone1.sensitivity.SetValue(5); + + Structs::TransportZoneOptionsStruct::Type zone2; + zone2.zone.SetNonNull(2); + zone2.sensitivity.SetValue(10); + + // Encode them into a TLV buffer + TLV::TLVWriter writer; + writer.Init(tlvBuffer, sizeof(tlvBuffer)); + + TLV::TLVWriter containerWriter; + CHIP_ERROR err; + + err = writer.OpenContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone1); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = DataModel::Encode(containerWriter, TLV::AnonymousTag(), zone2); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = writer.CloseContainer(containerWriter); + EXPECT_EQ(err, CHIP_NO_ERROR); + + size_t encodedLen = writer.GetLengthWritten(); + + // Decode the TLV into a DecodableList + TLV::TLVReader reader; + reader.Init(tlvBuffer, static_cast(encodedLen)); + err = reader.Next(); + EXPECT_EQ(err, CHIP_NO_ERROR); + + err = decodedList.Decode(reader); + EXPECT_EQ(err, CHIP_NO_ERROR); + + triggerOptions.motionZones.SetValue(DataModel::MakeNullable(decodedList)); + triggerOptions.motionSensitivity.SetValue(DataModel::MakeNullable((uint8_t) 5)); + triggerOptions.motionTimeControl.SetValue(motionTimeControl); + triggerOptions.maxPreRollLen.SetValue(1000); + + /*Test CMAFContainerOptionsStorage*/ + + cmafContainerOptions.chunkDuration = 1000; + cmafContainerOptions.metadataEnabled.SetValue(true); + + std::string cencKey = "1234567890ABCDEF"; + std::string cencKeyID = "1234567890ABCDEF"; + + cmafContainerOptions.CENCKey.SetValue(ByteSpan(reinterpret_cast(cencKey.c_str()), cencKey.size())); + cmafContainerOptions.CENCKeyID.SetValue(ByteSpan(reinterpret_cast(cencKeyID.c_str()), cencKeyID.size())); + + containerOptions.containerType = ContainerFormatEnum::kCmaf; + containerOptions.CMAFContainerOptions.SetValue(cmafContainerOptions); + + /*Test TransportOptionsStorage*/ + triggerOptions.triggerType = TransportTriggerTypeEnum::kMotion; + triggerOptions.motionZones.SetValue(DataModel::MakeNullable(decodedList)); + + triggerOptions.motionSensitivity.SetValue(DataModel::MakeNullable((uint8_t) 5)); + triggerOptions.motionTimeControl.SetValue(motionTimeControl); + triggerOptions.maxPreRollLen.SetValue(1000); + + transportOptions.streamUsage = StreamUsageEnum::kAnalysis; + transportOptions.videoStreamID.SetValue(1); + transportOptions.audioStreamID.SetValue(2); + transportOptions.endpointID = 1; + std::string url = "rtsp://192.168.1.100:554/stream"; + transportOptions.url = Span(url.data(), url.size()); + transportOptions.triggerOptions = triggerOptions; + transportOptions.ingestMethod = IngestMethodsEnum::kCMAFIngest; + transportOptions.containerOptions = containerOptions; + transportOptions.expiryTime.SetValue(1000); + + /*Test TransportConfigurationStorage*/ + + std::shared_ptr transportOptionsPtr{ new (std::nothrow) TransportOptionsStorage(transportOptions) }; + + TransportConfigurationStorage transportConfigurationStorage(1, transportOptionsPtr); + + url.clear(); // Clear the url string to test deep copy of url + + EXPECT_EQ(transportConfigurationStorage.connectionID, 1); + EXPECT_EQ(transportConfigurationStorage.transportStatus, TransportStatusEnum::kInactive); + EXPECT_EQ(transportConfigurationStorage.transportOptions.Value().streamUsage, StreamUsageEnum::kAnalysis); + EXPECT_EQ(transportConfigurationStorage.transportOptions.Value().videoStreamID.Value(), (uint16_t) 1); + EXPECT_EQ(transportConfigurationStorage.transportOptions.Value().audioStreamID.Value(), (uint16_t) 2); + EXPECT_EQ(transportConfigurationStorage.transportOptions.Value().endpointID, 1); + + std::string transportOptionsUrlStrConfiguration(transportConfigurationStorage.transportOptions.Value().url.data(), + transportConfigurationStorage.transportOptions.Value().url.size()); + EXPECT_EQ(transportOptionsUrlStrConfiguration, "rtsp://192.168.1.100:554/stream"); + + TransportConfigurationStorage transportConfigurationStorageCopy(transportConfigurationStorage); + + EXPECT_EQ(transportConfigurationStorageCopy.connectionID, 1); + EXPECT_EQ(transportConfigurationStorageCopy.transportStatus, TransportStatusEnum::kInactive); + EXPECT_EQ(transportConfigurationStorageCopy.transportOptions.Value().streamUsage, StreamUsageEnum::kAnalysis); + EXPECT_EQ(transportConfigurationStorageCopy.transportOptions.Value().videoStreamID.Value(), (uint16_t) 1); + EXPECT_EQ(transportConfigurationStorageCopy.transportOptions.Value().audioStreamID.Value(), (uint16_t) 2); + EXPECT_EQ(transportConfigurationStorageCopy.transportOptions.Value().endpointID, 1); + + std::string transportOptionsUrlStrConfigurationCopy(transportConfigurationStorageCopy.transportOptions.Value().url.data(), + transportConfigurationStorageCopy.transportOptions.Value().url.size()); + EXPECT_EQ(transportOptionsUrlStrConfigurationCopy, "rtsp://192.168.1.100:554/stream"); + + // Accessing buffer using base struct + + TransportConfigurationStruct BaseTransportConfiguration = + static_cast(transportConfigurationStorage); + + EXPECT_EQ(BaseTransportConfiguration.connectionID, 1); + EXPECT_EQ(BaseTransportConfiguration.transportStatus, TransportStatusEnum::kInactive); + EXPECT_EQ(BaseTransportConfiguration.transportOptions.Value().streamUsage, StreamUsageEnum::kAnalysis); + EXPECT_EQ(BaseTransportConfiguration.transportOptions.Value().videoStreamID.Value(), (uint16_t) 1); + EXPECT_EQ(BaseTransportConfiguration.transportOptions.Value().audioStreamID.Value(), (uint16_t) 2); + EXPECT_EQ(BaseTransportConfiguration.transportOptions.Value().endpointID, 1); + + std::string transportOptionsUrlStrConfigurationBase(BaseTransportConfiguration.transportOptions.Value().url.data(), + BaseTransportConfiguration.transportOptions.Value().url.size()); + EXPECT_EQ(transportOptionsUrlStrConfigurationBase, "rtsp://192.168.1.100:554/stream"); + + /*Test TransportConfigurationStorage with null transport options*/ + + std::shared_ptr nullTransportOptionsPtr; + + TransportConfigurationStorage transportConfigurationStorageNull(1, nullTransportOptionsPtr); + + EXPECT_EQ(transportConfigurationStorageNull.connectionID, 1); + EXPECT_EQ(transportConfigurationStorageNull.transportStatus, TransportStatusEnum::kInactive); + EXPECT_FALSE(transportConfigurationStorageNull.transportOptions.HasValue()); +} + +} // namespace PushAvStreamTransport +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/common/templates/config-data.yaml b/src/app/common/templates/config-data.yaml index b86ac3e7c4cfc2..3f374b2454cbd7 100644 --- a/src/app/common/templates/config-data.yaml +++ b/src/app/common/templates/config-data.yaml @@ -38,6 +38,7 @@ CommandHandlerInterfaceOnlyClusters: - Microwave Oven Control - Chime - Camera AV Stream Management + - Push AV Stream Transport - Commissioner Control - Commodity Price - Energy EVSE diff --git a/src/app/zap_cluster_list.json b/src/app/zap_cluster_list.json index 661c479ff36aaa..e7ecce2e1ec4b9 100644 --- a/src/app/zap_cluster_list.json +++ b/src/app/zap_cluster_list.json @@ -266,7 +266,7 @@ "NITROGEN_DIOXIDE_CONCENTRATION_MEASUREMENT_CLUSTER": [ "concentration-measurement-server" ], - "PUSH_AV_STREAM_TRANSPORT_CLUSTER": [], + "PUSH_AV_STREAM_TRANSPORT_CLUSTER": ["push-av-stream-transport-server"], "SAMPLE_MEI_CLUSTER": ["sample-mei-server"], "OCCUPANCY_SENSING_CLUSTER": ["occupancy-sensor-server"], "ON_OFF_CLUSTER": ["on-off-server"], diff --git a/zzz_generated/app-common/app-common/zap-generated/callback.h b/zzz_generated/app-common/app-common/zap-generated/callback.h index 7b638736189e73..f3e68931fe496b 100644 --- a/zzz_generated/app-common/app-common/zap-generated/callback.h +++ b/zzz_generated/app-common/app-common/zap-generated/callback.h @@ -7890,42 +7890,6 @@ bool emberAfZoneManagementClusterCreateOrUpdateTriggerCallback( bool emberAfZoneManagementClusterRemoveTriggerCallback( chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, const chip::app::Clusters::ZoneManagement::Commands::RemoveTrigger::DecodableType & commandData); -/** - * @brief Push AV Stream Transport Cluster AllocatePushTransport Command callback (from client) - */ -bool emberAfPushAvStreamTransportClusterAllocatePushTransportCallback( - chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::PushAvStreamTransport::Commands::AllocatePushTransport::DecodableType & commandData); -/** - * @brief Push AV Stream Transport Cluster DeallocatePushTransport Command callback (from client) - */ -bool emberAfPushAvStreamTransportClusterDeallocatePushTransportCallback( - chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::PushAvStreamTransport::Commands::DeallocatePushTransport::DecodableType & commandData); -/** - * @brief Push AV Stream Transport Cluster ModifyPushTransport Command callback (from client) - */ -bool emberAfPushAvStreamTransportClusterModifyPushTransportCallback( - chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::PushAvStreamTransport::Commands::ModifyPushTransport::DecodableType & commandData); -/** - * @brief Push AV Stream Transport Cluster SetTransportStatus Command callback (from client) - */ -bool emberAfPushAvStreamTransportClusterSetTransportStatusCallback( - chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::PushAvStreamTransport::Commands::SetTransportStatus::DecodableType & commandData); -/** - * @brief Push AV Stream Transport Cluster ManuallyTriggerTransport Command callback (from client) - */ -bool emberAfPushAvStreamTransportClusterManuallyTriggerTransportCallback( - chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::PushAvStreamTransport::Commands::ManuallyTriggerTransport::DecodableType & commandData); -/** - * @brief Push AV Stream Transport Cluster FindTransport Command callback (from client) - */ -bool emberAfPushAvStreamTransportClusterFindTransportCallback( - chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::PushAvStreamTransport::Commands::FindTransport::DecodableType & commandData); /** * @brief Commodity Tariff Cluster GetTariffComponent Command callback (from client) */