Files
esp-matter/examples/camera/main/clusters/webrtc-provider-manager.cpp
T

1112 lines
42 KiB
C++

/*
*
* 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 "webrtc-provider-manager.h"
#include "webrtc-kvs_esp_port.h"
#include "webrtc-kvs_esp_port_utils.h"
#include <app/server/Server.h>
#include <controller/InvokeInteraction.h>
#include <iostream>
#include <lib/support/logging/CHIPLogging.h>
#include <webrtc-transport.h>
#include <webrtc_bridge.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::WebRTCTransportProvider;
extern bool deferred_offer;
namespace {
// Constants
constexpr uint16_t kMaxConcurrentWebRTCSessions = 3;
} // namespace
void WebRTCProviderManager::SetCameraDevice(CameraDeviceInterface * aCameraDevice)
{
mCameraDevice = aCameraDevice;
}
void WebRTCProviderManager::Init()
{
ChipLogProgress(Camera, "Initializing WebRTC PeerConnection");
// Register our handler for signaling messages
webrtc_bridge_register_handler((webrtc_bridge_msg_cb_t) &webrtc_bridge_message_received_cb);
}
void WebRTCProviderManager::CloseConnection()
{
// Clean up all the Webrtc Transports
mWebrtcTransportMap.clear();
mSessionIdMap.clear();
}
void WebRTCProviderManager::SetWebRTCTransportProvider(WebRTCTransportProviderCluster * webRTCTransportProvider)
{
mWebRTCTransportProvider = webRTCTransportProvider;
}
CHIP_ERROR
WebRTCProviderManager::HandleSolicitOffer(const OfferRequestArgs & args, WebRTCSessionStruct & outSession, bool & outDeferredOffer)
{
// Initialize a new WebRTC session from the SolicitOfferRequestArgs
outSession.id = args.sessionId;
outSession.peerNodeID = args.peerNodeId;
outSession.peerEndpointID = args.originatingEndpointId;
outSession.streamUsage = args.streamUsage;
outSession.fabricIndex = args.fabricIndex;
uint16_t videoStreamID = 0;
uint16_t audioStreamID = 0;
// Resolve or allocate a VIDEO stream
if (args.videoStreamId.HasValue())
{
if (args.videoStreamId.Value().IsNull())
{
// TODO: Automatically select the closest matching video stream for the
// StreamUsage requested by looking at the and the server MAY allocate a
// new video stream if there are available resources.
}
else
{
outSession.videoStreamID = args.videoStreamId.Value();
videoStreamID = args.videoStreamId.Value().Value();
}
}
else
{
outSession.videoStreamID.SetNull();
}
// Resolve or allocate an AUDIO stream
if (args.audioStreamId.HasValue())
{
if (args.audioStreamId.Value().IsNull())
{
// TODO: Automatically select the closest matching audio stream for the
// StreamUsage requested and the server MAY allocate a new audio stream if
// there are available resources.
}
else
{
outSession.audioStreamID = args.audioStreamId.Value();
audioStreamID = args.audioStreamId.Value().Value();
}
}
else
{
outSession.audioStreamID.SetNull();
}
outDeferredOffer = deferred_offer;
ChipLogError(Camera, "Deferred Offer is set to: %d", outDeferredOffer);
WebrtcTransport * transport = GetTransport(args.sessionId);
WebrtcTransport::RequestArgs requestArgs;
requestArgs.sessionId = args.sessionId;
requestArgs.fabricIndex = args.fabricIndex;
requestArgs.peerNodeId = args.peerNodeId;
requestArgs.originatingEndpointId = args.originatingEndpointId;
requestArgs.videoStreamId = videoStreamID;
requestArgs.audioStreamId = audioStreamID;
requestArgs.peerId = ScopedNodeId(args.peerNodeId, args.fabricIndex);
if (transport == nullptr)
{
mWebrtcTransportMap[args.sessionId] = std::unique_ptr<WebrtcTransport>(new WebrtcTransport());
mSessionIdMap[ScopedNodeId(args.peerNodeId, args.fabricIndex)] = args.sessionId;
transport = mWebrtcTransportMap[args.sessionId].get();
transport->SetCallbacks(
[this](const std::string & sdp, SDPType type, const uint16_t sessionId) {
this->OnLocalDescription(sdp, type, sessionId);
},
[this](bool connected, const uint16_t sessionId) { this->OnConnectionStateChanged(connected, sessionId); });
}
transport->SetRequestArgs(requestArgs);
// Check resource availability before proceeding
// If we cannot allocate resources, send End command with OutOfResources
// reason
if (mWebrtcTransportMap.size() > kMaxConcurrentWebRTCSessions)
{
ChipLogProgress(Camera, "Resource exhaustion detected: maximum WebRTC sessions (%u)", kMaxConcurrentWebRTCSessions);
transport->SetCommandType(WebrtcTransport::CommandType::kEnd);
transport->MoveToState(WebrtcTransport::State::SendingEnd);
// The resource exhaustion happens internally in the DUT, but it still
// creates a session and then sends an End command with OutOfResources
// reason.
ScheduleEndSend(args.sessionId);
return CHIP_NO_ERROR;
}
transport->Start();
transport->AddTracks();
// Acquire the Video and Audio Streams from the CameraAVStreamManagement
// cluster and update the reference counts.
AcquireAudioVideoStreams(args.sessionId);
transport->MoveToState(WebrtcTransport::State::SendingOffer);
ChipLogProgress(Camera, "Generate and set the SDP");
if (transport->GetPeerConnection())
transport->GetPeerConnection()->CreateOffer(args.sessionId);
return CHIP_NO_ERROR;
}
void WebRTCProviderManager::RegisterWebrtcTransport(uint16_t sessionId)
{
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogProgress(Camera,
"WebRTC Transport is null for sessionId %u. Failed to "
"Register WebRTC Transport",
sessionId);
return;
}
WebrtcTransport::RequestArgs args = transport->GetRequestArgs();
}
void WebRTCProviderManager::UnregisterWebrtcTransport(uint16_t sessionId)
{
ChipLogProgress(Camera, "UnregisterWebrtcTransport called for sessionId: %u", sessionId);
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogProgress(Camera,
"WebRTC Transport is null for sessionId %u. Already "
"unregistered or not found",
sessionId);
return;
}
ChipLogProgress(Camera, "Successfully unregistered transport for sessionId: %u", sessionId);
}
CHIP_ERROR
WebRTCProviderManager::HandleProvideOffer(const ProvideOfferRequestArgs & args, WebRTCSessionStruct & outSession)
{
ChipLogError(Zcl, "Handle Provide Offer Delegate called");
// Initialize a new WebRTC session from the SolicitOfferRequestArgs
outSession.id = args.sessionId;
outSession.peerNodeID = args.peerNodeId;
outSession.peerEndpointID = args.originatingEndpointId;
outSession.streamUsage = args.streamUsage;
outSession.fabricIndex = args.fabricIndex;
uint16_t videoStreamID = 0;
uint16_t audioStreamID = 0;
// Resolve or allocate a VIDEO stream
if (args.videoStreamId.HasValue())
{
if (args.videoStreamId.Value().IsNull())
{
// TODO: Automatically select the closest matching video stream for the
// StreamUsage requested.
}
else
{
outSession.videoStreamID = args.videoStreamId.Value();
videoStreamID = args.videoStreamId.Value().Value();
}
}
else
{
outSession.videoStreamID.SetNull();
}
// Resolve or allocate an AUDIO stream
if (args.audioStreamId.HasValue())
{
if (args.audioStreamId.Value().IsNull())
{
// TODO: Automatically select the closest matching audio stream for the
// StreamUsage requested
}
else
{
outSession.audioStreamID = args.audioStreamId.Value();
audioStreamID = args.audioStreamId.Value().Value();
}
}
else
{
outSession.audioStreamID.SetNull();
}
// Process the SDP Offer, begin the ICE Candidate gathering phase, create the
// SDP Answer, and invoke Answer.
WebrtcTransport::RequestArgs requestArgs;
requestArgs.sessionId = args.sessionId;
requestArgs.fabricIndex = args.fabricIndex;
requestArgs.peerNodeId = args.peerNodeId;
requestArgs.originatingEndpointId = args.originatingEndpointId;
requestArgs.videoStreamId = videoStreamID;
requestArgs.audioStreamId = audioStreamID;
requestArgs.peerId = ScopedNodeId(args.peerNodeId, args.fabricIndex);
WebrtcTransport * transport = GetTransport(args.sessionId);
if (transport == nullptr)
{
mWebrtcTransportMap[args.sessionId] = std::unique_ptr<WebrtcTransport>(new WebrtcTransport());
mSessionIdMap[ScopedNodeId(args.peerNodeId, args.fabricIndex)] = args.sessionId;
transport = mWebrtcTransportMap[args.sessionId].get();
transport->SetCallbacks(
[this](const std::string & sdp, SDPType type, const uint16_t sessionId) {
this->OnLocalDescription(sdp, type, sessionId);
},
[this](bool connected, const uint16_t sessionId) { this->OnConnectionStateChanged(connected, sessionId); });
}
// Check resource availability before proceeding
// If we cannot allocate resources, respond with a response status of
// RESOURCE_EXHAUSTED
if (mWebrtcTransportMap.size() > kMaxConcurrentWebRTCSessions)
{
ChipLogProgress(Camera,
"Resource exhaustion detected in ProvideOffer: maximum "
"WebRTC sessions (%u)",
kMaxConcurrentWebRTCSessions);
return CHIP_IM_GLOBAL_STATUS(ResourceExhausted);
}
transport->SetRequestArgs(requestArgs);
transport->Start();
transport->AddTracks();
// Acquire the Video and Audio Streams from the CameraAVStreamManagement
// cluster and update the reference counts.
AcquireAudioVideoStreams(args.sessionId);
transport->MoveToState(WebrtcTransport::State::SendingAnswer);
if (transport->GetPeerConnection())
{
transport->GetPeerConnection()->SetRemoteDescription(args.sdp, SDPType::Offer);
transport->GetPeerConnection()->CreateAnswer();
}
return CHIP_NO_ERROR;
}
CHIP_ERROR
WebRTCProviderManager::HandleProvideAnswer(uint16_t sessionId, const std::string & sdpAnswer)
{
ChipLogProgress(Camera, "HandleProvideAnswer called with sessionId: %u", sessionId);
// Check if the provided sessionId matches your current sessions
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "Session ID %u does not match the current sessions", sessionId);
return CHIP_ERROR_INVALID_ARGUMENT;
}
if (sdpAnswer.empty())
{
ChipLogError(Camera, "Provided SDP Answer is empty for session ID %u", sessionId);
return CHIP_ERROR_INVALID_ARGUMENT;
}
if (!transport->GetPeerConnection())
{
ChipLogError(Camera,
"Cannot set remote description: mPeerConnection is null for "
"session ID %u",
sessionId);
return CHIP_ERROR_INCORRECT_STATE;
}
transport->GetPeerConnection()->SetRemoteDescription(sdpAnswer, SDPType::Answer);
transport->MoveToState(WebrtcTransport::State::SendingICECandidates);
ScheduleICECandidatesSend(sessionId);
return CHIP_NO_ERROR;
}
CHIP_ERROR WebRTCProviderManager::HandleProvideICECandidates(uint16_t sessionId, const std::vector<ICECandidateStruct> & candidates)
{
ChipLogProgress(Camera, "HandleProvideICECandidates called with sessionId: %u", sessionId);
// Check if the provided sessionId matches your current sessions
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "Session ID %u does not match the current sessions", sessionId);
return CHIP_ERROR_INVALID_ARGUMENT;
}
if (!transport->GetPeerConnection())
{
ChipLogError(Camera,
"Cannot process ICE candidates: PeerConnection is null for "
"session ID %u",
sessionId);
return CHIP_ERROR_INCORRECT_STATE;
}
if (candidates.empty())
{
ChipLogError(Camera, "Candidate list is empty. At least one candidate is expected.");
return CHIP_ERROR_INVALID_ARGUMENT;
}
for (const auto & candidate : candidates)
{
ChipLogProgress(Camera, "Applying candidate: %s",
std::string(candidate.candidate.begin(), candidate.candidate.end()).c_str());
std::string mid =
candidate.SDPMid.IsNull() ? "" : std::string(candidate.SDPMid.Value().begin(), candidate.SDPMid.Value().end());
transport->AddRemoteCandidate(std::string(candidate.candidate.begin(), candidate.candidate.end()), mid);
}
// Schedule sending Ice Candidates when remote candidates are received. This
// keeps the exchange simple
transport->MoveToState(WebrtcTransport::State::SendingICECandidates);
ScheduleICECandidatesSend(sessionId);
return CHIP_NO_ERROR;
}
CHIP_ERROR WebRTCProviderManager::HandleEndSession(uint16_t sessionId, WebRTCEndReasonEnum reasonCode,
DataModel::Nullable<uint16_t> videoStreamID,
DataModel::Nullable<uint16_t> audioStreamID)
{
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "Session ID %u does not match the current sessions", sessionId);
return CHIP_ERROR_INVALID_ARGUMENT;
}
if (transport != nullptr)
{
ChipLogProgress(Camera, "Delete Webrtc Transport for the session: %u", sessionId);
// Release the Video and Audio Streams from the CameraAVStreamManagement
// cluster and update the reference counts.
// TODO: Lookup the sessionID to get the Video/Audio StreamID
ReleaseAudioVideoStreams(sessionId);
UnregisterWebrtcTransport(sessionId);
mWebrtcTransportMap.erase(sessionId);
WebrtcTransport::RequestArgs args = transport->GetRequestArgs();
mSessionIdMap.erase(ScopedNodeId(args.peerNodeId, args.fabricIndex));
}
if (transport->ClosePeerConnection())
{
ChipLogProgress(Camera, "Closing peer connection: %u", sessionId);
}
return CHIP_NO_ERROR;
}
CHIP_ERROR
WebRTCProviderManager::ValidateStreamUsage(StreamUsageEnum streamUsage, Optional<DataModel::Nullable<uint16_t>> & videoStreamId,
Optional<DataModel::Nullable<uint16_t>> & audioStreamId)
{
if (mCameraDevice == nullptr)
{
ChipLogError(Camera, "CameraDeviceInterface not initialized");
return CHIP_ERROR_INCORRECT_STATE;
}
auto & avsmController = mCameraDevice->GetCameraAVStreamMgmtController();
return avsmController.ValidateStreamUsage(streamUsage, videoStreamId, audioStreamId);
}
CHIP_ERROR
WebRTCProviderManager::ValidateVideoStreamID(uint16_t videoStreamId)
{
if (mCameraDevice == nullptr)
{
ChipLogError(Camera, "CameraDeviceInterface not initialized");
return CHIP_ERROR_INCORRECT_STATE;
}
auto & avsmController = mCameraDevice->GetCameraAVStreamMgmtController();
return avsmController.ValidateVideoStreamID(videoStreamId);
}
CHIP_ERROR
WebRTCProviderManager::ValidateAudioStreamID(uint16_t audioStreamId)
{
if (mCameraDevice == nullptr)
{
ChipLogError(Camera, "CameraDeviceInterface not initialized");
return CHIP_ERROR_INCORRECT_STATE;
}
auto & avsmController = mCameraDevice->GetCameraAVStreamMgmtController();
return avsmController.ValidateAudioStreamID(audioStreamId);
}
CHIP_ERROR
WebRTCProviderManager::IsStreamUsageSupported(StreamUsageEnum streamUsage)
{
if (mCameraDevice == nullptr)
{
ChipLogError(Camera, "CameraDeviceInterface not initialized");
return CHIP_ERROR_INCORRECT_STATE;
}
auto & hal = mCameraDevice->GetCameraHALInterface();
auto & streamUsagePriorities = hal.GetStreamUsagePriorities();
// Check if the streamUsage is in the StreamUsagePriorities list
for (const auto & usage : streamUsagePriorities)
{
if (usage == streamUsage)
{
return CHIP_NO_ERROR;
}
}
ChipLogError(Camera, "StreamUsage %u not found in StreamUsagePriorities", to_underlying(streamUsage));
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR WebRTCProviderManager::IsHardPrivacyModeActive(bool & isActive)
{
if (mCameraDevice == nullptr)
{
ChipLogError(Camera, "CameraDeviceInterface not initialized");
return CHIP_ERROR_INCORRECT_STATE;
}
auto & avsmController = mCameraDevice->GetCameraAVStreamMgmtController();
return avsmController.IsHardPrivacyModeActive(isActive);
}
CHIP_ERROR
WebRTCProviderManager::IsSoftRecordingPrivacyModeActive(bool & isActive)
{
if (mCameraDevice == nullptr)
{
ChipLogError(Camera, "CameraDeviceInterface not initialized");
return CHIP_ERROR_INCORRECT_STATE;
}
auto & avsmController = mCameraDevice->GetCameraAVStreamMgmtController();
return avsmController.IsSoftRecordingPrivacyModeActive(isActive);
}
CHIP_ERROR
WebRTCProviderManager::IsSoftLivestreamPrivacyModeActive(bool & isActive)
{
if (mCameraDevice == nullptr)
{
ChipLogError(Camera, "CameraDeviceInterface not initialized");
return CHIP_ERROR_INCORRECT_STATE;
}
auto & avsmController = mCameraDevice->GetCameraAVStreamMgmtController();
return avsmController.IsSoftLivestreamPrivacyModeActive(isActive);
}
bool WebRTCProviderManager::HasAllocatedVideoStreams()
{
if (mCameraDevice == nullptr)
{
ChipLogError(Camera, "CameraDeviceInterface not initialized");
return false;
}
auto & avsmController = mCameraDevice->GetCameraAVStreamMgmtController();
return avsmController.HasAllocatedVideoStreams();
}
bool WebRTCProviderManager::HasAllocatedAudioStreams()
{
if (mCameraDevice == nullptr)
{
ChipLogError(Camera, "CameraDeviceInterface not initialized");
return false;
}
auto & avsmController = mCameraDevice->GetCameraAVStreamMgmtController();
return avsmController.HasAllocatedAudioStreams();
}
CHIP_ERROR WebRTCProviderManager::ValidateSFrameConfig(uint16_t cipherSuite, size_t baseKeyLength)
{
// Define supported cipher suites and their expected key lengths
// Based on SFrame RFC:
// https://datatracker.ietf.org/doc/html/draft-ietf-sframe-enc 0x0001:
// AES-128-GCM-SHA256 (16 byte key) 0x0002: AES-256-GCM-SHA512 (32 byte key)
constexpr uint16_t kCipherSuite_AES_128_GCM = 0x0001;
constexpr uint16_t kCipherSuite_AES_256_GCM = 0x0002;
constexpr size_t kAES_128_KeyLength = 16;
constexpr size_t kAES_256_KeyLength = 32;
size_t expectedKeyLength = 0;
// Validate cipher suite and determine expected key length
switch (cipherSuite)
{
case kCipherSuite_AES_128_GCM:
expectedKeyLength = kAES_128_KeyLength;
break;
case kCipherSuite_AES_256_GCM:
expectedKeyLength = kAES_256_KeyLength;
break;
default:
ChipLogError(Camera, "Unsupported SFrame cipher suite 0x%04X", cipherSuite);
return CHIP_ERROR_INVALID_ARGUMENT;
}
// Validate base key length matches the expected length for the cipher suite
if (baseKeyLength != expectedKeyLength)
{
ChipLogError(Camera,
"SFrame base key length mismatch - expected %u bytes for "
"cipher suite 0x%04X, got %u bytes",
static_cast<unsigned int>(expectedKeyLength), cipherSuite, static_cast<unsigned int>(baseKeyLength));
return CHIP_ERROR_INVALID_ARGUMENT;
}
return CHIP_NO_ERROR;
}
CHIP_ERROR WebRTCProviderManager::IsUTCTimeNull(bool & isNull)
{
// TODO: The implementation SHALL:
// - Read the UTCTime attribute from the Time Synchronization cluster
// (0x0038)
// - Return whether the attribute is null or has a valid value
return CHIP_NO_ERROR;
}
void WebRTCProviderManager::ScheduleOfferSend(uint16_t sessionId)
{
ChipLogProgress(Camera, "ScheduleOfferSend called.");
DeviceLayer::SystemLayer().ScheduleLambda([this, sessionId]() {
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
return;
}
WebrtcTransport::RequestArgs args = transport->GetRequestArgs();
ChipLogProgress(Camera, "Sending Offer command to node " ChipLogFormatX64, ChipLogValueX64(args.peerNodeId));
transport->SetCommandType(WebrtcTransport::CommandType::kOffer);
// Attempt to find or establish a CASE session to the target PeerId.
CASESessionManager * caseSessionMgr = Server::GetInstance().GetCASESessionManager();
VerifyOrDie(caseSessionMgr != nullptr);
// WebRTC Answer requires a large payload session establishment.
caseSessionMgr->FindOrEstablishSession(args.peerId, &mOnConnectedCallback, &mOnConnectionFailureCallback,
TransportPayloadCapability::kLargePayload);
});
}
void WebRTCProviderManager::ScheduleAnswerSend(uint16_t sessionId)
{
ChipLogProgress(Camera, "ScheduleAnswerSend called.");
DeviceLayer::SystemLayer().ScheduleLambda([this, sessionId]() {
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
return;
}
WebrtcTransport::RequestArgs requestArgs = transport->GetRequestArgs();
chip::ScopedNodeId peerId = requestArgs.peerId;
ChipLogProgress(Camera, "Sending Answer command to node " ChipLogFormatX64, ChipLogValueX64(peerId.GetNodeId()));
transport->SetCommandType(WebrtcTransport::CommandType::kAnswer);
// Attempt to find or establish a CASE session to the target PeerId.
CASESessionManager * caseSessionMgr = Server::GetInstance().GetCASESessionManager();
VerifyOrDie(caseSessionMgr != nullptr);
// WebRTC Answer requires a large payload session establishment.
caseSessionMgr->FindOrEstablishSession(peerId, &mOnConnectedCallback, &mOnConnectionFailureCallback,
TransportPayloadCapability::kLargePayload);
});
}
void WebRTCProviderManager::ScheduleEndSend(uint16_t sessionId)
{
ChipLogProgress(Camera, "ScheduleEndSend called.");
DeviceLayer::SystemLayer().ScheduleLambda([this, sessionId]() {
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
return;
}
WebrtcTransport::RequestArgs requestArgs = transport->GetRequestArgs();
chip::ScopedNodeId peerId = requestArgs.peerId;
ChipLogProgress(Camera, "Sending End command to node " ChipLogFormatX64, ChipLogValueX64(peerId.GetNodeId()));
transport->SetCommandType(WebrtcTransport::CommandType::kEnd);
// Attempt to find or establish a CASE session to the target PeerId.
CASESessionManager * caseSessionMgr = Server::GetInstance().GetCASESessionManager();
VerifyOrDie(caseSessionMgr != nullptr);
// WebRTC Answer requires a large payload session establishment.
caseSessionMgr->FindOrEstablishSession(peerId, &mOnConnectedCallback, &mOnConnectionFailureCallback,
TransportPayloadCapability::kLargePayload);
});
}
void WebRTCProviderManager::ScheduleICECandidatesSend(uint16_t sessionId)
{
ChipLogProgress(Camera, "ScheduleICECandidatesSend called.");
DeviceLayer::SystemLayer().ScheduleLambda([this, sessionId]() {
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
return;
}
WebrtcTransport::RequestArgs requestArgs = transport->GetRequestArgs();
chip::ScopedNodeId peerId = requestArgs.peerId;
ChipLogProgress(Camera, "Sending ICECandidates command to node " ChipLogFormatX64, ChipLogValueX64(peerId.GetNodeId()));
transport->SetCommandType(WebrtcTransport::CommandType::kICECandidates);
// Attempt to find or establish a CASE session to the target PeerId.
CASESessionManager * caseSessionMgr = Server::GetInstance().GetCASESessionManager();
VerifyOrDie(caseSessionMgr != nullptr);
// WebRTC Answer requires a large payload session establishment.
caseSessionMgr->FindOrEstablishSession(peerId, &mOnConnectedCallback, &mOnConnectionFailureCallback,
TransportPayloadCapability::kLargePayload);
});
}
void WebRTCProviderManager::OnDeviceConnected(void * context, Messaging::ExchangeManager & exchangeMgr,
const SessionHandle & sessionHandle)
{
WebRTCProviderManager * self = reinterpret_cast<WebRTCProviderManager *>(context);
VerifyOrReturn(self != nullptr, ChipLogError(Camera, "OnDeviceConnected:: context is null"));
// Derive sessionId from sessionHandle by looking up the peer ScopedNodeId
// (NodeId + FabricIndex)
ScopedNodeId peerScopedNodeId = sessionHandle->GetPeer();
auto sessionIt = self->mSessionIdMap.find(peerScopedNodeId);
if (sessionIt == self->mSessionIdMap.end())
{
ChipLogError(Camera,
"OnDeviceConnected:: no session found for peer ScopedNodeId: "
"[%d:" ChipLogFormatX64 "]",
peerScopedNodeId.GetFabricIndex(), ChipLogValueX64(peerScopedNodeId.GetNodeId()));
return;
}
uint16_t sessionId = sessionIt->second;
WebrtcTransport * transport = self->GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "OnDeviceConnected:: transport not found for sessionId: %u", sessionId);
return;
}
ChipLogProgress(Camera,
"CASE session established, sending command with Command "
"Type: %d, for sessionID: %u",
static_cast<int>(transport->GetCommandType()), sessionId);
CHIP_ERROR err = CHIP_NO_ERROR;
switch (transport->GetCommandType())
{
case WebrtcTransport::CommandType::kOffer:
err = self->SendOfferCommand(exchangeMgr, sessionHandle, sessionId);
transport->MoveToState(WebrtcTransport::State::Idle);
break;
case WebrtcTransport::CommandType::kAnswer:
err = self->SendAnswerCommand(exchangeMgr, sessionHandle, sessionId);
transport->MoveToState(WebrtcTransport::State::Idle);
break;
case WebrtcTransport::CommandType::kICECandidates:
err = self->SendICECandidatesCommand(exchangeMgr, sessionHandle, sessionId);
transport->MoveToState(WebrtcTransport::State::Idle);
break;
case WebrtcTransport::CommandType::kEnd: {
// Determine the end reason - check if it's due to privacy mode or resource
// exhaustion
WebRTCEndReasonEnum endReason = WebRTCEndReasonEnum::kOutOfResources;
if (self->mSoftLiveStreamPrivacyEnabled)
{
endReason = WebRTCEndReasonEnum::kPrivacyMode;
}
err = self->SendEndCommand(exchangeMgr, sessionHandle, sessionId, endReason);
// Release the Video and Audio Streams from the CameraAVStreamManagement
// cluster and update the reference counts.
self->ReleaseAudioVideoStreams(sessionId);
self->UnregisterWebrtcTransport(sessionId);
WebrtcTransport::RequestArgs args = transport->GetRequestArgs();
self->mSessionIdMap.erase(ScopedNodeId(args.peerNodeId, args.fabricIndex));
transport->MoveToState(WebrtcTransport::State::Idle);
// remove from current sessions list
self->mWebRTCTransportProvider->RemoveSession(sessionId);
self->mWebrtcTransportMap.erase(sessionId);
break;
}
default:
err = CHIP_ERROR_INVALID_ARGUMENT;
break;
}
if (err != CHIP_NO_ERROR)
{
ChipLogError(Camera, "OnDeviceConnected::SendCommand failed: %" CHIP_ERROR_FORMAT, err.Format());
}
}
void WebRTCProviderManager::OnDeviceConnectionFailure(void * context, const ScopedNodeId & peerId, CHIP_ERROR err)
{
LogErrorOnFailure(err);
WebRTCProviderManager * self = reinterpret_cast<WebRTCProviderManager *>(context);
VerifyOrReturn(self != nullptr, ChipLogError(Camera, "OnDeviceConnectionFailure: context is null"));
}
WebrtcTransport * WebRTCProviderManager::GetTransport(uint16_t sessionId)
{
WebrtcTransport * transport = nullptr;
if (mWebrtcTransportMap.find(sessionId) != mWebrtcTransportMap.end())
{
transport = mWebrtcTransportMap[sessionId].get();
}
return transport;
}
void WebRTCProviderManager::LiveStreamPrivacyModeChanged(bool privacyModeEnabled)
{
mSoftLiveStreamPrivacyEnabled = privacyModeEnabled;
if (privacyModeEnabled)
{
WebrtcTransport * transport = nullptr;
uint16_t sessionId = 0;
for (auto & mapEntry : mWebrtcTransportMap)
{
sessionId = mapEntry.first;
transport = (WebrtcTransport *) mapEntry.second.get();
if (transport == nullptr)
{
continue;
}
transport->MoveToState(WebrtcTransport::State::SendingEnd);
ScheduleEndSend(sessionId);
}
}
else
{
ChipLogProgress(Camera, "Privacy mode is disabled");
}
}
CHIP_ERROR
WebRTCProviderManager::SendOfferCommand(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle,
uint16_t sessionId)
{
auto onSuccess = [](const ConcreteCommandPath & commandPath, const StatusIB & status, const auto & dataResponse) {
ChipLogProgress(Camera, "Offer command succeeds");
};
auto onFailure = [](CHIP_ERROR error) { ChipLogError(Camera, "Offer command failed: %" CHIP_ERROR_FORMAT, error.Format()); };
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "SendOfferCommand failed, WebTransport not found for sessionId: %u", sessionId);
return CHIP_ERROR_INTERNAL;
}
// Build the command
WebRTCTransportRequestor::Commands::Offer::Type command;
command.webRTCSessionID = sessionId;
std::string localSdp = transport->GetLocalDescription();
command.sdp = CharSpan::fromCharString(localSdp.c_str());
WebrtcTransport::RequestArgs args = transport->GetRequestArgs();
// Now invoke the command using the found session handle
return Controller::InvokeCommandRequest(&exchangeMgr, sessionHandle, args.originatingEndpointId, command, onSuccess, onFailure,
/* timedInvokeTimeoutMs = */ NullOptional,
/* responseTimeout = */ NullOptional,
/* outCancelFn = */ nullptr, /*allowLargePayload = */ true);
}
void WebRTCProviderManager::OnLocalDescription(const std::string & sdp, SDPType type, const uint16_t sessionId)
{
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "SendOfferCommand failed, WebTransport not found for sessionId: %u", sessionId);
return;
}
WebrtcTransport::State state = transport->GetState();
if (state == WebrtcTransport::State::SendingAnswer && type != SDPType::Answer)
{
return;
}
const char * typeStr = (type == SDPType::Offer) ? "offer" : "answer";
std::string localSdp = sdp;
ChipLogProgress(Camera, "Local Description (%s):", typeStr);
ChipLogProgress(Camera, "%s", localSdp.c_str());
ChipLogProgress(Camera, "Size of SDP: %zu", localSdp.size());
switch (state)
{
case WebrtcTransport::State::SendingOffer:
ScheduleOfferSend(sessionId);
break;
case WebrtcTransport::State::SendingAnswer:
ScheduleAnswerSend(sessionId);
break;
default:
break;
}
}
void WebRTCProviderManager::OnConnectionStateChanged(bool connected, const uint16_t sessionId)
{
ChipLogProgress(Camera, "Connection state changed for session %u: %s", sessionId, connected ? "connected" : "disconnected");
if (connected)
{
RegisterWebrtcTransport(sessionId);
}
else
{
// Schedule cleanup on Matter thread to ensure proper locking when calling
// RemoveSession. Safe to capture 'this' by value: WebRTCProviderManager is
// a member of the global CameraDevice object which has static storage
// duration and lives for the entire program lifetime.
DeviceLayer::SystemLayer().ScheduleLambda([this, sessionId]() {
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogProgress(Camera,
"Transport not found for session %u during disconnect; "
"session may have already been cleaned up",
sessionId);
return;
}
// Connection was closed/disconnected by the peer - clean up the session
ChipLogProgress(Camera, "Peer connection closed for session %u, cleaning up resources", sessionId);
// Release the Video and Audio Streams from the CameraAVStreamManagement
// cluster and update the reference counts.
ReleaseAudioVideoStreams(sessionId);
// Capture args before unregistering in case the transport is invalidated
WebrtcTransport::RequestArgs args = transport->GetRequestArgs();
// Unregister the transport from the media controller
UnregisterWebrtcTransport(sessionId);
// Remove from session maps
mSessionIdMap.erase(ScopedNodeId(args.peerNodeId, args.fabricIndex));
// Remove from current sessions list in the WebRTC Transport Provider
// This MUST be called on the Matter thread with the stack lock held
if (mWebRTCTransportProvider != nullptr)
{
mWebRTCTransportProvider->RemoveSession(sessionId);
}
// Finally, remove and destroy the transport
mWebrtcTransportMap.erase(sessionId);
ChipLogProgress(Camera, "Session %u cleanup completed", sessionId);
});
}
}
CHIP_ERROR WebRTCProviderManager::SendAnswerCommand(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle,
uint16_t sessionId)
{
auto onSuccess = [](const ConcreteCommandPath & commandPath, const StatusIB & status, const auto & dataResponse) {
ChipLogProgress(Camera, "Answer command succeeds");
};
auto onFailure = [](CHIP_ERROR error) { ChipLogError(Camera, "Answer command failed: %" CHIP_ERROR_FORMAT, error.Format()); };
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "SendOfferCommand failed, WebTransport not found for sessionId: %u", sessionId);
return CHIP_ERROR_INTERNAL;
}
// Build the command
WebRTCTransportRequestor::Commands::Answer::Type command;
command.webRTCSessionID = sessionId;
std::string localSdp = transport->GetLocalDescription();
command.sdp = CharSpan::fromCharString(localSdp.c_str());
WebrtcTransport::RequestArgs requestArgs = transport->GetRequestArgs();
// Now invoke the command using the found session handle
return Controller::InvokeCommandRequest(&exchangeMgr, sessionHandle, requestArgs.originatingEndpointId, command, onSuccess,
onFailure,
/* timedInvokeTimeoutMs = */ NullOptional,
/* responseTimeout = */ NullOptional,
/* outCancelFn = */ nullptr, /*allowLargePayload = */ true);
}
CHIP_ERROR WebRTCProviderManager::SendICECandidatesCommand(Messaging::ExchangeManager & exchangeMgr,
const SessionHandle & sessionHandle, uint16_t sessionId)
{
auto onSuccess = [](const ConcreteCommandPath & commandPath, const StatusIB & status, const auto & dataResponse) {
ChipLogProgress(Camera, "ICECandidates command succeeds");
};
auto onFailure = [](CHIP_ERROR error) {
ChipLogError(Camera, "ICECandidates command failed: %" CHIP_ERROR_FORMAT, error.Format());
};
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "WebTransport not found for the sessionId: %u", sessionId);
return CHIP_ERROR_INTERNAL;
}
std::vector<std::string> localCandidates = transport->GetCandidates();
// Build the command
WebRTCTransportRequestor::Commands::ICECandidates::Type command;
if (localCandidates.empty())
{
ChipLogError(Camera, "No local ICE candidates to send");
return CHIP_ERROR_INCORRECT_STATE;
}
std::vector<ICECandidateStruct> iceCandidateStructList;
for (const auto & candidate : localCandidates)
{
ICECandidateStruct iceCandidate = { CharSpan::fromCharString(candidate.c_str()) };
iceCandidateStructList.push_back(iceCandidate);
}
command.webRTCSessionID = sessionId;
command.ICECandidates = DataModel::List<const ICECandidateStruct>(iceCandidateStructList.data(), iceCandidateStructList.size());
WebrtcTransport::RequestArgs requestArgs = transport->GetRequestArgs();
// Now invoke the command using the found session handle
return Controller::InvokeCommandRequest(&exchangeMgr, sessionHandle, requestArgs.originatingEndpointId, command, onSuccess,
onFailure,
/* timedInvokeTimeoutMs = */ NullOptional,
/* responseTimeout = */ NullOptional,
/* outCancelFn = */ nullptr, /*allowLargePayload = */ true);
}
CHIP_ERROR WebRTCProviderManager::SendEndCommand(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle,
uint16_t sessionId, WebRTCEndReasonEnum endReason)
{
auto onSuccess = [](const ConcreteCommandPath & commandPath, const StatusIB & status, const auto & dataResponse) {
ChipLogProgress(Camera, "End command succeeds");
};
auto onFailure = [](CHIP_ERROR error) { ChipLogError(Camera, "End command failed: %" CHIP_ERROR_FORMAT, error.Format()); };
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "WebTransport not found for the sessionId: %u", sessionId);
return CHIP_ERROR_INTERNAL;
}
// Build the command
WebRTCTransportRequestor::Commands::End::Type command;
command.webRTCSessionID = sessionId;
command.reason = endReason;
WebrtcTransport::RequestArgs requestArgs = transport->GetRequestArgs();
// Now invoke the command using the found session handle
return Controller::InvokeCommandRequest(&exchangeMgr, sessionHandle, requestArgs.originatingEndpointId, command, onSuccess,
onFailure,
/* timedInvokeTimeoutMs = */ NullOptional,
/* responseTimeout = */ NullOptional,
/* outCancelFn = */ nullptr, /*allowLargePayload = */ true);
}
CHIP_ERROR WebRTCProviderManager::AcquireAudioVideoStreams(uint16_t sessionId)
{
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "WebTransport not found for the sessionId: %u", sessionId);
return CHIP_ERROR_INTERNAL;
}
WebrtcTransport::RequestArgs args = transport->GetRequestArgs();
return mCameraDevice->GetCameraAVStreamMgmtDelegate().OnTransportAcquireAudioVideoStreams(args.audioStreamId,
args.videoStreamId);
}
CHIP_ERROR WebRTCProviderManager::ReleaseAudioVideoStreams(uint16_t sessionId)
{
WebrtcTransport * transport = GetTransport(sessionId);
if (transport == nullptr)
{
ChipLogError(Camera, "WebTransport not found for the sessionId: %u", sessionId);
return CHIP_ERROR_INTERNAL;
}
WebrtcTransport::RequestArgs args = transport->GetRequestArgs();
// TODO: Use passed in audio/video stream ids corresponding to a sessionId.
return mCameraDevice->GetCameraAVStreamMgmtDelegate().OnTransportReleaseAudioVideoStreams(args.audioStreamId,
args.videoStreamId);
}