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 2967989f7fda66..9f2a2ca9a68c8e 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 @@ -7403,6 +7403,344 @@ 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; + } + + struct TransportConfigurationStruct { + int16u connectionID = 0; + TransportStatusEnum transportStatus = 1; + optional TransportOptionsStruct transportOptions = 2; + } + + 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 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; + } + + struct TransportConfigurationStruct { + int16u connectionID = 0; + TransportStatusEnum transportStatus = 1; + optional TransportOptionsStruct transportOptions = 2; + } + + 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; @@ -8502,6 +8840,7 @@ endpoint 1 { device type ma_onofflight = 256, version 1; binding cluster OnOff; + binding cluster PushAvStreamTransport; server cluster Identify { ram attribute identifyTime default = 0x0000; @@ -9768,6 +10107,25 @@ endpoint 1 { handle command DPTZRelativeMove; } + server cluster PushAvStreamTransport { + 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 187bd75e13a2a8..01ce147fb3d274 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 @@ -1582,7 +1582,7 @@ "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -21432,6 +21432,270 @@ } ] }, + { + "name": "Push AV Stream Transport", + "code": 1365, + "mfgCode": null, + "define": "PUSH_AV_STREAM_TRANSPORT_CLUSTER", + "side": "client", + "enabled": 1, + "apiMaturity": "provisional", + "commands": [ + { + "name": "AllocatePushTransport", + "code": 0, + "mfgCode": null, + "source": "client", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "AllocatePushTransportResponse", + "code": 1, + "mfgCode": null, + "source": "server", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "DeallocatePushTransport", + "code": 2, + "mfgCode": null, + "source": "client", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "ModifyPushTransport", + "code": 3, + "mfgCode": null, + "source": "client", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "SetTransportStatus", + "code": 4, + "mfgCode": null, + "source": "client", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "ManuallyTriggerTransport", + "code": 5, + "mfgCode": null, + "source": "client", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "FindTransport", + "code": 6, + "mfgCode": null, + "source": "client", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "FindTransportResponse", + "code": 7, + "mfgCode": null, + "source": "server", + "isIncoming": 1, + "isEnabled": 1 + } + ] + }, + { + "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 + } + ] + }, { "name": "Chime", "code": 1366, @@ -21597,10 +21861,10 @@ "side": "server", "type": "MeterTypeEnum", "included": 1, - "storageOption": "RAM", + "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -21613,10 +21877,10 @@ "side": "server", "type": "char_string", "included": 1, - "storageOption": "RAM", + "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -21629,10 +21893,10 @@ "side": "server", "type": "char_string", "included": 1, - "storageOption": "RAM", + "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -21645,10 +21909,10 @@ "side": "server", "type": "char_string", "included": 1, - "storageOption": "RAM", + "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -21664,7 +21928,7 @@ "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -21680,7 +21944,7 @@ "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -21696,7 +21960,7 @@ "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -21712,7 +21976,7 @@ "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, @@ -21725,10 +21989,10 @@ "side": "server", "type": "bitmap32", "included": 1, - "storageOption": "RAM", + "storageOption": "External", "singleton": 0, "bounded": 0, - "defaultValue": "0", + "defaultValue": null, "reportable": 1, "minInterval": 1, "maxInterval": 65534, 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..18d799efdf64ca --- /dev/null +++ b/examples/all-clusters-app/all-clusters-common/include/push-av-stream-transport-delegate-impl.h @@ -0,0 +1,80 @@ +/* + * + * 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 Structs::TransportOptionsStruct::DecodableType 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); + + CHIP_ERROR ValidateStreamUsage(StreamUsageEnum streamUsage, const Optional> & videoStreamId, + const Optional> & audioStreamId); + CHIP_ERROR ValidateBandwidthLimit(StreamUsageEnum streamUsage, const Optional> & videoStreamId, + const Optional> & audioStreamId); + PushAvStreamTransportStatusEnum GetTransportStatus(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..33a834724489d2 --- /dev/null +++ b/examples/all-clusters-app/all-clusters-common/src/push-av-stream-transport-delegate-impl.cpp @@ -0,0 +1,191 @@ +/* + * + * 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; + +// Global pointer to overall PushAV Stream Transport implementing the Cluster delegate. +std::unique_ptr sPushAvStramTransportInstance; +// Global pointer to PushAV Stream Transport Server SDK cluster; +std::unique_ptr sPushAvStramTransportClusterServerInstance; + +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 Structs::TransportOptionsStruct::DecodableType 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; +} + +CHIP_ERROR PushAvStreamTransportManager::ValidateBandwidthLimit(StreamUsageEnum streamUsage, + const Optional> & videoStreamId, + const Optional> & audioStreamId) +{ + // TODO: Validates the requested stream usage against the camera's resource management. + // Returning CHIP_NO_ERROR to pass through checks in the Server Implementation. + return CHIP_NO_ERROR; +} + +bool PushAvStreamTransportManager::ValidateUrl(std::string url) +{ + return true; +} + +CHIP_ERROR +PushAvStreamTransportManager::ValidateStreamUsage(StreamUsageEnum streamUsage, + const Optional> & videoStreamId, + const Optional> & audioStreamId) +{ + // TODO: Validates the requested stream usage against the camera's resource management and stream priority policies. + // Returning CHIP_NO_ERROR to pass through checks in the Server Implementation. + return CHIP_NO_ERROR; +} + +PushAvStreamTransportStatusEnum PushAvStreamTransportManager::GetTransportStatus(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, "Persistent attributes loaded"); + + return CHIP_NO_ERROR; +} + +void emberAfPushAvStreamTransportClusterInitCallback(EndpointId endpoint) +{ + VerifyOrReturn( + endpoint == 1, // this cluster is only enabled for endpoint 1. + ChipLogError(Zcl, "Push AV Stream Transport cluster delegate is not implemented for endpoint with id %d.", endpoint)); + + VerifyOrReturn(!sPushAvStramTransportInstance && !sPushAvStramTransportClusterServerInstance); + + sPushAvStramTransportInstance = std::make_unique(); + sPushAvStramTransportInstance->Init(); + + BitFlags features; + + sPushAvStramTransportClusterServerInstance = + std::make_unique(*sPushAvStramTransportInstance.get(), endpoint, features); + sPushAvStramTransportClusterServerInstance->Init(); +} + +void emberAfPushAvStreamTransportClusterShutdownCallback(EndpointId endpoint) +{ + sPushAvStramTransportClusterServerInstance = nullptr; + sPushAvStramTransportInstance = nullptr; +} diff --git a/examples/all-clusters-app/linux/BUILD.gn b/examples/all-clusters-app/linux/BUILD.gn index 57a01544f37567..1523f1efd5a616 100644 --- a/examples/all-clusters-app/linux/BUILD.gn +++ b/examples/all-clusters-app/linux/BUILD.gn @@ -57,6 +57,7 @@ source_set("chip-all-clusters-common") { "${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/power-topology-stub.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/scripts/tools/check_includes_config.py b/scripts/tools/check_includes_config.py index a83f9db2b14404..fa1593a7213219 100644 --- a/scripts/tools/check_includes_config.py +++ b/scripts/tools/check_includes_config.py @@ -142,6 +142,7 @@ 'src/app/clusters/camera-av-stream-management-server/camera-av-stream-management-server.h': {'vector'}, '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/credentials/attestation_verifier/FileAttestationTrustStore.h': {'vector'}, 'src/credentials/attestation_verifier/FileAttestationTrustStore.cpp': {'string'}, 'src/credentials/attestation_verifier/TestDACRevocationDelegateImpl.cpp': {'fstream'}, 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..a309e9b59916ff --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/BUILD.gn @@ -0,0 +1,15 @@ +# 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. +group("push-av-stream-transport-server") { +} 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..f7ada4d5188470 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/app_config_dependent_sources.cmake @@ -0,0 +1,21 @@ +# 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-server.cpp" + "${CLUSTER_DIR}/push-av-stream-transport-server.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..786feca9b86f84 --- /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 = [ + "push-av-stream-transport-server.cpp", + "push-av-stream-transport-server.h", +] diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-server.cpp b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-server.cpp new file mode 100644 index 00000000000000..ce5bac4c3e8014 --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-server.cpp @@ -0,0 +1,1092 @@ +/** + * + * 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 +#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 { +namespace PushAvStreamTransport { + +PushAvStreamTransportServer::PushAvStreamTransportServer(PushAvStreamTransportDelegate & aDelegate, EndpointId aEndpointId, + const BitFlags aFeatures) : + AttributeAccessInterface(MakeOptional(aEndpointId), PushAvStreamTransport::Id), + CommandHandlerInterface(MakeOptional(aEndpointId), PushAvStreamTransport::Id), mDelegate(aDelegate), mFeatures(aFeatures), + mSupportedFormats{ SupportedFormatStruct{ ContainerFormatEnum::kCmaf, IngestMethodsEnum::kCMAFIngest } } +{ + /* set the base class delegates endpointId */ + mDelegate.SetEndpointId(aEndpointId); +} + +PushAvStreamTransportServer::~PushAvStreamTransportServer() +{ + Shutdown(); +} + +CHIP_ERROR PushAvStreamTransportServer::Init() +{ + LoadPersistentAttributes(); + + VerifyOrReturnError(AttributeAccessInterfaceRegistry::Instance().Register(this), CHIP_ERROR_INTERNAL); + ReturnErrorOnFailure(CommandHandlerInterfaceRegistry::Instance().RegisterCommandHandler(this)); + return CHIP_NO_ERROR; +} + +void PushAvStreamTransportServer::Shutdown() +{ // Unregister command handler and attribute access interfaces + CommandHandlerInterfaceRegistry::Instance().UnregisterCommandHandler(this); + AttributeAccessInterfaceRegistry::Instance().Unregister(this); +} + +bool PushAvStreamTransportServer::HasFeature(Feature feature) const +{ + return mFeatures.Has(feature); +} + +CHIP_ERROR PushAvStreamTransportServer::ReadAndEncodeSupportedFormats(const AttributeValueEncoder::ListEncodeHelper & encoder) +{ + for (const auto & supportsFormat : mSupportedFormats) + { + ReturnErrorOnFailure(encoder.Encode(supportsFormat)); + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR PushAvStreamTransportServer::ReadAndEncodeCurrentConnections(const AttributeValueEncoder::ListEncodeHelper & encoder) +{ + for (const auto & currentConnections : mCurrentConnections) + { + ReturnErrorOnFailure(encoder.Encode(currentConnections.transportConfiguration)); + } + + return CHIP_NO_ERROR; +} + +PushAvStreamTransportServer::UpsertResultEnum PushAvStreamTransportServer::UpsertStreamTransportConnection( + const TransportConfigurationStorageWithFabricIndex & transportConfiguration) +{ + UpsertResultEnum result; + auto it = std::find_if(mCurrentConnections.begin(), mCurrentConnections.end(), + [id = transportConfiguration.transportConfiguration.connectionID](const auto & existing) { + return existing.transportConfiguration.connectionID == id; + }); + + if (it != mCurrentConnections.end()) + { + *it = transportConfiguration; + result = UpsertResultEnum::kUpdated; + } + else + { + mCurrentConnections.push_back(transportConfiguration); + result = UpsertResultEnum::kInserted; + } + + MatterReportingAttributeChangeCallback(AttributeAccessInterface::GetEndpointId().Value(), PushAvStreamTransport::Id, + PushAvStreamTransport::Attributes::CurrentConnections::Id); + + return result; +} + +void PushAvStreamTransportServer::RemoveStreamTransportConnection(const uint16_t transportConnectionId) +{ + size_t originalSize = mCurrentConnections.size(); + + // Erase-Remove idiom + mCurrentConnections.erase(std::remove_if(mCurrentConnections.begin(), mCurrentConnections.end(), + [transportConnectionId](const TransportConfigurationStorageWithFabricIndex & s) { + return s.transportConfiguration.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(AttributeAccessInterface::GetEndpointId().Value(), PushAvStreamTransport::Id, + PushAvStreamTransport::Attributes::CurrentConnections::Id); + } +} + +CHIP_ERROR PushAvStreamTransportServer::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) +{ + VerifyOrDie(aPath.mClusterId == PushAvStreamTransport::Id); + ChipLogProgress(Zcl, "Push AV Stream Transport[ep=%d]: Reading", AttributeAccessInterface::GetEndpointId().Value()); + + switch (aPath.mAttributeId) + { + case FeatureMap::Id: + ReturnErrorOnFailure(aEncoder.Encode(mFeatures)); + break; + + case SupportedFormats::Id: + ReturnErrorOnFailure(aEncoder.EncodeList( + [this](const auto & encoder) -> CHIP_ERROR { return this->ReadAndEncodeSupportedFormats(encoder); })); + break; + + case CurrentConnections::Id: + ReturnErrorOnFailure(aEncoder.EncodeList( + [this](const auto & encoder) -> CHIP_ERROR { return this->ReadAndEncodeCurrentConnections(encoder); })); + break; + } + + return CHIP_NO_ERROR; +} + +void PushAvStreamTransportServer::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(); +} + +// CommandHandlerInterface +void PushAvStreamTransportServer::InvokeCommand(HandlerContext & handlerContext) +{ + ChipLogDetail(Zcl, "PushAV: InvokeCommand"); + switch (handlerContext.mRequestPath.mCommandId) + { + case Commands::AllocatePushTransport::Id: + ChipLogDetail(Zcl, "PushAVStreamTransport: Allocating Push Transport"); + + HandleCommand( + handlerContext, + [this](HandlerContext & ctx, const auto & commandData) { HandleAllocatePushTransport(ctx, commandData); }); + + break; + + case Commands::DeallocatePushTransport::Id: + ChipLogDetail(Zcl, "PushAVStreamTransport: Deallocating Push Transport"); + + HandleCommand( + handlerContext, + [this](HandlerContext & ctx, const auto & commandData) { HandleDeallocatePushTransport(ctx, commandData); }); + + break; + + case Commands::ModifyPushTransport::Id: + ChipLogDetail(Zcl, "PushAVStreamTransport: Modifying Push Transport"); + + HandleCommand( + handlerContext, + [this](HandlerContext & ctx, const auto & commandData) { HandleModifyPushTransport(ctx, commandData); }); + + break; + + case Commands::SetTransportStatus::Id: + ChipLogDetail(Zcl, "PushAVStreamTransport: Setting Push Transport Status"); + + HandleCommand( + handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleSetTransportStatus(ctx, commandData); }); + + break; + + case Commands::ManuallyTriggerTransport::Id: + ChipLogDetail(Zcl, "PushAVStreamTransport: Manually Triggered Push Transport"); + + HandleCommand( + handlerContext, + [this](HandlerContext & ctx, const auto & commandData) { HandleManuallyTriggerTransport(ctx, commandData); }); + + break; + + case Commands::FindTransport::Id: + ChipLogDetail(Zcl, "PushAVStreamTransport: Finding Push Transport"); + + HandleCommand( + handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleFindTransport(ctx, commandData); }); + + break; + default: + // Mark unrecognized command as UnsupportedCommand + handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); + break; + } +} + +TransportConfigurationStorageWithFabricIndex * +PushAvStreamTransportServer::FindStreamTransportConnection(const uint16_t connectionID) +{ + for (auto & transportConnection : mCurrentConnections) + { + if (transportConnection.transportConfiguration.connectionID == connectionID) + return &transportConnection; + } + return nullptr; +} + +uint16_t PushAvStreamTransportServer::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 PushAvStreamTransportServer::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() == true) + { + ChipLogProgress(Zcl, "Push AV Stream Transport Deallocate timer expired. %s", "Deallocating"); + + // Remove connection from CurrentConnections + transportDeallocateContext->instance->RemoveStreamTransportConnection(connectionID); + } + else + { + ChipLogError(Zcl, "Push AV Stream Transport Deallocate timer expired. %s", "Deallocation Failed"); + } + + delete transportDeallocateContext; +} + +void PushAvStreamTransportServer::ScheduleTransportDeallocate(uint16_t connectionID, uint32_t timeoutSec) +{ + uint32_t timeoutMs = timeoutSec * MILLISECOND_TICKS_PER_SECOND; + + PushAVStreamTransportDeallocateCallbackContext * transportDeallocateContext = + new (std::nothrow) PushAVStreamTransportDeallocateCallbackContext{ this, connectionID }; + + if (transportDeallocateContext == nullptr) + { + ChipLogError(Zcl, "Failed to allocate memory for deallocate context"); + return; + } + + CHIP_ERROR err = DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Milliseconds32(timeoutMs), + PushAVStreamTransportDeallocateCallback, + static_cast(transportDeallocateContext)); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "Failed to schedule deallocate: timeout=%" PRIu32 ", status=%" CHIP_ERROR_FORMAT, timeoutSec, + err.Format()); + } +} + +void PushAvStreamTransportServer::HandleAllocatePushTransport(HandlerContext & ctx, + const Commands::AllocatePushTransport::DecodableType & commandData) +{ + Commands::AllocatePushTransportResponse::Type response; + auto & transportOptions = commandData.transportOptions; + + // Contraints check on incoming transport Options + + VerifyOrReturn(transportOptions.streamUsage != StreamUsageEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid streamUsage ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + VerifyOrReturn(transportOptions.videoStreamID.HasValue() || transportOptions.audioStreamID.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing videoStreamID and audioStreamID", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + VerifyOrReturn(transportOptions.url.size() <= 2000, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing videoStreamID and audioStreamID", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + + auto & triggerOptions = transportOptions.triggerOptions; + + VerifyOrReturn(triggerOptions.triggerType != TransportTriggerTypeEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid triggerType ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + if (triggerOptions.triggerType == TransportTriggerTypeEnum::kMotion) + { + VerifyOrReturn(triggerOptions.motionZones.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing motion zones ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + if (triggerOptions.motionZones.Value().IsNull() == false) + { + auto & motionZonesList = triggerOptions.motionZones; + auto iter = motionZonesList.Value().Value().begin(); + + while (iter.Next()) + { + auto & transportZoneOption = iter.GetValue(); + + if (mFeatures.Has(Feature::kPerZoneSensitivity)) + { + VerifyOrReturn(transportZoneOption.sensitivity.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing Zone Sensitivity ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + VerifyOrReturn(transportZoneOption.sensitivity.Value() >= 1 && transportZoneOption.sensitivity.Value() <= 10, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Zone Sensitivity Constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + } + } + } + + if (mFeatures.Has(Feature::kPerZoneSensitivity) == false) + { + VerifyOrReturn(triggerOptions.motionSensitivity.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing Motion Sensitivity ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + VerifyOrReturn( + triggerOptions.motionSensitivity.Value().Value() >= 1 && triggerOptions.motionSensitivity.Value().Value() <= 10, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Motion Sensitivity Constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + } + + VerifyOrReturn(triggerOptions.motionTimeControl.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing Motion Time Control ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + VerifyOrReturn(triggerOptions.motionTimeControl.Value().initialDuration >= 1, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Motion Time Control (InitialDuration) Constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + + VerifyOrReturn(triggerOptions.motionTimeControl.Value().maxDuration >= 1, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Motion Time Control (MaxDuration) Constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + } + + if (triggerOptions.triggerType == TransportTriggerTypeEnum::kMotion || + triggerOptions.triggerType == TransportTriggerTypeEnum::kCommand) + { + VerifyOrReturn(triggerOptions.maxPreRollLen.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing Max Pre Roll Len field ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + } + + VerifyOrReturn(transportOptions.ingestMethod != IngestMethodsEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid Ingest Method ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + // Todo: TLSEndpointID Validation + + IngestMethodsEnum ingestMethod = commandData.transportOptions.ingestMethod; + + VerifyOrReturn(transportOptions.ingestMethod != IngestMethodsEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid Ingest Method ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + ContainerOptionsStruct containerOptions = commandData.transportOptions.containerOptions; + + VerifyOrReturn(containerOptions.containerType != ContainerFormatEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid Container Format ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + if (containerOptions.containerType == ContainerFormatEnum::kCmaf) + { + VerifyOrReturn(containerOptions.CMAFContainerOptions.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing CMAF Container Options ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + if (containerOptions.CMAFContainerOptions.Value().CENCKey.HasValue()) + { + VerifyOrReturn(containerOptions.CMAFContainerOptions.Value().CENCKey.Value().size() <= kMaxCENCKeyLength, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: CMAF Container Options CENC Key constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + } + + if (mFeatures.Has(Feature::kMetadata)) + { + VerifyOrReturn(containerOptions.CMAFContainerOptions.Value().metadataEnabled.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing CMAF Container Options MetadataEnabled ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + } + + if (containerOptions.CMAFContainerOptions.Value().CENCKey.HasValue()) + { + VerifyOrReturn(containerOptions.CMAFContainerOptions.Value().CENCKeyID.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing CMAF Container Options CENC Key ID ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + VerifyOrReturn(containerOptions.CMAFContainerOptions.Value().CENCKeyID.Value().size() <= kMaxCENCKeyIDLength, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: CMAF Container Options CENC Key ID constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + } + } + + 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)", + AttributeAccessInterface::GetEndpointId().Value(), static_cast(ingestMethod), + static_cast(containerOptions.containerType)); + ctx.mCommandHandler.AddClusterSpecificFailure(ctx.mRequestPath, status); + return; + } + + 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", AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddClusterSpecificFailure(ctx.mRequestPath, status); + return; + } + + /*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", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddClusterSpecificFailure(ctx.mRequestPath, status); + return; + } + + // Validate ZoneId + + // Validate Bandwidth Requirement + CHIP_ERROR err = mDelegate.ValidateBandwidthLimit(transportOptions.streamUsage, transportOptions.videoStreamID, + transportOptions.audioStreamID); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Resource Exhausted", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ResourceExhausted); + return; + } + + // Validate the StreamUsageEnum as per resource management and stream priorities. + err = + mDelegate.ValidateStreamUsage(transportOptions.streamUsage, transportOptions.videoStreamID, transportOptions.audioStreamID); + if (err != CHIP_NO_ERROR) + { + auto status = to_underlying(StatusCodeEnum::kInvalidStream); + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid Stream", AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddClusterSpecificFailure(ctx.mRequestPath, status); + return; + } + + uint16_t connectionID = GenerateConnectionID(); + + if (connectionID == kMaxConnectionId) + { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Max Connections Exhausted", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ResourceExhausted); + return; + } + + std::shared_ptr transportOptionsPtr{ new (std::nothrow) TransportOptionsStorage(transportOptions) }; + + if (transportOptionsPtr == nullptr) + { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Memory Allocation failed for transportOptions", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ResourceExhausted); + return; + } + + TransportConfigurationStorage outTransportConfiguration(connectionID, transportOptionsPtr); + + Status status = mDelegate.AllocatePushTransport(*transportOptionsPtr, connectionID); + + if (status == Status::Success) + { + // add connection to CurrentConnections + FabricIndex peerFabricIndex = ctx.mCommandHandler.GetAccessingFabricIndex(); + + TransportConfigurationStorageWithFabricIndex transportConfiguration({ outTransportConfiguration, peerFabricIndex }); + + UpsertStreamTransportConnection(transportConfiguration); + response.transportConfiguration = outTransportConfiguration; + + // ExpiryTime Handling + if (transportOptions.expiryTime.HasValue()) + { + ScheduleTransportDeallocate(connectionID, transportOptions.expiryTime.Value()); + } + + ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); + } + else + { + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); + } +} + +void PushAvStreamTransportServer::HandleDeallocatePushTransport( + HandlerContext & ctx, const Commands::DeallocatePushTransport::DecodableType & commandData) +{ + uint16_t connectionID = commandData.connectionID; + TransportConfigurationStorageWithFabricIndex * transportConfiguration = FindStreamTransportConnection(connectionID); + if (transportConfiguration == nullptr) + { + ChipLogError(Zcl, "HandleDeallocatePushTransport[ep=%d]: ConnectionID Not Found.", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + + if (transportConfiguration->fabricIndex != ctx.mCommandHandler.GetAccessingFabricIndex()) + { + ChipLogError(Zcl, "HandleDeallocatePushTransport[ep=%d]: ConnectionID Not Found.", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + + // Call the delegate + auto delegateStatus = Protocols::InteractionModel::ClusterStatusCode(mDelegate.DeallocatePushTransport(connectionID)); + + if (delegateStatus.IsSuccess() == true) + { + // Remove connection from CurrentConnections + RemoveStreamTransportConnection(connectionID); + } + + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, delegateStatus); +} + +void PushAvStreamTransportServer::HandleModifyPushTransport(HandlerContext & ctx, + const Commands::ModifyPushTransport::DecodableType & commandData) +{ + Status status = Status::Success; + uint16_t connectionID = commandData.connectionID; + auto & transportOptions = commandData.transportOptions; + + // Contraints check on incoming transport Options + + VerifyOrReturn(transportOptions.streamUsage != StreamUsageEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid streamUsage ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + VerifyOrReturn(transportOptions.videoStreamID.HasValue() || transportOptions.audioStreamID.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing videoStreamID and audioStreamID", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + VerifyOrReturn(transportOptions.url.size() <= 2000, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing videoStreamID and audioStreamID", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + + auto & triggerOptions = transportOptions.triggerOptions; + + VerifyOrReturn(triggerOptions.triggerType != TransportTriggerTypeEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid triggerType ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + if (triggerOptions.triggerType == TransportTriggerTypeEnum::kMotion) + { + VerifyOrReturn(triggerOptions.motionZones.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing motion zones ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + if (triggerOptions.motionZones.Value().IsNull() == false) + { + auto & motionZonesList = triggerOptions.motionZones; + auto iter = motionZonesList.Value().Value().begin(); + + while (iter.Next()) + { + auto & transportZoneOption = iter.GetValue(); + + if (mFeatures.Has(Feature::kPerZoneSensitivity)) + { + VerifyOrReturn(transportZoneOption.sensitivity.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing Zone Sensitivity ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + VerifyOrReturn(transportZoneOption.sensitivity.Value() >= 1 && transportZoneOption.sensitivity.Value() <= 10, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Zone Sensitivity Constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + } + } + } + + if (mFeatures.Has(Feature::kPerZoneSensitivity) == false) + { + VerifyOrReturn(triggerOptions.motionSensitivity.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing Motion Sensitivity ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + VerifyOrReturn( + triggerOptions.motionSensitivity.Value().Value() >= 1 && triggerOptions.motionSensitivity.Value().Value() <= 10, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Motion Sensitivity Constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + } + + VerifyOrReturn(triggerOptions.motionTimeControl.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing Motion Time Control ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + VerifyOrReturn(triggerOptions.motionTimeControl.Value().initialDuration >= 1, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Motion Time Control (InitialDuration) Constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + + VerifyOrReturn(triggerOptions.motionTimeControl.Value().maxDuration >= 1, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Motion Time Control (MaxDuration) Constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + } + + if (triggerOptions.triggerType == TransportTriggerTypeEnum::kMotion || + triggerOptions.triggerType == TransportTriggerTypeEnum::kCommand) + { + VerifyOrReturn(triggerOptions.maxPreRollLen.HasValue(), { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Missing Max Pre Roll Len field ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + } + + VerifyOrReturn(transportOptions.ingestMethod != IngestMethodsEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleAllocatePushTransport[ep=%d]: Invalid Ingest Method ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + TransportConfigurationStorageWithFabricIndex * transportConfiguration = FindStreamTransportConnection(connectionID); + + if (transportConfiguration == nullptr) + { + ChipLogError(Zcl, "HandleModifyPushTransport[ep=%d]: ConnectionID Not Found.", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + + if (transportConfiguration->fabricIndex != ctx.mCommandHandler.GetAccessingFabricIndex()) + { + ChipLogError(Zcl, "HandleModifyPushTransport[ep=%d]: ConnectionID Not Found.", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + + if (mDelegate.GetTransportStatus(connectionID) == PushAvStreamTransportStatusEnum::kBusy) + { + ChipLogError(Zcl, "HandleModifyPushTransport[ep=%d]: Connection is Busy", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Busy); + return; + } + + std::shared_ptr transportOptionsPtr{ new (std::nothrow) TransportOptionsStorage(transportOptions) }; + + if (transportOptionsPtr == nullptr) + { + ChipLogError(Zcl, "HandleModifyPushTransport[ep=%d]: Memory Allocation failed for transportOptions", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ResourceExhausted); + return; + } + // Call the delegate + status = mDelegate.ModifyPushTransport(connectionID, transportOptions); + + if (status == Status::Success) + { + transportConfiguration->transportConfiguration.SetTransportOptionsPtr(transportOptionsPtr); + } + + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); +} + +void PushAvStreamTransportServer::HandleSetTransportStatus(HandlerContext & ctx, + const Commands::SetTransportStatus::DecodableType & commandData) +{ + Status status = Status::Success; + DataModel::Nullable connectionID = commandData.connectionID; + auto & transportStatus = commandData.transportStatus; + std::vector connectionIDList; + + if (connectionID.IsNull()) + { + for (auto & transportConnection : mCurrentConnections) + { + if (transportConnection.fabricIndex == ctx.mCommandHandler.GetAccessingFabricIndex()) + { + connectionIDList.push_back(transportConnection.transportConfiguration.connectionID); + } + } + } + else + { + TransportConfigurationStorageWithFabricIndex * transportConfiguration = FindStreamTransportConnection(connectionID.Value()); + if (transportConfiguration == nullptr) + { + ChipLogError(Zcl, "HandleSetTransportStatus[ep=%d]: ConnectionID Not Found.", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + if (transportConfiguration->fabricIndex != ctx.mCommandHandler.GetAccessingFabricIndex()) + { + ChipLogError(Zcl, "HandleSetTransportStatus[ep=%d]: ConnectionID Not Found.", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + 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.transportConfiguration.connectionID == connID) + { + transportConnection.transportConfiguration.transportStatus = transportStatus; + } + } + } + } + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); +} + +void PushAvStreamTransportServer::HandleManuallyTriggerTransport( + HandlerContext & ctx, const Commands::ManuallyTriggerTransport::DecodableType & commandData) +{ + Status status = Status::Success; + uint16_t connectionID = commandData.connectionID; + auto & activationReason = commandData.activationReason; + + VerifyOrReturn(activationReason != TriggerActivationReasonEnum::kUnknownEnumValue, { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Invalid Activation Reason ", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + }); + + Optional timeControl = commandData.timeControl; + + if (timeControl.HasValue()) + { + VerifyOrReturn(timeControl.Value().initialDuration >= 1, { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Motion Time Control (InitialDuration) Constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + + VerifyOrReturn(timeControl.Value().maxDuration >= 1, { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Motion Time Control (MaxDuration) Constraint Error", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); + }); + } + + TransportConfigurationStorageWithFabricIndex * transportConfiguration = FindStreamTransportConnection(connectionID); + + if (transportConfiguration == nullptr) + { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: ConnectionID Not Found.", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + + if (transportConfiguration->fabricIndex != ctx.mCommandHandler.GetAccessingFabricIndex()) + { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: ConnectionID Not Found.", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + + if (mDelegate.GetTransportStatus(connectionID) == PushAvStreamTransportStatusEnum::kBusy) + { + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Connection is Busy", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Busy); + return; + } + + if (transportConfiguration->transportConfiguration.transportStatus == TransportStatusEnum::kInactive) + { + auto clusterStatus = to_underlying(StatusCodeEnum::kInvalidTransportStatus); + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Invalid Transport status", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddClusterSpecificFailure(ctx.mRequestPath, clusterStatus); + return; + } + if (transportConfiguration->transportConfiguration.transportOptions.HasValue()) + { + if (transportConfiguration->transportConfiguration.transportOptions.Value().triggerOptions.triggerType == + TransportTriggerTypeEnum::kContinuous) + { + + auto clusterStatus = to_underlying(StatusCodeEnum::kInvalidTriggerType); + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Invalid Trigger type", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddClusterSpecificFailure(ctx.mRequestPath, clusterStatus); + return; + } + if (transportConfiguration->transportConfiguration.transportOptions.Value().triggerOptions.triggerType == + TransportTriggerTypeEnum::kCommand && + timeControl.HasValue() == false) + { + + ChipLogError(Zcl, "HandleManuallyTriggerTransport[ep=%d]: Time control field not present", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::DynamicConstraintError); + return; + } + } + + // When trigger type is motion in the allocated transport but triggering it manually + if (timeControl.HasValue() == false) + { + timeControl = transportConfiguration->transportConfiguration.transportOptions.Value().triggerOptions.motionTimeControl; + } + + // Call the delegate + status = mDelegate.ManuallyTriggerTransport(connectionID, activationReason, timeControl); + + if (status == Status::Success) + { + mDelegate.GeneratePushTransportBeginEvent(connectionID, TransportTriggerTypeEnum::kCommand, MakeOptional(activationReason)); + } + + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); +} + +void PushAvStreamTransportServer::HandleFindTransport(HandlerContext & ctx, + const Commands::FindTransport::DecodableType & commandData) +{ + Commands::FindTransportResponse::Type response; + + Optional> connectionID = commandData.connectionID; + + std::vector transportConfigurations; + + if ((connectionID.HasValue() == false) || connectionID.Value().IsNull()) + { + if (mCurrentConnections.size() == 0) + { + ChipLogError(Zcl, "HandleFindTransport[ep=%d]: ConnectionID not found", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + + for (auto & connection : mCurrentConnections) + { + if (connection.fabricIndex == ctx.mCommandHandler.GetAccessingFabricIndex()) + { + transportConfigurations.push_back(connection.transportConfiguration); + } + } + } + else + { + TransportConfigurationStorageWithFabricIndex * transportConfiguration = + FindStreamTransportConnection(connectionID.Value().Value()); + if (transportConfiguration == nullptr) + { + ChipLogError(Zcl, "HandleFindTransport[ep=%d]: ConnectionID not found", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + if (transportConfiguration->fabricIndex != ctx.mCommandHandler.GetAccessingFabricIndex()) + { + ChipLogError(Zcl, "HandleFindTransport[ep=%d]: ConnectionID Not Found.", + AttributeAccessInterface::GetEndpointId().Value()); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + return; + } + transportConfigurations.push_back(transportConfiguration->transportConfiguration); + } + + if (transportConfigurations.size() == 0) + { + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::NotFound); + } + + response.transportConfigurations = + DataModel::List(transportConfigurations.data(), transportConfigurations.size()); + + ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); +} + +Status PushAvStreamTransportDelegate::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 PushAvStreamTransportDelegate::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 PushAvStreamTransport +} // namespace Clusters +} // namespace app +} // namespace chip + +/** @brief Push AV Stream Transport Cluster Server Init + * + * Server Init + * + */ +void MatterPushAvStreamTransportPluginServerInitCallback() {} diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-server.h b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-server.h new file mode 100644 index 00000000000000..6eb4c3074e4a5a --- /dev/null +++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-server.h @@ -0,0 +1,655 @@ +/* + * + * 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 { +namespace PushAvStreamTransport { + +static constexpr size_t kMaxUrlLength = 2000u; +static constexpr size_t kMaxCENCKeyLength = 16u; +static constexpr size_t kMaxCENCKeyIDLength = 16u; + +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; + +enum class PushAvStreamTransportStatusEnum : uint8_t +{ + kBusy = 0x00, + kIdle = 0x01, + kUnknown = 0x02 +}; + +struct TransportTriggerOptionsStorage : public TransportTriggerOptionsStruct +{ + TransportTriggerOptionsStorage(){}; + + TransportTriggerOptionsStorage(const TransportTriggerOptionsStorage & aTransportTriggerOptionsStorage) + { + *this = aTransportTriggerOptionsStorage; + } + + TransportTriggerOptionsStorage & operator=(const TransportTriggerOptionsStorage & aTransportTriggerOptionsStorage) + { + triggerType = aTransportTriggerOptionsStorage.triggerType; + + mTransportZoneOptions = aTransportTriggerOptionsStorage.mTransportZoneOptions; + + // Rebind motionZones to point to the copied vector if it was set + if (aTransportTriggerOptionsStorage.motionZones.HasValue() && !aTransportTriggerOptionsStorage.motionZones.Value().IsNull()) + { + motionZones.SetValue(DataModel::Nullable>( + Span(mTransportZoneOptions.data(), mTransportZoneOptions.size()))); + } + else + { + motionZones.Missing(); + } + + motionSensitivity = aTransportTriggerOptionsStorage.motionSensitivity; + motionTimeControl = aTransportTriggerOptionsStorage.motionTimeControl; + maxPreRollLen = aTransportTriggerOptionsStorage.maxPreRollLen; + + return *this; + } + + TransportTriggerOptionsStorage(Structs::TransportTriggerOptionsStruct::DecodableType triggerOptions) + { + triggerType = triggerOptions.triggerType; + + if (triggerOptions.triggerType == TransportTriggerTypeEnum::kMotion && triggerOptions.motionZones.HasValue()) + { + if (triggerOptions.motionZones.Value().IsNull() == false) + { + auto & motionZonesList = triggerOptions.motionZones; + auto iter = motionZonesList.Value().Value().begin(); + + while (iter.Next()) + { + auto & transportZoneOption = iter.GetValue(); + mTransportZoneOptions.push_back(transportZoneOption); + } + + motionZones.SetValue(DataModel::Nullable>( + Span(mTransportZoneOptions.data(), mTransportZoneOptions.size()))); + } + } + else + { + motionZones.Missing(); + } + + motionSensitivity = triggerOptions.motionSensitivity; + motionTimeControl = triggerOptions.motionTimeControl; + maxPreRollLen = triggerOptions.maxPreRollLen; + } + +private: + std::vector mTransportZoneOptions; +}; + +struct CMAFContainerOptionsStorage : public CMAFContainerOptionsStruct +{ + CMAFContainerOptionsStorage(){}; + + CMAFContainerOptionsStorage(const CMAFContainerOptionsStorage & aCMAFContainerOptionsStorage) + { + *this = aCMAFContainerOptionsStorage; + } + + CMAFContainerOptionsStorage & operator=(const CMAFContainerOptionsStorage & aCMAFContainerOptionsStorage) + { + chunkDuration = aCMAFContainerOptionsStorage.chunkDuration; + + std::memcpy(mCENCKeyBuffer, aCMAFContainerOptionsStorage.mCENCKeyBuffer, sizeof(mCENCKeyBuffer)); + + std::memcpy(mCENCKeyIDBuffer, aCMAFContainerOptionsStorage.mCENCKeyIDBuffer, sizeof(mCENCKeyIDBuffer)); + + if (aCMAFContainerOptionsStorage.CENCKey.HasValue()) + { + CENCKey.SetValue(ByteSpan(mCENCKeyBuffer, aCMAFContainerOptionsStorage.CENCKey.Value().size())); + } + else + { + CENCKey.Missing(); + } + + metadataEnabled = aCMAFContainerOptionsStorage.metadataEnabled; + + if (aCMAFContainerOptionsStorage.CENCKeyID.HasValue()) + { + CENCKeyID.SetValue(ByteSpan(mCENCKeyIDBuffer, aCMAFContainerOptionsStorage.CENCKeyID.Value().size())); + } + else + { + CENCKeyID.Missing(); + } + + return *this; + } + + CMAFContainerOptionsStorage(Optional CMAFContainerOptions) + { + if (CMAFContainerOptions.HasValue() == true) + { + chunkDuration = CMAFContainerOptions.Value().chunkDuration; + + if (CMAFContainerOptions.Value().CENCKey.HasValue()) + { + MutableByteSpan CENCKeyBuffer(mCENCKeyBuffer); + CopySpanToMutableSpan(CMAFContainerOptions.Value().CENCKey.Value(), CENCKeyBuffer); + CENCKey.SetValue(CENCKeyBuffer); + } + else + { + CENCKey.Missing(); + } + + metadataEnabled = CMAFContainerOptions.Value().metadataEnabled; + + if (CMAFContainerOptions.Value().CENCKey.HasValue()) + { + MutableByteSpan CENCKeyIDBuffer(mCENCKeyIDBuffer); + CopySpanToMutableSpan(CMAFContainerOptions.Value().CENCKeyID.Value(), CENCKeyIDBuffer); + CENCKeyID.SetValue(CENCKeyIDBuffer); + } + else + { + CENCKeyID.Missing(); + } + } + } + +private: + uint8_t mCENCKeyBuffer[kMaxCENCKeyLength]; + uint8_t mCENCKeyIDBuffer[kMaxCENCKeyIDLength]; +}; + +struct ContainerOptionsStorage : public ContainerOptionsStruct +{ + ContainerOptionsStorage(){}; + + ContainerOptionsStorage(const ContainerOptionsStorage & aContainerOptionsStorage) { *this = aContainerOptionsStorage; } + + ContainerOptionsStorage & operator=(const ContainerOptionsStorage & aContainerOptionsStorage) + { + containerType = aContainerOptionsStorage.containerType; + + mCMAFContainerStorage = aContainerOptionsStorage.mCMAFContainerStorage; + + CMAFContainerOptions.SetValue(mCMAFContainerStorage); + + return *this; + } + + ContainerOptionsStorage(Structs::ContainerOptionsStruct::DecodableType containerOptions) + { + containerType = containerOptions.containerType; + + if (containerType == ContainerFormatEnum::kCmaf) + { + mCMAFContainerStorage = CMAFContainerOptionsStorage(containerOptions.CMAFContainerOptions); + CMAFContainerOptions.SetValue(mCMAFContainerStorage); + } + else + { + CMAFContainerOptions.Missing(); + } + } + +private: + CMAFContainerOptionsStorage mCMAFContainerStorage; +}; + +struct TransportOptionsStorage : public TransportOptionsStruct +{ + TransportOptionsStorage(){}; + + TransportOptionsStorage(const TransportOptionsStorage & aTransportOptionsStorage) { *this = aTransportOptionsStorage; } + + TransportOptionsStorage & operator=(const TransportOptionsStorage & aTransportOptionsStorage) + { + 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(Structs::TransportOptionsStruct::DecodableType transportOptions) + { + streamUsage = transportOptions.streamUsage; + videoStreamID = transportOptions.videoStreamID; + audioStreamID = transportOptions.audioStreamID; + endpointID = transportOptions.endpointID; + + MutableCharSpan urlBuffer(mUrlBuffer); + CopyCharSpanToMutableCharSpanWithTruncation(transportOptions.url, urlBuffer); + url = urlBuffer; + + mTriggerOptionsStorage = TransportTriggerOptionsStorage(transportOptions.triggerOptions); + triggerOptions = mTriggerOptionsStorage; + + ingestMethod = transportOptions.ingestMethod; + + mContainerOptionsStorage = ContainerOptionsStorage(transportOptions.containerOptions); + containerOptions = mContainerOptionsStorage; + + expiryTime = transportOptions.expiryTime; + } + +private: + char mUrlBuffer[kMaxUrlLength]; + TransportTriggerOptionsStorage mTriggerOptionsStorage; + ContainerOptionsStorage mContainerOptionsStorage; +}; + +struct TransportConfigurationStorage : public TransportConfigurationStruct +{ + TransportConfigurationStorage() {} + + TransportConfigurationStorage(const TransportConfigurationStorage & aTransportConfigurationStorage) + { + *this = aTransportConfigurationStorage; + } + + TransportConfigurationStorage & operator=(const TransportConfigurationStorage & aTransportConfigurationStorage) + { + connectionID = aTransportConfigurationStorage.connectionID; + transportStatus = aTransportConfigurationStorage.transportStatus; + + mTransportOptionsPtr = aTransportConfigurationStorage.mTransportOptionsPtr; + + if (mTransportOptionsPtr) + { + transportOptions.SetValue(*mTransportOptionsPtr); + } + else + { + transportOptions.Missing(); + } + + 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.Missing(); + } + } + std::shared_ptr GetTransportOptionsPtr() const { return mTransportOptionsPtr; } + void SetTransportOptionsPtr(std::shared_ptr aTransportOptionsPtr) + { + mTransportOptionsPtr = aTransportOptionsPtr; + if (mTransportOptionsPtr) + { + transportOptions.SetValue(*mTransportOptionsPtr); + } + else + { + transportOptions.Missing(); + } + } + +private: + std::shared_ptr mTransportOptionsPtr; +}; + +struct TransportConfigurationStorageWithFabricIndex +{ + TransportConfigurationStorage transportConfiguration; + FabricIndex fabricIndex; +}; + +/** @brief + * Defines interfaces for implementing application-specific logic for various aspects of the PushAvStreamTransport Delegate. + * Specifically, it defines interfaces for the command handling and loading of the allocated streams. + */ +class PushAvStreamTransportDelegate +{ +public: + PushAvStreamTransportDelegate() = default; + + virtual ~PushAvStreamTransportDelegate() = default; + + void SetEndpointId(EndpointId aEndpoint) { mEndpointId = aEndpoint; } + + /** + * @brief Handle Command Delegate for stream transport allocation with the provided transport configuration option. + * + * @param transportOptions[in] represent the configuration options of the transport to be allocated + * + * @param connectionID[in] Indicates the connectionID to allocate. + * + * @return Success if the allocation is successful and a PushTransportConnectionID was produced; otherwise, the command SHALL + * be rejected with Failure. + * + * The buffers storing URL, Trigger Options, Motion Zones, Container Options is owned by the PushAVStreamTransport Server. + * The buffers are allocated on success of AllocatePushTransport and deallocated on success of DeallocatePushTransport + * command. The delegate is expected to process the transport following transport options: URL : Validate the URL + * StreamUsage,VideoStreamID,AudioStreamID for selection of Stream. + * Allocate the transport and map it to the connectionID. + * On Success TransportConfigurationStruct is sent as response by the server. + */ + virtual Protocols::InteractionModel::Status AllocatePushTransport(const TransportOptionsStruct & transportOptions, + const uint16_t connectionID) = 0; + /** + * @brief Handle Command Delegate for Stream transport deallocation for the + * provided connectionID. + * + * @param connectionID[in] Indicates the connectionID to deallocate. + * + * @return Success if the transport deallocation is successful; otherwise, the delegate is expected to return status code BUSY + * if the transport is currently uploading. + * + */ + virtual Protocols::InteractionModel::Status DeallocatePushTransport(const uint16_t connectionID) = 0; + /** + * @brief Handle Command Delegate for Stream transport modification. + * + * @param connectionID [in] Indicates the connectionID of the stream transport to modify. + * + * @param transportOptions [out] represents the Transport Options to modify. + * + * The buffers storing URL, Trigger Options, Motion Zones, Container Options is owned by the PushAVStreamTransport Server. + * The allocated buffers are cleared and reassigned on success of ModifyPushTransport and deallocated on success of + * DeallocatePushTransport. + * + * @return Success if the stream transport modification is successful; otherwise, the command SHALL be rejected with Failure. + */ + virtual Protocols::InteractionModel::Status + ModifyPushTransport(const uint16_t connectionID, const Structs::TransportOptionsStruct::DecodableType transportOptions) = 0; + + /** + * @brief Handle Command Delegate for Stream transport modification. + * + * @param connectionIDList [in] represent the list of connectionIDs for which new transport status to apply. + * @param transportStatus [in] represents the updated status of the connection(s). + * + * @return Success if the stream transport status is successfully set; otherwise, the command SHALL be rejected with Failure. + */ + virtual Protocols::InteractionModel::Status SetTransportStatus(const std::vector connectionIDList, + TransportStatusEnum transportStatus) = 0; + /** + * @brief Handle Command Delegate to request the Node to manually start the specified push transport. + * + * @param connectionID [in] Indicates the connectionID of the stream transport to set trigger for. + * + * @param activationReason [in] Provide information as to why the transport was started or stopped. + * + * @param timeControl [in] Configuration to control the life cycle of a triggered transport. + * + * The delegate is expected to begin transmission using the timeControl values. + * + * Emitting of PushTransportBegin event is handled by the server when delegate returns success. + * + * The delegate should emit PushTransportEnd Event using GeneratePushTransportEndEvent() API when timeControl values when +transmission ends. + * @return Success if the stream transport trigger is successful; otherwise, the command SHALL be rejected with Failure. + */ + virtual Protocols::InteractionModel::Status + ManuallyTriggerTransport(const uint16_t connectionID, TriggerActivationReasonEnum activationReason, + const Optional & timeControl) = 0; + + /** + * @brief Validates the url + * @param[in] url The url to validate + * + * @return boolean value true if the url is valid else false. + */ + virtual bool ValidateUrl(std::string url) = 0; + + /** + * @brief Validates the bandwidth requirement against the camera's resource management + * + * The implementation SHALL ensure: + * - The requested stream usage (streamUsage) is allowed given the current allocation of + * camera resources (e.g. CPU, memory, network bandwidth). + * + * @param[in] streamUsage The desired usage type for the stream (e.g. live view, recording, etc.). + * @param[in] videoStreamId Optional identifier for the requested video stream. + * @param[in] audioStreamId Optional identifier for the requested audio stream. + * + * @return CHIP_ERROR CHIP_NO_ERROR if the stream usage is valid; an appropriate error code otherwise. + */ + + virtual CHIP_ERROR ValidateBandwidthLimit(StreamUsageEnum streamUsage, + const Optional> & videoStreamId, + const Optional> & audioStreamId) = 0; + + /** + * @brief Validates the requested stream usage against the camera's resource management + * and stream priority policies. + * + * The implementation SHALL ensure: + * - The requested stream usage (streamUsage) is allowed given the current allocation of + * camera resources (e.g. CPU, memory, network bandwidth) and the prioritized stream list. + * + * @param[in] streamUsage The desired usage type for the stream (e.g. live view, recording, etc.). + * @param[in] videoStreamId Optional identifier for the requested video stream. + * @param[in] audioStreamId Optional identifier for the requested audio stream. + * + * @return CHIP_ERROR CHIP_NO_ERROR if the stream usage is valid; an appropriate error code otherwise. + */ + virtual CHIP_ERROR ValidateStreamUsage(StreamUsageEnum streamUsage, + const Optional> & videoStreamId, + const Optional> & audioStreamId) = 0; + + /** + * @brief Gets the status of the transport + * + * @param[in] connectionID Indicates the connectionID of the stream transport to check status + * + * @return busy if transport is uploading else idle + */ + virtual PushAvStreamTransportStatusEnum GetTransportStatus(const uint16_t connectionID) = 0; + + /** + * @brief Delegate callback for notifying change in an attribute. + * + */ + virtual void OnAttributeChanged(AttributeId attributeId) = 0; + + /** + * @brief + * Delegate functions to load the allocated transport connections. + * The delegate application is responsible for creating and persisting these connections ( based on the Allocation commands ). + * These Load APIs would be used to load the pre-allocated transport connections context information into the cluster server + * list, at initialization. Once loaded, the cluster server would be serving Reads on these attributes. The list is updatable + * via the Add/Remove functions for the respective transport connections. + * + * @note + * + * The required buffers are managed by TransportConfigurationStorage, the delegate function is expected to populate the vector + * correctly. + */ + virtual CHIP_ERROR LoadCurrentConnections(std::vector & currentConnections) = 0; + + /** + * @brief Callback into the delegate once persistent attributes managed by + * the Cluster have been loaded from Storage. + */ + virtual CHIP_ERROR PersistentAttributesLoadedCallback() = 0; + + // Send Push AV Stream Transport events + Protocols::InteractionModel::Status + GeneratePushTransportBeginEvent(const uint16_t connectionID, const TransportTriggerTypeEnum triggerType, + const Optional activationReason); + Protocols::InteractionModel::Status GeneratePushTransportEndEvent(const uint16_t connectionID, + const TransportTriggerTypeEnum triggerType, + const Optional activationReason); + +protected: + EndpointId mEndpointId = 0; +}; + +class PushAvStreamTransportServer : public AttributeAccessInterface, public CommandHandlerInterface +{ +public: + /** + * Creates a Push AV Stream Transport server instance. The Init() function needs to be called for this instance to be registered + * and called by the interaction model at the appropriate times. + * @param aDelegate A reference to the delegate to be used by this server. + * @param aEndpointId The endpoint on which this cluster exists. This must match the zap configuration. + * @param aFeatures The bitflags value that identifies which features are supported by this instance. + * Note: the caller must ensure that the delegate lives throughout the instance's lifetime. + */ + PushAvStreamTransportServer(PushAvStreamTransportDelegate & delegate, EndpointId endpointId, const BitFlags aFeatures); + + ~PushAvStreamTransportServer() override; + + /** + * @brief Initialise the Push AV Stream Transport server instance. + * This function must be called after defining an PushAvStreamTransportServer class object. + * @return Returns an error if the given endpoint and cluster ID have not been enabled in zap or if the + * CommandHandler or AttributeHandler registration fails, else returns CHIP_NO_ERROR. + * This method also checks if the feature setting is valid, if invalid it will return CHIP_ERROR_INVALID_ARGUMENT. + */ + CHIP_ERROR Init(); + + /** + * @brief + * Unregisters the command handler and attribute interface, releasing resources. + */ + void Shutdown(); + + bool HasFeature(Feature feature) const; + + static void PushAVStreamTransportDeallocateCallback(chip::System::Layer *, void * callbackContext); + +private: + enum class UpsertResultEnum : uint8_t + { + kInserted = 0x00, + kUpdated = 0x01, + }; + + struct PushAVStreamTransportDeallocateCallbackContext + { + PushAvStreamTransportServer * instance; + uint16_t connectionID; + }; + + PushAvStreamTransportDelegate & mDelegate; + + const BitFlags mFeatures; + + // Attributes + std::vector mSupportedFormats; + /*Moved from TransportConfigurationStruct to TransportConfigurationStructWithFabricIndex + * to perform fabric index checks + */ + std::vector mCurrentConnections; + + /** + * IM-level implementation of read + * @return appropriately mapped CHIP_ERROR if applicable + */ + CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override; + + /** + * Helper function that loads all the persistent attributes from the KVS. + */ + void LoadPersistentAttributes(); + + // Helpers to read list items via delegate APIs + CHIP_ERROR ReadAndEncodeCurrentConnections(const AttributeValueEncoder::ListEncodeHelper & encoder); + CHIP_ERROR ReadAndEncodeSupportedFormats(const AttributeValueEncoder::ListEncodeHelper & encoder); + + // Helper functions + uint16_t GenerateConnectionID(); + + TransportConfigurationStorageWithFabricIndex * FindStreamTransportConnection(const uint16_t connectionID); + + // Add/Remove Management functions for transport + UpsertResultEnum UpsertStreamTransportConnection(const TransportConfigurationStorageWithFabricIndex & transportConfiguration); + + void RemoveStreamTransportConnection(const uint16_t connectionID); + + /** + * @brief Schedule deallocate with a given timeout + * + * @param endpointId endpoint where DoorLockServer is running + * @param timeoutSec timeout in seconds + */ + void ScheduleTransportDeallocate(chip::EndpointId endpointId, uint32_t timeoutSec); + + /** + * @brief Inherited from CommandHandlerInterface + */ + void InvokeCommand(HandlerContext & ctx) override; + + void HandleAllocatePushTransport(HandlerContext & ctx, const Commands::AllocatePushTransport::DecodableType & req); + + void HandleDeallocatePushTransport(HandlerContext & ctx, const Commands::DeallocatePushTransport::DecodableType & req); + + void HandleModifyPushTransport(HandlerContext & ctx, const Commands::ModifyPushTransport::DecodableType & req); + + void HandleSetTransportStatus(HandlerContext & ctx, const Commands::SetTransportStatus::DecodableType & req); + + void HandleManuallyTriggerTransport(HandlerContext & ctx, const Commands::ManuallyTriggerTransport::DecodableType & req); + + void HandleFindTransport(HandlerContext & ctx, const Commands::FindTransport::DecodableType & req); +}; + +} // 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 999fc8731b5695..60550c6daf7d2b 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 8b715ece9a9117..286ef4c231f6bf 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 aca139a6d72a2e..da6b573414ee50 100644 --- a/zzz_generated/app-common/app-common/zap-generated/callback.h +++ b/zzz_generated/app-common/app-common/zap-generated/callback.h @@ -7884,42 +7884,6 @@ bool emberAfZoneManagementClusterGetTwoDCartesianZoneCallback( bool emberAfZoneManagementClusterRemoveZoneCallback( chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, const chip::app::Clusters::ZoneManagement::Commands::RemoveZone::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) */