| /* | |
| * Copyright (c) 2026 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 <pw_unit_test/framework.h> | |
| #include <app/clusters/av-analysis-server/DefaultAvAnalysisCameraClient.h> | |
| #include <clusters/CameraAvStreamManagement/Commands.h> | |
| #include <clusters/CameraAvStreamManagement/Ids.h> | |
| #include <clusters/Descriptor/Attributes.h> | |
| #include <clusters/Descriptor/Ids.h> | |
| #include <lib/core/TLV.h> | |
| namespace { | |
| using namespace chip; | |
| using namespace chip::app; | |
| using namespace chip::app::Clusters; | |
| using namespace Protocols::InteractionModel; | |
| constexpr EndpointId kCameraEndpoint = 1; | |
| const ScopedNodeId kCameraNode(0x1234, 1); | |
| CommandSender * const kTestSender = reinterpret_cast<CommandSender *>(0xC0FFEE); | |
| // Intercepts the network boundary: records session requests instead of establishing CASE sessions. | |
| // The camera's behavior is simulated by invoking the public CommandSender::Callback methods. | |
| class InterceptingCameraClient : public DefaultAvAnalysisCameraClient | |
| { | |
| public: | |
| int mConnectRequests = 0; | |
| ScopedNodeId mLastPeer; | |
| // Pretends the request reached the camera through aSender. The pointer is only ever compared, | |
| // never dereferenced, so tests can hand in a distinct non-null value per interaction. | |
| void SimulateCommandSent(CommandSender * aSender) | |
| { | |
| CurrentRequest().SetInvokedSender(aSender); | |
| CurrentRequest().Advance(Request::Phase::kInvoking); | |
| } | |
| protected: | |
| void EstablishSession(const ScopedNodeId & aCameraNode) override | |
| { | |
| mConnectRequests++; | |
| mLastPeer = aCameraNode; | |
| } | |
| }; | |
| class RecordingCallback : public AvAnalysisCameraClient::Callback | |
| { | |
| public: | |
| int mAllocatedCount = 0; | |
| int mDeallocatedCount = 0; | |
| Status mLastStatus = Status::Success; | |
| uint16_t mLastStreamId = 0; | |
| void OnVideoStreamAllocated(Status aStatus, uint16_t aVideoStreamId) override | |
| { | |
| mAllocatedCount++; | |
| mLastStatus = aStatus; | |
| mLastStreamId = aVideoStreamId; | |
| } | |
| void OnVideoStreamDeallocated(Status aStatus, uint16_t aVideoStreamId) override | |
| { | |
| mDeallocatedCount++; | |
| mLastStatus = aStatus; | |
| mLastStreamId = aVideoStreamId; | |
| } | |
| }; | |
| // Positions aReader on an anonymous TLV element encoded by aEncode | |
| // True once aReader is positioned on the encoded value. ASSERT_* returns from the function it | |
| // appears in, so this reports instead: a caller must not hand on a reader it never positioned. | |
| template <typename EncodeFn> | |
| [[nodiscard]] bool EncodeTlv(uint8_t * aBuffer, size_t aBufferSize, TLV::TLVReader & aReader, EncodeFn aEncode) | |
| { | |
| TLV::TLVWriter writer; | |
| writer.Init(aBuffer, aBufferSize); | |
| VerifyOrReturnValue(aEncode(writer) == CHIP_NO_ERROR, false); | |
| aReader.Init(aBuffer, writer.GetLengthWritten()); | |
| return aReader.Next() == CHIP_NO_ERROR; | |
| } | |
| // Encodes a Descriptor ServerList value and positions aReader on it | |
| [[nodiscard]] bool EncodeServerList(Span<const ClusterId> aClusters, MutableByteSpan aBuffer, TLV::TLVReader & aReader) | |
| { | |
| return EncodeTlv(aBuffer.data(), aBuffer.size(), aReader, [aClusters](TLV::TLVWriter & w) { | |
| TLV::TLVType outer; | |
| ReturnErrorOnFailure(w.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, outer)); | |
| for (ClusterId cluster : aClusters) | |
| { | |
| ReturnErrorOnFailure(w.Put(TLV::AnonymousTag(), cluster)); | |
| } | |
| return w.EndContainer(outer); | |
| }); | |
| } | |
| // Exposes the protected request builder and profile for direct testing | |
| class ProfileTestClient : public DefaultAvAnalysisCameraClient | |
| { | |
| public: | |
| using DefaultAvAnalysisCameraClient::BuildAllocateRequest; | |
| using DefaultAvAnalysisCameraClient::CameraProfile; | |
| using DefaultAvAnalysisCameraClient::CurrentProfile; | |
| using DefaultAvAnalysisCameraClient::CurrentRequest; | |
| using DefaultAvAnalysisCameraClient::HandleCapabilityReport; | |
| using DefaultAvAnalysisCameraClient::HandleServerListReport; | |
| using DefaultAvAnalysisCameraClient::NormalizeProfile; | |
| using DefaultAvAnalysisCameraClient::OnProfileDiscoveryComplete; | |
| using DefaultAvAnalysisCameraClient::Request; | |
| protected: | |
| // Discovery does not run on its own; the test drives OnProfileDiscoveryComplete explicitly | |
| void StartProfileDiscovery() override {} | |
| void EstablishSession(const ScopedNodeId & aCameraNode) override {} | |
| }; | |
| // Encodes a TLV array of values, the shape every list-valued capability attribute uses | |
| template <typename T> | |
| CHIP_ERROR EncodeArray(TLV::TLVWriter & aWriter, Span<const T> aValues) | |
| { | |
| TLV::TLVType outer; | |
| ReturnErrorOnFailure(aWriter.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, outer)); | |
| for (const T & value : aValues) | |
| { | |
| ReturnErrorOnFailure(DataModel::Encode(aWriter, TLV::AnonymousTag(), value)); | |
| } | |
| return aWriter.EndContainer(outer); | |
| } | |
| // Feeds one AVSM capability report into the client, as the capabilities read would. | |
| template <typename EncodeFn> | |
| void FeedCapability(ProfileTestClient & aClient, AttributeId aAttributeId, EncodeFn aEncode) | |
| { | |
| uint8_t buffer[192]; | |
| TLV::TLVReader reader; | |
| const bool encoded = EncodeTlv(buffer, sizeof(buffer), reader, aEncode); | |
| EXPECT_TRUE(encoded); | |
| if (encoded) | |
| { | |
| aClient.HandleCapabilityReport(ConcreteDataAttributePath(kInvalidEndpointId, CameraAvStreamManagement::Id, aAttributeId), | |
| reader); | |
| } | |
| } | |
| class ReentrantCallback : public AvAnalysisCameraClient::Callback | |
| { | |
| public: | |
| InterceptingCameraClient * mClient = nullptr; | |
| bool mStartAnother = true; | |
| int mAllocatedCount = 0; | |
| int mDeallocatedCount = 0; | |
| Status mLastStatus = Status::Success; | |
| void OnVideoStreamAllocated(Status aStatus, uint16_t aVideoStreamId) override | |
| { | |
| mAllocatedCount++; | |
| mLastStatus = aStatus; | |
| if (mStartAnother) | |
| { | |
| mStartAnother = false; | |
| EXPECT_EQ(mClient->RequestVideoStreamDeallocation(kCameraNode, 9, *this), CHIP_NO_ERROR); | |
| } | |
| } | |
| void OnVideoStreamDeallocated(Status aStatus, uint16_t aVideoStreamId) override | |
| { | |
| mDeallocatedCount++; | |
| mLastStatus = aStatus; | |
| } | |
| }; | |
| struct TestDefaultAvAnalysisCameraClient : public ::testing::Test | |
| { | |
| static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } | |
| static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } | |
| void SetUp() override { ASSERT_EQ(mClient.Init(&mCASESessionManager), CHIP_NO_ERROR); } | |
| // Never used for real sessions: InterceptingCameraClient overrides EstablishSession. | |
| CASESessionManager mCASESessionManager; | |
| InterceptingCameraClient mClient; | |
| RecordingCallback mCallback; | |
| }; | |
| TEST_F(TestDefaultAvAnalysisCameraClient, InitArgumentValidation) | |
| { | |
| DefaultAvAnalysisCameraClient client; | |
| EXPECT_EQ(client.Init(nullptr), CHIP_ERROR_INVALID_ARGUMENT); | |
| EXPECT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, RequestStartsSessionEstablishment) | |
| { | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| EXPECT_EQ(mClient.mConnectRequests, 1); | |
| EXPECT_EQ(mClient.mLastPeer, kCameraNode); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 0); // Nothing completed yet | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, SecondRequestWhilePendingIsBusy) | |
| { | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| EXPECT_EQ(mClient.RequestVideoStreamDeallocation(kCameraNode, 5, mCallback), CHIP_ERROR_BUSY); | |
| EXPECT_EQ(mClient.mConnectRequests, 1); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, AllocationSuccessDecodesVideoStreamId) | |
| { | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| mClient.SimulateCommandSent(kTestSender); | |
| // Craft the camera's VideoStreamAllocateResponse{VideoStreamID=7} payload | |
| uint8_t buffer[64]; | |
| TLV::TLVWriter writer; | |
| writer.Init(buffer); | |
| TLV::TLVType containerType; | |
| ASSERT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, containerType), CHIP_NO_ERROR); | |
| ASSERT_EQ(writer.Put(TLV::ContextTag(0), static_cast<uint16_t>(7)), CHIP_NO_ERROR); | |
| ASSERT_EQ(writer.EndContainer(containerType), CHIP_NO_ERROR); | |
| TLV::TLVReader reader; | |
| reader.Init(buffer, writer.GetLengthWritten()); | |
| ASSERT_EQ(reader.Next(), CHIP_NO_ERROR); | |
| ConcreteCommandPath responsePath(kCameraEndpoint, CameraAvStreamManagement::Id, | |
| CameraAvStreamManagement::Commands::VideoStreamAllocateResponse::Id); | |
| mClient.OnResponse(kTestSender, responsePath, StatusIB(), &reader); | |
| mClient.OnDone(kTestSender); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::Success); | |
| EXPECT_EQ(mCallback.mLastStreamId, 7); | |
| // Completed: the client accepts a new request again | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, AllocationSuccessWithoutPayloadIsFailure) | |
| { | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| mClient.SimulateCommandSent(kTestSender); | |
| ConcreteCommandPath responsePath(kCameraEndpoint, CameraAvStreamManagement::Id, | |
| CameraAvStreamManagement::Commands::VideoStreamAllocateResponse::Id); | |
| mClient.OnResponse(kTestSender, responsePath, StatusIB(), nullptr); | |
| mClient.OnDone(kTestSender); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::Failure); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, CameraErrorStatusIsPropagated) | |
| { | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| mClient.SimulateCommandSent(kTestSender); | |
| // Spec 11.9.8: the camera's status code (e.g. RESOURCE_EXHAUSTED) is propagated verbatim | |
| mClient.OnError(kTestSender, StatusIB(Status::ResourceExhausted).ToChipError()); | |
| mClient.OnDone(kTestSender); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::ResourceExhausted); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, DeallocationSuccessReportsStreamId) | |
| { | |
| EXPECT_EQ(mClient.RequestVideoStreamDeallocation(kCameraNode, 9, mCallback), CHIP_NO_ERROR); | |
| mClient.SimulateCommandSent(kTestSender); | |
| ConcreteCommandPath responsePath(kCameraEndpoint, CameraAvStreamManagement::Id, | |
| CameraAvStreamManagement::Commands::VideoStreamDeallocate::Id); | |
| mClient.OnResponse(kTestSender, responsePath, StatusIB(), nullptr); | |
| mClient.OnDone(kTestSender); | |
| EXPECT_EQ(mCallback.mDeallocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::Success); | |
| EXPECT_EQ(mCallback.mLastStreamId, 9); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, DeallocationResponseOnAnUnexpectedPathIsAFailure) | |
| { | |
| EXPECT_EQ(mClient.RequestVideoStreamDeallocation(kCameraNode, 9, mCallback), CHIP_NO_ERROR); | |
| mClient.SimulateCommandSent(kTestSender); | |
| // Success echoed on some other command's path is not the camera's answer to our deallocation | |
| ConcreteCommandPath foreignPath(kCameraEndpoint, CameraAvStreamManagement::Id, | |
| CameraAvStreamManagement::Commands::VideoStreamAllocateResponse::Id); | |
| mClient.OnResponse(kTestSender, foreignPath, StatusIB(), nullptr); | |
| EXPECT_EQ(mCallback.mDeallocatedCount, 0); // Ignored, not taken as the outcome | |
| // OnDone concludes the interaction: exactly one completion, and it is a failure | |
| mClient.OnDone(kTestSender); | |
| EXPECT_EQ(mCallback.mDeallocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::Failure); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, CompletionIsDeliveredExactlyOnce) | |
| { | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| mClient.SimulateCommandSent(kTestSender); | |
| mClient.OnError(kTestSender, StatusIB(Status::Failure).ToChipError()); | |
| mClient.OnDone(kTestSender); // OnDone after OnError must not produce a second callback | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, AllocateRequestFollowsProfileBounds) | |
| { | |
| ProfileTestClient::CameraProfile profile; | |
| profile.avsmEndpoint = 5; | |
| profile.hasWatermark = true; | |
| profile.hasOSD = false; | |
| profile.minFrameRate = 10; | |
| profile.maxFrameRate = 24; | |
| profile.minWidth = 320; | |
| profile.minHeight = 240; | |
| profile.maxWidth = 1280; | |
| profile.maxHeight = 720; | |
| profile.minBitRateBps = 100000; | |
| profile.maxBitRateBps = 1500000; | |
| auto request = ProfileTestClient::BuildAllocateRequest(profile); | |
| EXPECT_EQ(request.streamUsage, Globals::StreamUsageEnum::kAnalysis); | |
| EXPECT_EQ(request.minFrameRate, 10); | |
| EXPECT_EQ(request.maxFrameRate, 24); | |
| EXPECT_EQ(request.minResolution.width, 320); | |
| EXPECT_EQ(request.maxResolution.width, 1280); | |
| EXPECT_EQ(request.maxResolution.height, 720); | |
| EXPECT_EQ(request.minBitRate, 100000u); | |
| EXPECT_EQ(request.maxBitRate, 1500000u); | |
| ASSERT_TRUE(request.watermarkEnabled.HasValue()); | |
| EXPECT_FALSE(request.watermarkEnabled.Value()); | |
| EXPECT_FALSE(request.OSDEnabled.HasValue()); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, DefaultProfileHasSaneBoundsAndNoCameraSpecifics) | |
| { | |
| ProfileTestClient::CameraProfile profile; | |
| // Camera-specific values come only from discovery | |
| EXPECT_EQ(profile.avsmEndpoint, kInvalidEndpointId); | |
| EXPECT_FALSE(profile.hasWatermark); | |
| EXPECT_FALSE(profile.hasOSD); | |
| EXPECT_GT(profile.maxFrameRate, profile.minFrameRate); | |
| EXPECT_GT(profile.maxBitRateBps, profile.minBitRateBps); | |
| EXPECT_GE(profile.maxWidth, profile.minWidth); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, DiscoveryFailureFailsRequest) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| EXPECT_EQ(client.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 0); // Nothing completes until discovery does | |
| client.OnProfileDiscoveryComplete(CHIP_ERROR_TIMEOUT); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::Failure); | |
| // Client is reusable after the failed discovery | |
| EXPECT_EQ(client.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, DiscoverySuccessWithoutSessionFailsRequest) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| EXPECT_EQ(client.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| // Discovery reporting success without a held session cannot send the command | |
| client.OnProfileDiscoveryComplete(CHIP_NO_ERROR); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::Failure); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, CancelDropsPendingRequestWithoutCompletion) | |
| { | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| mClient.SimulateCommandSent(kTestSender); | |
| mClient.Cancel(); | |
| // Late interaction events after Cancel must not produce a completion | |
| mClient.OnError(kTestSender, StatusIB(Status::Failure).ToChipError()); | |
| mClient.OnDone(kTestSender); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 0); | |
| // The client accepts a new request after cancellation | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| EXPECT_EQ(mClient.mConnectRequests, 2); | |
| } | |
| // Feeds the two bound-carrying capability reports in the given order and returns the reconciled | |
| // profile, so a test can prove the outcome does not depend on which arrived first. | |
| DefaultAvAnalysisCameraClient::CameraProfile ReconcileBounds(bool aSensorFirst) | |
| { | |
| ProfileTestClient client; | |
| CASESessionManager caseSessionManager; | |
| VerifyOrDie(client.Init(&caseSessionManager) == CHIP_NO_ERROR); | |
| const auto feedSensor = [&client] { | |
| CameraAvStreamManagement::Structs::VideoSensorParamsStruct::Type sensorParams; | |
| sensorParams.sensorWidth = 1920; | |
| sensorParams.sensorHeight = 1080; | |
| sensorParams.maxFPS = 30; | |
| FeedCapability(client, CameraAvStreamManagement::Attributes::VideoSensorParams::Id, | |
| [&](TLV::TLVWriter & w) { return DataModel::Encode(w, TLV::AnonymousTag(), sensorParams); }); | |
| }; | |
| // A self-inconsistent camera: its smallest encodable point exceeds its own sensor | |
| const auto feedTradeOffPoint = [&client] { | |
| using TradeOffPoint = CameraAvStreamManagement::Structs::RateDistortionTradeOffPointsStruct::Type; | |
| TradeOffPoint point; | |
| point.codec = CameraAvStreamManagement::VideoCodecEnum::kH264; | |
| point.resolution.width = 3840; | |
| point.resolution.height = 2160; | |
| point.minBitRate = 8000000; | |
| FeedCapability(client, CameraAvStreamManagement::Attributes::RateDistortionTradeOffPoints::Id, | |
| [&](TLV::TLVWriter & w) { return EncodeArray(w, Span<const TradeOffPoint>(&point, 1)); }); | |
| }; | |
| if (aSensorFirst) | |
| { | |
| feedSensor(); | |
| feedTradeOffPoint(); | |
| } | |
| else | |
| { | |
| feedTradeOffPoint(); | |
| feedSensor(); | |
| } | |
| DefaultAvAnalysisCameraClient::CameraProfile profile = client.CurrentProfile(); | |
| ProfileTestClient::NormalizeProfile(profile); | |
| return profile; | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, ReconciledBoundsDoNotDependOnReportOrder) | |
| { | |
| const auto sensorFirst = ReconcileBounds(/* aSensorFirst = */ true); | |
| const auto pointsFirst = ReconcileBounds(/* aSensorFirst = */ false); | |
| // Attribute reports have no guaranteed order, so the same data must reconcile the same way | |
| EXPECT_EQ(sensorFirst.minWidth, pointsFirst.minWidth); | |
| EXPECT_EQ(sensorFirst.maxWidth, pointsFirst.maxWidth); | |
| EXPECT_EQ(sensorFirst.minHeight, pointsFirst.minHeight); | |
| EXPECT_EQ(sensorFirst.maxHeight, pointsFirst.maxHeight); | |
| EXPECT_EQ(sensorFirst.minBitRateBps, pointsFirst.minBitRateBps); | |
| EXPECT_EQ(sensorFirst.maxBitRateBps, pointsFirst.maxBitRateBps); | |
| // The sensor is the camera's ceiling, so the resolution never exceeds what it reported | |
| EXPECT_EQ(sensorFirst.maxWidth, 1920); | |
| EXPECT_EQ(sensorFirst.minWidth, 1920); | |
| // Nothing publishes a maximum bit rate, so the default rises to what the camera needs | |
| EXPECT_EQ(sensorFirst.minBitRateBps, 8000000u); | |
| EXPECT_EQ(sensorFirst.maxBitRateBps, 8000000u); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, NormalizeProfileKeepsBoundsSatisfiable) | |
| { | |
| // Camera-reported minimums can land above the maximums; such a range cannot be requested. | |
| using Profile = ProfileTestClient::CameraProfile; | |
| Profile profile; | |
| profile.minWidth = 3840; | |
| profile.minHeight = 2160; | |
| profile.minFrameRate = 60; | |
| profile.minBitRateBps = 8000000; | |
| ProfileTestClient::NormalizeProfile(profile); | |
| EXPECT_EQ(profile.minWidth, Profile::kDefaultMaxWidth); | |
| EXPECT_EQ(profile.maxWidth, Profile::kDefaultMaxWidth); | |
| EXPECT_EQ(profile.minHeight, Profile::kDefaultMaxHeight); | |
| EXPECT_EQ(profile.minFrameRate, Profile::kDefaultMaxFrameRate); | |
| EXPECT_EQ(profile.minBitRateBps, 8000000u); | |
| EXPECT_EQ(profile.maxBitRateBps, 8000000u); | |
| auto request = ProfileTestClient::BuildAllocateRequest(profile); | |
| EXPECT_GE(request.maxResolution.width, request.minResolution.width); | |
| EXPECT_GE(request.maxResolution.height, request.minResolution.height); | |
| EXPECT_GE(request.maxBitRate, request.minBitRate); | |
| EXPECT_GE(request.maxFrameRate, request.minFrameRate); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, DiscoveryFindsAvsmEndpointInServerList) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| uint8_t buffer[64]; | |
| TLV::TLVReader reader; | |
| // Endpoint 0 serves Descriptor but not CameraAVStreamManagement: not a match | |
| const ClusterId kRootClusters[] = { Descriptor::Id }; | |
| ASSERT_TRUE(EncodeServerList(Span<const ClusterId>(kRootClusters), MutableByteSpan(buffer), reader)); | |
| ConcreteDataAttributePath rootPath(0, Descriptor::Id, Descriptor::Attributes::ServerList::Id); | |
| client.HandleServerListReport(rootPath, reader); | |
| EXPECT_EQ(client.CurrentProfile().avsmEndpoint, kInvalidEndpointId); | |
| // Endpoint 3 serves CameraAVStreamManagement: discovered | |
| const ClusterId kCameraClusters[] = { Descriptor::Id, CameraAvStreamManagement::Id }; | |
| ASSERT_TRUE(EncodeServerList(Span<const ClusterId>(kCameraClusters), MutableByteSpan(buffer), reader)); | |
| ConcreteDataAttributePath cameraPath(3, Descriptor::Id, Descriptor::Attributes::ServerList::Id); | |
| client.HandleServerListReport(cameraPath, reader); | |
| EXPECT_EQ(client.CurrentProfile().avsmEndpoint, 3); | |
| // First match wins: a later endpoint with the cluster does not overwrite it | |
| ASSERT_TRUE(EncodeServerList(Span<const ClusterId>(kCameraClusters), MutableByteSpan(buffer), reader)); | |
| ConcreteDataAttributePath laterPath(4, Descriptor::Id, Descriptor::Attributes::ServerList::Id); | |
| client.HandleServerListReport(laterPath, reader); | |
| EXPECT_EQ(client.CurrentProfile().avsmEndpoint, 3); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, CapabilityReadDerivesTraitsFromFeatureMap) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| const uint32_t featureMap = to_underlying(CameraAvStreamManagement::Feature::kWatermark); | |
| FeedCapability(client, Globals::Attributes::FeatureMap::Id, | |
| [&](TLV::TLVWriter & w) { return w.Put(TLV::AnonymousTag(), featureMap); }); | |
| EXPECT_TRUE(client.CurrentProfile().hasWatermark); | |
| EXPECT_FALSE(client.CurrentProfile().hasOSD); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, CapabilityReadBoundsResolutionAndFrameRate) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| CameraAvStreamManagement::Structs::VideoSensorParamsStruct::Type sensorParams; | |
| sensorParams.sensorWidth = 320; | |
| sensorParams.sensorHeight = 240; | |
| sensorParams.maxFPS = 10; | |
| FeedCapability(client, CameraAvStreamManagement::Attributes::VideoSensorParams::Id, | |
| [&](TLV::TLVWriter & w) { return DataModel::Encode(w, TLV::AnonymousTag(), sensorParams); }); | |
| EXPECT_EQ(client.CurrentProfile().maxWidth, 320); | |
| EXPECT_EQ(client.CurrentProfile().maxHeight, 240); | |
| EXPECT_EQ(client.CurrentProfile().maxFrameRate, 10); | |
| // Reconciliation happens once, where the request is built | |
| DefaultAvAnalysisCameraClient::CameraProfile profile = client.CurrentProfile(); | |
| ProfileTestClient::NormalizeProfile(profile); | |
| // The defaults (640x480 at 15fps) exceeded the sensor, so they were clamped down to it | |
| EXPECT_EQ(profile.minWidth, 320); | |
| EXPECT_EQ(profile.minHeight, 240); | |
| EXPECT_EQ(profile.minFrameRate, 10); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, CapabilityReadTakesMinimumsFromTradeOffPoints) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| using TradeOffPoint = CameraAvStreamManagement::Structs::RateDistortionTradeOffPointsStruct::Type; | |
| TradeOffPoint points[2]; | |
| // Values deliberately unlike the profile defaults, so each assertion below can fail | |
| points[0].codec = CameraAvStreamManagement::VideoCodecEnum::kH264; | |
| points[0].resolution.width = 512; | |
| points[0].resolution.height = 288; | |
| points[0].minBitRate = 250000; | |
| points[1].codec = CameraAvStreamManagement::VideoCodecEnum::kHevc; | |
| points[1].resolution.width = 320; | |
| points[1].resolution.height = 180; | |
| points[1].minBitRate = 100000; | |
| FeedCapability(client, CameraAvStreamManagement::Attributes::RateDistortionTradeOffPoints::Id, | |
| [&](TLV::TLVWriter & w) { return EncodeArray(w, Span<const TradeOffPoint>(points)); }); | |
| // Only the H.264 point counts; the HEVC one must be ignored | |
| EXPECT_EQ(client.CurrentProfile().minBitRateBps, 250000u); | |
| EXPECT_EQ(client.CurrentProfile().minWidth, 512); | |
| EXPECT_EQ(client.CurrentProfile().minHeight, 288); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, EveryCapabilityAttributeIsRequiredForAnAllocation) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| EXPECT_FALSE(client.CurrentRequest().AllCapabilitiesReported()); | |
| const uint32_t featureMap = to_underlying(CameraAvStreamManagement::Feature::kWatermark); | |
| FeedCapability(client, Globals::Attributes::FeatureMap::Id, | |
| [&](TLV::TLVWriter & w) { return w.Put(TLV::AnonymousTag(), featureMap); }); | |
| EXPECT_FALSE(client.CurrentRequest().AllCapabilitiesReported()); | |
| CameraAvStreamManagement::Structs::VideoSensorParamsStruct::Type sensorParams; | |
| sensorParams.sensorWidth = 1280; | |
| sensorParams.sensorHeight = 720; | |
| sensorParams.maxFPS = 25; | |
| FeedCapability(client, CameraAvStreamManagement::Attributes::VideoSensorParams::Id, | |
| [&](TLV::TLVWriter & w) { return DataModel::Encode(w, TLV::AnonymousTag(), sensorParams); }); | |
| EXPECT_FALSE(client.CurrentRequest().AllCapabilitiesReported()); | |
| using TradeOffPoint = CameraAvStreamManagement::Structs::RateDistortionTradeOffPointsStruct::Type; | |
| TradeOffPoint point; | |
| point.codec = CameraAvStreamManagement::VideoCodecEnum::kH264; | |
| point.resolution.width = 640; | |
| point.resolution.height = 360; | |
| point.minBitRate = 250000; | |
| FeedCapability(client, CameraAvStreamManagement::Attributes::RateDistortionTradeOffPoints::Id, | |
| [&](TLV::TLVWriter & w) { return EncodeArray(w, Span<const TradeOffPoint>(&point, 1)); }); | |
| EXPECT_FALSE(client.CurrentRequest().AllCapabilitiesReported()); | |
| const Globals::StreamUsageEnum usages[] = { Globals::StreamUsageEnum::kAnalysis }; | |
| FeedCapability(client, CameraAvStreamManagement::Attributes::SupportedStreamUsages::Id, | |
| [&](TLV::TLVWriter & w) { return EncodeArray(w, Span<const Globals::StreamUsageEnum>(usages)); }); | |
| EXPECT_TRUE(client.CurrentRequest().AllCapabilitiesReported()); | |
| EXPECT_EQ(client.CurrentRequest().MissingCapabilities(), 0); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, CapabilityReadDetectsMissingAnalysisUsage) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| EXPECT_TRUE(client.CurrentRequest().AnalysisUsageSupported()); | |
| const Globals::StreamUsageEnum usages[] = { Globals::StreamUsageEnum::kLiveView, Globals::StreamUsageEnum::kRecording }; | |
| FeedCapability(client, CameraAvStreamManagement::Attributes::SupportedStreamUsages::Id, | |
| [&](TLV::TLVWriter & w) { return EncodeArray(w, Span<const Globals::StreamUsageEnum>(usages)); }); | |
| EXPECT_FALSE(client.CurrentRequest().AnalysisUsageSupported()); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, DoneWithoutResponseIsFailure) | |
| { | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| mClient.SimulateCommandSent(kTestSender); | |
| mClient.OnDone(kTestSender); // Interaction ended with neither response nor error | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::Failure); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, StaleInteractionDoesNotFailTheNextRequest) | |
| { | |
| // A CommandSender always reports OnDone after OnResponse. If the response's completion starts a | |
| // new request, that trailing OnDone belongs to the finished interaction and must not touch it. | |
| auto * firstSender = reinterpret_cast<CommandSender *>(0x1); | |
| auto * secondSender = reinterpret_cast<CommandSender *>(0x2); | |
| ReentrantCallback callback; | |
| callback.mClient = &mClient; | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, callback), CHIP_NO_ERROR); | |
| mClient.SimulateCommandSent(firstSender); | |
| ConcreteCommandPath responsePath(kCameraEndpoint, CameraAvStreamManagement::Id, | |
| CameraAvStreamManagement::Commands::VideoStreamAllocateResponse::Id); | |
| mClient.OnResponse(firstSender, responsePath, StatusIB(), nullptr); | |
| ASSERT_EQ(callback.mAllocatedCount, 1); | |
| // The deallocation started from that completion is now in flight | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, callback), CHIP_ERROR_BUSY); | |
| // The first interaction's trailing OnDone must leave it alone | |
| mClient.OnDone(firstSender); | |
| EXPECT_EQ(callback.mDeallocatedCount, 0); | |
| EXPECT_EQ(mClient.RequestVideoStreamAllocation(kCameraNode, callback), CHIP_ERROR_BUSY); | |
| // The second interaction still completes normally | |
| mClient.SimulateCommandSent(secondSender); | |
| mClient.OnError(secondSender, StatusIB(Status::NotFound).ToChipError()); | |
| EXPECT_EQ(callback.mDeallocatedCount, 1); | |
| EXPECT_EQ(callback.mLastStatus, Status::NotFound); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, AttributeReportWithoutARequestIsIgnored) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| uint8_t buffer[64]; | |
| TLV::TLVReader reader; | |
| const ClusterId kCameraClusters[] = { Descriptor::Id, CameraAvStreamManagement::Id }; | |
| ASSERT_TRUE(EncodeServerList(Span<const ClusterId>(kCameraClusters), MutableByteSpan(buffer), reader)); | |
| // No request is in flight, so there is no discovery phase to route this report to | |
| ConcreteDataAttributePath path(3, Descriptor::Id, Descriptor::Attributes::ServerList::Id); | |
| client.OnAttributeData(path, &reader, StatusIB()); | |
| EXPECT_EQ(client.CurrentProfile().avsmEndpoint, kInvalidEndpointId); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, EndpointDiscoveryWithoutAvsmEndpointReturnsNotFound) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| ASSERT_EQ(client.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| client.CurrentRequest().Advance(ProfileTestClient::Request::Phase::kDiscoveringEndpoint); | |
| // Endpoint 0 serves Descriptor but not CameraAVStreamManagement | |
| uint8_t buffer[64]; | |
| TLV::TLVReader reader; | |
| const ClusterId kRootClusters[] = { Descriptor::Id }; | |
| ASSERT_TRUE(EncodeServerList(Span<const ClusterId>(kRootClusters), MutableByteSpan(buffer), reader)); | |
| ConcreteDataAttributePath rootPath(0, Descriptor::Id, Descriptor::Attributes::ServerList::Id); | |
| client.OnAttributeData(rootPath, &reader, StatusIB()); | |
| // Read finishes cleanly with no matching endpoint -> Status::NotFound | |
| client.OnDone(static_cast<ReadClient *>(nullptr)); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::NotFound); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, EndpointDiscoveryErrorReturnsFailure) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| ASSERT_EQ(client.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| client.CurrentRequest().Advance(ProfileTestClient::Request::Phase::kDiscoveringEndpoint); | |
| // Read fails with a transport/IM error -> Status::Failure | |
| client.OnError(CHIP_ERROR_TIMEOUT); | |
| client.OnDone(static_cast<ReadClient *>(nullptr)); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::Failure); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, EndpointDiscoveryAttributeStatusFailureReturnsFailure) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| ASSERT_EQ(client.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| client.CurrentRequest().Advance(ProfileTestClient::Request::Phase::kDiscoveringEndpoint); | |
| ConcreteDataAttributePath rootPath(0, Descriptor::Id, Descriptor::Attributes::ServerList::Id); | |
| client.OnAttributeData(rootPath, nullptr, StatusIB(Status::Failure)); | |
| client.OnDone(static_cast<ReadClient *>(nullptr)); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::Failure); | |
| } | |
| TEST_F(TestDefaultAvAnalysisCameraClient, EndpointDiscoveryDecodeFailureReturnsFailure) | |
| { | |
| ProfileTestClient client; | |
| ASSERT_EQ(client.Init(&mCASESessionManager), CHIP_NO_ERROR); | |
| ASSERT_EQ(client.RequestVideoStreamAllocation(kCameraNode, mCallback), CHIP_NO_ERROR); | |
| client.CurrentRequest().Advance(ProfileTestClient::Request::Phase::kDiscoveringEndpoint); | |
| // Encode a scalar integer instead of a TLV array for ServerList | |
| uint8_t buffer[32]; | |
| TLV::TLVReader reader; | |
| ASSERT_TRUE(EncodeTlv(buffer, sizeof(buffer), reader, | |
| [](TLV::TLVWriter & w) { return w.Put(TLV::AnonymousTag(), static_cast<uint32_t>(42)); })); | |
| ConcreteDataAttributePath rootPath(0, Descriptor::Id, Descriptor::Attributes::ServerList::Id); | |
| client.OnAttributeData(rootPath, &reader, StatusIB()); | |
| client.OnDone(static_cast<ReadClient *>(nullptr)); | |
| EXPECT_EQ(mCallback.mAllocatedCount, 1); | |
| EXPECT_EQ(mCallback.mLastStatus, Status::Failure); | |
| } | |
| } // namespace |