| /* | |
| * | |
| * Copyright (c) 2026 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 <app/clusters/av-analysis-server/DefaultAvAnalysisWebRTCClient.h> | |
| #include <app/AttributePathParams.h> | |
| #include <app/InteractionModelEngine.h> | |
| #include <clusters/Descriptor/Attributes.h> | |
| #include <clusters/Descriptor/Ids.h> | |
| #include <clusters/WebRTCTransportProvider/Ids.h> | |
| #include <lib/support/CodeUtils.h> | |
| #include <lib/support/logging/CHIPLogging.h> | |
| using chip::Protocols::InteractionModel::Status; | |
| namespace chip { | |
| namespace app { | |
| namespace Clusters { | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::Init(CASESessionManager * aCASESessionManager, | |
| AvAnalysisWebRTCPeerDelegate * aPeerDelegate, | |
| WebRTCTransportRequestor::WebRTCTransportRequestorCluster * aRequestorCluster, | |
| uint8_t aMaxSessions) | |
| { | |
| VerifyOrReturnError(aCASESessionManager != nullptr, CHIP_ERROR_INVALID_ARGUMENT); | |
| VerifyOrReturnError(aPeerDelegate != nullptr, CHIP_ERROR_INVALID_ARGUMENT); | |
| VerifyOrReturnError(aRequestorCluster != nullptr, CHIP_ERROR_INVALID_ARGUMENT); | |
| VerifyOrReturnError(aMaxSessions > 0, CHIP_ERROR_INVALID_ARGUMENT); | |
| VerifyOrReturnError(!mSessions, CHIP_ERROR_INCORRECT_STATE); | |
| VerifyOrReturnError(mSessions.Calloc(aMaxSessions), CHIP_ERROR_NO_MEMORY); | |
| mCASESessionManager = aCASESessionManager; | |
| mPeerDelegate = aPeerDelegate; | |
| mRequestorCluster = aRequestorCluster; | |
| mMaxSessions = aMaxSessions; | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::RequestSession(const ScopedNodeId & aCameraNode, EndpointId aWebRTCEndpoint, | |
| uint16_t aVideoStreamId, AvAnalysisWebRTCClient::Callback & aCallback) | |
| { | |
| ReturnErrorOnFailure(CanStartRequest()); | |
| // Refused before any signaling: a session the camera granted but this node could not track would | |
| // be one nobody ends. Single flight means only this request can consume the slot found here. | |
| VerifyOrReturnError(FindFreeSession() != nullptr, CHIP_ERROR_NO_MEMORY); | |
| mRequest.BeginProvideOffer(aCameraNode, aWebRTCEndpoint, aVideoStreamId, aCallback); | |
| EstablishSession(aCameraNode); | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::EndSession(const ScopedNodeId & aCameraNode, EndpointId aWebRTCEndpoint, | |
| uint16_t aWebRTCSessionId, AvAnalysisWebRTCClient::Callback & aCallback) | |
| { | |
| // Ending a session this client tracks, at the endpoint it was initiated on | |
| TrackedSession * session = FindTrackedSession(aCameraNode, aWebRTCSessionId); | |
| VerifyOrReturnError(session != nullptr, CHIP_ERROR_INVALID_ARGUMENT); | |
| VerifyOrReturnError(session->providerEndpoint == aWebRTCEndpoint, CHIP_ERROR_INVALID_ARGUMENT); | |
| ReturnErrorOnFailure(CanStartRequest()); | |
| mRequest.BeginEndSession(aCameraNode, aWebRTCEndpoint, aWebRTCSessionId, aCallback); | |
| EstablishSession(aCameraNode); | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::SendICECandidates(const ScopedNodeId & aCameraNode, uint16_t aWebRTCSessionId, | |
| Span<const Globals::Structs::ICECandidateStruct::Type> aCandidates) | |
| { | |
| // Candidates for a session this client tracks, routed to the camera that assigned it | |
| TrackedSession * session = FindTrackedSession(aCameraNode, aWebRTCSessionId); | |
| VerifyOrReturnError(session != nullptr, CHIP_ERROR_INVALID_ARGUMENT); | |
| VerifyOrReturnError(!aCandidates.empty(), CHIP_ERROR_INVALID_ARGUMENT); | |
| ReturnErrorOnFailure(CanStartRequest()); | |
| mICECandidates.clear(); | |
| mICECandidates.reserve(aCandidates.size()); | |
| for (const auto & candidate : aCandidates) | |
| { | |
| BufferedICECandidate & buffered = mICECandidates.emplace_back(); | |
| buffered.candidate.assign(candidate.candidate.data(), candidate.candidate.size()); | |
| if (!candidate.SDPMid.IsNull()) | |
| { | |
| buffered.sdpMid.emplace(candidate.SDPMid.Value().data(), candidate.SDPMid.Value().size()); | |
| } | |
| buffered.sdpMLineIndex = candidate.SDPMLineIndex; | |
| } | |
| mRequest.BeginProvideICECandidates(session->cameraNode, session->providerEndpoint, aWebRTCSessionId); | |
| EstablishSession(mRequest.CameraNode()); | |
| return CHIP_NO_ERROR; | |
| } | |
| void DefaultAvAnalysisWebRTCClient::Cancel() | |
| { | |
| if (mRequest.InFlight()) | |
| { | |
| // Deregister from a session establishment still in progress | |
| mOnConnectedCallback.Cancel(); | |
| mOnConnectionFailureCallback.Cancel(); | |
| // Abort a provider check or command exchange still in progress; callbacks die with them | |
| ResetReadClient(); | |
| mCommandSender.reset(); | |
| // Forget the request without delivering a completion; the application drops the peer | |
| // connection it may have created for it | |
| const bool offerRequested = mRequest.OfferRequested(); | |
| mRequest.Reset(); | |
| mOfferSdp.clear(); | |
| mICECandidates.clear(); | |
| if (offerRequested) | |
| { | |
| mPeerDelegate->OnOfferAbandoned(); | |
| } | |
| } | |
| // Forget the initiated sessions without delivering their outcomes | |
| for (uint8_t i = 0; i < mMaxSessions; i++) | |
| { | |
| if (mSessions[i].inUse) | |
| { | |
| ReleaseSession(mSessions[i]); | |
| } | |
| } | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::InvokeOnHeldSession(CommandId aCommandId, const DataModel::EncodableToTLV & aRequest) | |
| { | |
| VerifyOrReturnError(mRequest.HasSession(), CHIP_ERROR_INCORRECT_STATE); | |
| // A CommandSender may be destroyed at any time except from its own OnResponse or OnError, so | |
| // replacing a live one is only safe outside those; single flight means there is none here. | |
| VerifyOrReturnError(!mCommandSender, CHIP_ERROR_INCORRECT_STATE); | |
| auto session = mRequest.Session(); | |
| mCommandSender = Platform::MakeUnique<CommandSender>(this, &mRequest.ExchangeManager(), /* aIsTimedRequest = */ false, | |
| /* aSuppressResponse = */ false, session.Value()->AllowsLargePayload()); | |
| VerifyOrReturnError(mCommandSender != nullptr, CHIP_ERROR_NO_MEMORY); | |
| // Recorded before sending: a send can dispatch the interaction's completion synchronously, and | |
| // that callback must already recognise this sender as ours. | |
| mRequest.SetInvokedSender(mCommandSender.get()); | |
| mRequest.Advance(Request::Phase::kInvoking); | |
| CommandPathParams commandPath{ mRequest.WebRTCEndpoint(), WebRTCTransportProvider::Id, aCommandId, | |
| CommandPathFlags::kEndpointIdValid }; | |
| CommandSender::AddRequestDataParameters addRequestDataParams; | |
| CHIP_ERROR err = mCommandSender->AddRequestData(commandPath, aRequest, addRequestDataParams); | |
| if (err == CHIP_NO_ERROR) | |
| { | |
| err = mCommandSender->SendCommandRequest(session.Value()); | |
| } | |
| if (err != CHIP_NO_ERROR) | |
| { | |
| mRequest.SetInvokedSender(nullptr); | |
| mCommandSender.reset(); | |
| } | |
| return err; | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::CanStartRequest() const | |
| { | |
| VerifyOrReturnError(mCASESessionManager != nullptr, CHIP_ERROR_INCORRECT_STATE); | |
| VerifyOrReturnError(!mRequest.InFlight(), CHIP_ERROR_BUSY); | |
| return CHIP_NO_ERROR; | |
| } | |
| void DefaultAvAnalysisWebRTCClient::OnDeviceConnected(void * context, Messaging::ExchangeManager & exchangeMgr, | |
| const SessionHandle & sessionHandle) | |
| { | |
| auto * self = static_cast<DefaultAvAnalysisWebRTCClient *>(context); | |
| VerifyOrReturn(self->mRequest.InPhase(Request::Phase::kConnecting), | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: unexpected session establishment")); | |
| self->mRequest.HoldSession(sessionHandle, exchangeMgr); | |
| self->ContinueWithSession(); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::ContinueWithSession() | |
| { | |
| if (mRequest.GetCommandType() == Request::CommandType::kEndSession) | |
| { | |
| CHIP_ERROR endErr = SendEndSession(); | |
| if (endErr != CHIP_NO_ERROR) | |
| { | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: EndSession not sent: %" CHIP_ERROR_FORMAT, endErr.Format()); | |
| FinishRequest(Status::Failure, mRequest.WebRTCSessionId()); | |
| } | |
| return; | |
| } | |
| if (mRequest.GetCommandType() == Request::CommandType::kProvideICECandidates) | |
| { | |
| CHIP_ERROR iceErr = SendProvideICECandidates(); | |
| if (iceErr != CHIP_NO_ERROR) | |
| { | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: ProvideICECandidates not sent: %" CHIP_ERROR_FORMAT, iceErr.Format()); | |
| FinishRequest(Status::Failure, mRequest.WebRTCSessionId()); | |
| } | |
| return; | |
| } | |
| // the provided endpoint must host WebRTCTransportProvider before a session may be initiated toward it | |
| mRequest.Advance(Request::Phase::kCheckingProvider); | |
| CHIP_ERROR err = SendProviderCheckRead(); | |
| if (err != CHIP_NO_ERROR) | |
| { | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: provider check not started: %" CHIP_ERROR_FORMAT, err.Format()); | |
| FinishRequest(Status::Failure, mRequest.WebRTCSessionId()); | |
| } | |
| } | |
| void DefaultAvAnalysisWebRTCClient::OnDeviceConnectionFailure(void * context, const ScopedNodeId & peerId, CHIP_ERROR error) | |
| { | |
| auto * self = static_cast<DefaultAvAnalysisWebRTCClient *>(context); | |
| VerifyOrReturn(self->mRequest.InPhase(Request::Phase::kConnecting)); | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: could not reach camera node: %" CHIP_ERROR_FORMAT, error.Format()); | |
| self->FinishRequest(Status::Failure, self->mRequest.WebRTCSessionId()); | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::SendProviderCheckRead() | |
| { | |
| VerifyOrReturnError(mRequest.HasSession(), CHIP_ERROR_INCORRECT_STATE); | |
| // A ReadClient may be destroyed outside its callbacks and from its own OnDone, but not from any | |
| // other one, so a live one is never replaced; single flight means there is none here. | |
| VerifyOrReturnError(!mReadClient, CHIP_ERROR_INCORRECT_STATE); | |
| AttributePathParams readPath(mRequest.WebRTCEndpoint(), Descriptor::Id, Descriptor::Attributes::ServerList::Id); | |
| ReadPrepareParams readParams(mRequest.Session().Value()); | |
| readParams.mpAttributePathParamsList = &readPath; | |
| readParams.mAttributePathParamsListSize = 1; | |
| mReadCallback = Platform::MakeUnique<BufferedReadCallback>(*this); | |
| VerifyOrReturnError(mReadCallback != nullptr, CHIP_ERROR_NO_MEMORY); | |
| mReadClient = Platform::MakeUnique<ReadClient>(InteractionModelEngine::GetInstance(), &mRequest.ExchangeManager(), | |
| *mReadCallback, ReadClient::InteractionType::Read); | |
| if (mReadClient == nullptr) | |
| { | |
| mReadCallback.reset(); | |
| return CHIP_ERROR_NO_MEMORY; | |
| } | |
| CHIP_ERROR err = mReadClient->SendRequest(readParams); | |
| if (err != CHIP_NO_ERROR) | |
| { | |
| ResetReadClient(); | |
| } | |
| return err; | |
| } | |
| void DefaultAvAnalysisWebRTCClient::ResetReadClient() | |
| { | |
| // The ReadClient holds the buffered callback by reference, so it goes first | |
| mReadClient.reset(); | |
| mReadCallback.reset(); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, | |
| const StatusIB & aStatus) | |
| { | |
| VerifyOrReturn(aStatus.IsSuccess() && apData != nullptr); | |
| VerifyOrReturn(mRequest.InPhase(Request::Phase::kCheckingProvider)); | |
| HandleServerListReport(aPath, *apData); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::HandleServerListReport(const ConcreteDataAttributePath & aPath, TLV::TLVReader & aData) | |
| { | |
| VerifyOrReturn(aPath.mClusterId == Descriptor::Id && aPath.mAttributeId == Descriptor::Attributes::ServerList::Id && | |
| aPath.mEndpointId == mRequest.WebRTCEndpoint()); | |
| DataModel::DecodableList<ClusterId> serverList; | |
| VerifyOrReturn(DataModel::Decode(aData, serverList) == CHIP_NO_ERROR, | |
| mRequest.SetProviderCheck(Request::ProviderCheck::kUnreadable)); | |
| auto iter = serverList.begin(); | |
| while (iter.Next()) | |
| { | |
| if (iter.GetValue() == WebRTCTransportProvider::Id) | |
| { | |
| mRequest.SetProviderCheck(Request::ProviderCheck::kFound); | |
| return; | |
| } | |
| } | |
| if (iter.GetStatus() != CHIP_NO_ERROR) | |
| { | |
| mRequest.SetProviderCheck(Request::ProviderCheck::kUnreadable); | |
| } | |
| } | |
| void DefaultAvAnalysisWebRTCClient::OnError(CHIP_ERROR aError) | |
| { | |
| VerifyOrReturn(mRequest.InPhase(Request::Phase::kCheckingProvider)); | |
| // A failed read says nothing about the endpoint; OnDone follows and fails the request | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: provider check error: %" CHIP_ERROR_FORMAT, aError.Format()); | |
| mRequest.SetProviderCheck(Request::ProviderCheck::kUnreadable); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::OnDone(ReadClient * apReadClient) | |
| { | |
| ResetReadClient(); | |
| VerifyOrReturn(mRequest.InPhase(Request::Phase::kCheckingProvider), | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: unexpected read completion")); | |
| OnProviderCheckComplete(); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::OnProviderCheckComplete() | |
| { | |
| switch (mRequest.GetProviderCheck()) | |
| { | |
| case Request::ProviderCheck::kUnreadable: | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: the ServerList of endpoint %u could not be read", mRequest.WebRTCEndpoint()); | |
| FinishRequest(Status::Failure, mRequest.WebRTCSessionId()); | |
| return; | |
| case Request::ProviderCheck::kNotFound: | |
| // no WebRTCTransportProvider on the endpoint the command named | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: no WebRTCTransportProvider on endpoint %u", mRequest.WebRTCEndpoint()); | |
| FinishRequest(Status::NotFound, mRequest.WebRTCSessionId()); | |
| return; | |
| case Request::ProviderCheck::kFound: | |
| break; | |
| } | |
| mRequest.Advance(Request::Phase::kCreatingOffer); | |
| CHIP_ERROR err = mPeerDelegate->CreateOffer(*this); | |
| // An offer delivered synchronously has moved the request on; only a request still awaiting one fails here | |
| if (err != CHIP_NO_ERROR && mRequest.InPhase(Request::Phase::kCreatingOffer)) | |
| { | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: no SDP offer forthcoming: %" CHIP_ERROR_FORMAT, err.Format()); | |
| FinishRequest(Status::Failure, mRequest.WebRTCSessionId()); | |
| } | |
| } | |
| void DefaultAvAnalysisWebRTCClient::OnOfferReady(CHIP_ERROR aError, CharSpan aSdp) | |
| { | |
| VerifyOrReturn(mRequest.InPhase(Request::Phase::kCreatingOffer), | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: unexpected offer delivery")); | |
| if (aError != CHIP_NO_ERROR || aSdp.empty()) | |
| { | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: the application produced no SDP offer: %" CHIP_ERROR_FORMAT, aError.Format()); | |
| FinishRequest(Status::Failure, mRequest.WebRTCSessionId()); | |
| return; | |
| } | |
| mOfferSdp.assign(aSdp.data(), aSdp.size()); | |
| CHIP_ERROR err = SendProvideOffer(); | |
| if (err != CHIP_NO_ERROR) | |
| { | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: ProvideOffer not sent: %" CHIP_ERROR_FORMAT, err.Format()); | |
| FinishRequest(Status::Failure, mRequest.WebRTCSessionId()); | |
| } | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::BuildProvideOffer(WebRTCTransportProvider::Commands::ProvideOffer::Type & aRequest, | |
| uint16_t & aVideoStream) const | |
| { | |
| aVideoStream = mRequest.VideoStreamId(); | |
| aRequest.webRTCSessionID.SetNull(); // a new session, to be assigned by the camera | |
| aRequest.sdp = CharSpan(mOfferSdp.data(), mOfferSdp.size()); | |
| aRequest.streamUsage = Globals::StreamUsageEnum::kAnalysis; | |
| // The requestor cluster's registered path is where the camera's answering commands must land | |
| aRequest.originatingEndpointID = mRequestorCluster->GetPaths().front().mEndpointId; | |
| aRequest.videoStreams = MakeOptional(DataModel::List<const uint16_t>(&aVideoStream, 1)); | |
| // No audio for analysis; ICE servers and transport policy are the camera's defaults | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::SendProvideOffer() | |
| { | |
| uint16_t videoStream = 0; | |
| WebRTCTransportProvider::Commands::ProvideOffer::Type request; | |
| ReturnErrorOnFailure(BuildProvideOffer(request, videoStream)); | |
| return InvokeOnHeldSession(WebRTCTransportProvider::Commands::ProvideOffer::Id, | |
| DataModel::EncodableType<WebRTCTransportProvider::Commands::ProvideOffer::Type>(request)); | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::BuildEndSession(WebRTCTransportProvider::Commands::EndSession::Type & aRequest) const | |
| { | |
| aRequest.webRTCSessionID = mRequest.WebRTCSessionId(); | |
| aRequest.reason = Globals::WebRTCEndReasonEnum::kUserHangup; | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::SendEndSession() | |
| { | |
| WebRTCTransportProvider::Commands::EndSession::Type request; | |
| ReturnErrorOnFailure(BuildEndSession(request)); | |
| return InvokeOnHeldSession(WebRTCTransportProvider::Commands::EndSession::Id, | |
| DataModel::EncodableType<WebRTCTransportProvider::Commands::EndSession::Type>(request)); | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::BuildProvideICECandidates( | |
| WebRTCTransportProvider::Commands::ProvideICECandidates::Type & aRequest, | |
| std::vector<Globals::Structs::ICECandidateStruct::Type> & aCandidates) const | |
| { | |
| VerifyOrReturnError(!mICECandidates.empty(), CHIP_ERROR_INCORRECT_STATE); | |
| aCandidates.clear(); | |
| aCandidates.reserve(mICECandidates.size()); | |
| for (const BufferedICECandidate & buffered : mICECandidates) | |
| { | |
| Globals::Structs::ICECandidateStruct::Type & candidate = aCandidates.emplace_back(); | |
| candidate.candidate = CharSpan(buffered.candidate.data(), buffered.candidate.size()); | |
| if (buffered.sdpMid.has_value()) | |
| { | |
| candidate.SDPMid.SetNonNull(CharSpan(buffered.sdpMid->data(), buffered.sdpMid->size())); | |
| } | |
| candidate.SDPMLineIndex = buffered.sdpMLineIndex; | |
| } | |
| aRequest.webRTCSessionID = mRequest.WebRTCSessionId(); | |
| aRequest.ICECandidates = | |
| DataModel::List<const Globals::Structs::ICECandidateStruct::Type>(aCandidates.data(), aCandidates.size()); | |
| return CHIP_NO_ERROR; | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::SendProvideICECandidates() | |
| { | |
| // Encoded by AddRequestData within InvokeOnHeldSession, so the list storage need only live this long | |
| std::vector<Globals::Structs::ICECandidateStruct::Type> candidates; | |
| WebRTCTransportProvider::Commands::ProvideICECandidates::Type request; | |
| ReturnErrorOnFailure(BuildProvideICECandidates(request, candidates)); | |
| return InvokeOnHeldSession(WebRTCTransportProvider::Commands::ProvideICECandidates::Id, | |
| DataModel::EncodableType<WebRTCTransportProvider::Commands::ProvideICECandidates::Type>(request)); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::OnResponse(CommandSender * apCommandSender, const ConcreteCommandPath & aPath, | |
| const StatusIB & aStatusIB, TLV::TLVReader * apData) | |
| { | |
| VerifyOrReturn(mRequest.WasInvokedBy(apCommandSender), | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: response for an interaction already finished with")); | |
| VerifyOrReturn(mRequest.InPhase(Request::Phase::kInvoking), | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: a second response for a command already responded to")); | |
| if (mRequest.GetCommandType() == Request::CommandType::kProvideICECandidates || | |
| mRequest.GetCommandType() == Request::CommandType::kEndSession) | |
| { | |
| // Answered with a status alone. Only a SUCCESS status reaches OnResponse; the CommandSender | |
| // delivers every error status through OnError instead. | |
| VerifyOrReturn(aStatusIB.IsSuccess()); | |
| mRequest.Advance(Request::Phase::kResponded); | |
| return; | |
| } | |
| VerifyOrReturn(aStatusIB.IsSuccess() && apData != nullptr, | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: ProvideOffer response carried no data")); | |
| VerifyOrReturn(aPath.mEndpointId == mRequest.WebRTCEndpoint() && aPath.mClusterId == WebRTCTransportProvider::Id && | |
| aPath.mCommandId == WebRTCTransportProvider::Commands::ProvideOfferResponse::Id, | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: unexpected response path")); | |
| WebRTCTransportProvider::Commands::ProvideOfferResponse::DecodableType response; | |
| VerifyOrReturn(DataModel::Decode(*apData, response) == CHIP_NO_ERROR, | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: ProvideOfferResponse did not decode")); | |
| mRequest.SetOfferResponse(response.webRTCSessionID); | |
| mRequest.Advance(Request::Phase::kResponded); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::OnError(const CommandSender * apCommandSender, CHIP_ERROR aError) | |
| { | |
| VerifyOrReturn(mRequest.WasInvokedBy(apCommandSender), | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: error for an interaction already finished with")); | |
| VerifyOrReturn(mRequest.InPhase(Request::Phase::kInvoking), | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: error after the command was already responded to")); | |
| // A camera status response, or Failure for a transport error such as a timeout. Recorded only: | |
| // the sender is still alive here, so completion waits for OnDone | |
| Status status = Status::Failure; | |
| if (aError.IsIMStatus()) | |
| { | |
| status = StatusIB(aError).mStatus; | |
| } | |
| mRequest.Fail(status); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::OnDone(CommandSender * apCommandSender) | |
| { | |
| const bool isOurs = mRequest.WasInvokedBy(apCommandSender); | |
| // The one place a CommandSender may be destroyed, and every command outcome is delivered from | |
| // here, so the client is free to accept a request started from the completion. | |
| if (mCommandSender.get() == apCommandSender) | |
| { | |
| mCommandSender.reset(); | |
| } | |
| VerifyOrReturn(isOurs); | |
| const uint16_t webRTCSessionId = mRequest.WebRTCSessionId(); | |
| const ScopedNodeId cameraNode = mRequest.CameraNode(); | |
| if (mRequest.InPhase(Request::Phase::kFailed)) | |
| { | |
| FinishRequest(mRequest.FailureStatus(), webRTCSessionId); | |
| return; | |
| } | |
| // Still kInvoking: the camera's response was rejected by OnResponse, so the command has no | |
| // usable outcome | |
| if (!mRequest.InPhase(Request::Phase::kResponded)) | |
| { | |
| FinishRequest(Status::Failure, webRTCSessionId); | |
| return; | |
| } | |
| if (mRequest.GetCommandType() == Request::CommandType::kProvideICECandidates || | |
| mRequest.GetCommandType() == Request::CommandType::kEndSession) | |
| { | |
| FinishRequest(Status::Success, webRTCSessionId); | |
| return; | |
| } | |
| CHIP_ERROR err = RegisterSession(webRTCSessionId); | |
| if (err != CHIP_NO_ERROR) | |
| { | |
| // A slot was free when the request started and only this path takes one, so the request | |
| // fails rather than leaving the camera holding a session nobody tracks. | |
| ChipLogError(Zcl, "AvAnalysisWebRTCClient: session %u not tracked: %" CHIP_ERROR_FORMAT, webRTCSessionId, err.Format()); | |
| FinishRequest(Status::ResourceExhausted, webRTCSessionId); | |
| return; | |
| } | |
| FinishRequest(Status::Success, webRTCSessionId); | |
| // Last, once the request is over and the callback knows the session: the application binds the | |
| // peer connection it created for this offer to the assigned id | |
| mPeerDelegate->OnSessionAssigned(cameraNode, webRTCSessionId); | |
| } | |
| CHIP_ERROR DefaultAvAnalysisWebRTCClient::RegisterSession(uint16_t aWebRTCSessionId) | |
| { | |
| // A camera restarts its ids from 0, so one may come back for a session we still track because | |
| // its End never reached us; that session died with the camera, so its stream is failed first | |
| if (FindTrackedSession(mRequest.CameraNode(), aWebRTCSessionId) != nullptr) | |
| { | |
| FailTrackedSession(mRequest.CameraNode(), aWebRTCSessionId); | |
| } | |
| TrackedSession * slot = FindFreeSession(); | |
| VerifyOrReturnError(slot != nullptr, CHIP_ERROR_NO_MEMORY); | |
| // Tracked session | |
| slot->inUse = true; | |
| slot->webRTCSessionId = aWebRTCSessionId; | |
| slot->callback = mRequest.PeekCallback(); | |
| slot->cameraNode = mRequest.CameraNode(); | |
| slot->providerEndpoint = mRequest.WebRTCEndpoint(); | |
| slot->videoStreamId = mRequest.VideoStreamId(); | |
| // The requestor cluster is the receiving end of this session | |
| Globals::Structs::WebRTCSessionStruct::Type session; | |
| session.id = aWebRTCSessionId; | |
| session.peerNodeID = mRequest.CameraNode().GetNodeId(); | |
| session.fabricIndex = mRequest.CameraNode().GetFabricIndex(); | |
| session.peerEndpointID = mRequest.WebRTCEndpoint(); | |
| session.streamUsage = Globals::StreamUsageEnum::kAnalysis; | |
| session.videoStreams = MakeOptional(DataModel::List<const uint16_t>(&slot->videoStreamId, 1)); | |
| session.videoStreamID = DataModel::MakeNullable(slot->videoStreamId); | |
| session.audioStreamID.SetNull(); | |
| mRequestorCluster->UpsertSession(session); | |
| return CHIP_NO_ERROR; | |
| } | |
| void DefaultAvAnalysisWebRTCClient::ReleaseSession(TrackedSession & aSession) | |
| { | |
| const ScopedNodeId cameraNode = aSession.cameraNode; | |
| const uint16_t webRTCSessionId = aSession.webRTCSessionId; | |
| mRequestorCluster->RemoveSession(webRTCSessionId, cameraNode.GetNodeId(), cameraNode.GetFabricIndex()); | |
| aSession = TrackedSession{}; | |
| mPeerDelegate->OnSessionClosed(cameraNode, webRTCSessionId); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::NotifyConnected(const ScopedNodeId & aCameraNode, uint16_t aWebRTCSessionId) | |
| { | |
| TrackedSession * session = FindTrackedSession(aCameraNode, aWebRTCSessionId); | |
| VerifyOrReturn( | |
| session != nullptr, | |
| ChipLogDetail(Zcl, "AvAnalysisWebRTCClient: connection of untracked session %u of " ChipLogFormatScopedNodeId " ignored", | |
| aWebRTCSessionId, ChipLogValueScopedNodeId(aCameraNode))); | |
| session->callback->OnSessionActive(aCameraNode, aWebRTCSessionId); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::NotifyFailed(const ScopedNodeId & aCameraNode, uint16_t aWebRTCSessionId) | |
| { | |
| FailTrackedSession(aCameraNode, aWebRTCSessionId); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::NotifyEnded(const ScopedNodeId & aCameraNode, uint16_t aWebRTCSessionId) | |
| { | |
| // An End this node did not ask for: to the AV Analysis cluster the session has failed | |
| FailTrackedSession(aCameraNode, aWebRTCSessionId); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::FailTrackedSession(const ScopedNodeId & aCameraNode, uint16_t aWebRTCSessionId) | |
| { | |
| TrackedSession * session = FindTrackedSession(aCameraNode, aWebRTCSessionId); | |
| VerifyOrReturn(session != nullptr, | |
| ChipLogDetail(Zcl, | |
| "AvAnalysisWebRTCClient: end of untracked session %u of " ChipLogFormatScopedNodeId " ignored", | |
| aWebRTCSessionId, ChipLogValueScopedNodeId(aCameraNode))); | |
| // Released before the callback learns of it, so the callback finds the session already gone | |
| AvAnalysisWebRTCClient::Callback * callback = session->callback; | |
| ReleaseSession(*session); | |
| callback->OnSessionFailed(aCameraNode, aWebRTCSessionId); | |
| } | |
| void DefaultAvAnalysisWebRTCClient::FinishRequest(Status aStatus, uint16_t aWebRTCSessionId) | |
| { | |
| VerifyOrReturn(mRequest.InFlight()); | |
| const Request::CommandType completed = mRequest.GetCommandType(); | |
| const ScopedNodeId cameraNode = mRequest.CameraNode(); | |
| AvAnalysisWebRTCClient::Callback * callback = mRequest.TakeCallback(); | |
| // An offer request failing after asking for the offer leaves the application a peer connection | |
| // that will never get a session | |
| const bool offerAbandoned = mRequest.OfferRequested() && aStatus != Status::Success; | |
| // Whether ProvideOffer reached the camera: a send that failed rolled this back | |
| const bool offerSent = mRequest.Invoked(); | |
| mRequest.Reset(); | |
| mOfferSdp.clear(); | |
| mICECandidates.clear(); | |
| if (offerAbandoned) | |
| { | |
| mPeerDelegate->OnOfferAbandoned(); | |
| } | |
| if (completed == Request::CommandType::kProvideICECandidates) | |
| { | |
| // Nobody awaits this outcome; a camera left without our candidates fails the connection, | |
| // which the application reports through NotifyFailed | |
| if (aStatus != Status::Success) | |
| { | |
| ChipLogError(Zcl, | |
| "AvAnalysisWebRTCClient: ProvideICECandidates for session %u of " ChipLogFormatScopedNodeId | |
| " failed with status 0x%02x", | |
| aWebRTCSessionId, ChipLogValueScopedNodeId(cameraNode), to_underlying(aStatus)); | |
| } | |
| return; | |
| } | |
| VerifyOrReturn(callback != nullptr); | |
| if (completed == Request::CommandType::kProvideOffer) | |
| { | |
| callback->OnSessionInitiated(aStatus, aWebRTCSessionId, offerSent); | |
| return; | |
| } | |
| // Whatever became of the EndSession, this node is done with the session: a stream whose | |
| // EndSession failed goes to Failure, whose only exit is a new session, so nothing could ever | |
| // end this one again. Its peer connection closes with it; a camera that still holds the | |
| // session ends it on its side once the peer is gone. | |
| TrackedSession * session = FindTrackedSession(cameraNode, aWebRTCSessionId); | |
| if (session != nullptr) | |
| { | |
| ReleaseSession(*session); | |
| } | |
| callback->OnSessionEnded(aStatus, aWebRTCSessionId); | |
| } | |
| DefaultAvAnalysisWebRTCClient::TrackedSession * DefaultAvAnalysisWebRTCClient::FindFreeSession() | |
| { | |
| for (uint8_t i = 0; i < mMaxSessions; i++) | |
| { | |
| if (!mSessions[i].inUse) | |
| { | |
| return &mSessions[i]; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| DefaultAvAnalysisWebRTCClient::TrackedSession * DefaultAvAnalysisWebRTCClient::FindTrackedSession(const ScopedNodeId & aCameraNode, | |
| uint16_t aWebRTCSessionId) | |
| { | |
| for (uint8_t i = 0; i < mMaxSessions; i++) | |
| { | |
| if (mSessions[i].inUse && mSessions[i].cameraNode == aCameraNode && mSessions[i].webRTCSessionId == aWebRTCSessionId) | |
| { | |
| return &mSessions[i]; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| } // namespace Clusters | |
| } // namespace app | |
| } // namespace chip |