Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 57 additions & 9 deletions core/deviceDrivers/matter/sbmd/specs/camera.sbmd.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@ function parseSessions(sessionsJson) {
}

function findStreamingSessionId(sessions) {
if (sessions === null || sessions === undefined)
{
return null;
}
Comment on lines 338 to +342

// Return the id of the (single) session in the 'streaming' state, or null if there is none.
for (var id in sessions) {
if (sessions[id].state === 'streaming') {
Expand All @@ -346,6 +351,34 @@ function findStreamingSessionId(sessions) {
return null;
}

// Return the session id whose stored webRTCSessionID matches an incoming
// WebRTCTransportRequestor command. Use this when the command already carries a
// webRTCSessionID so the resource update event can be attributed to the exact
// session, which matters when multiple clients are active and streaming-state
// inference would be ambiguous.
function findSessionIdByWebRTCSessionID(sessions, webRTCSessionID)
{
if (webRTCSessionID === undefined || webRTCSessionID === null)
{
return null;
}

if (sessions === null || sessions === undefined)
{
return null;
}

for (var id in sessions)
{
if (sessions[id].webRTCSessionID === webRTCSessionID)
{
return id;
}
}

return null;
}

function executeCreateSession(args) {
var sessionsJson = args.supplements.transientData[TD_SESSIONS];
var sessions = parseSessions(sessionsJson);
Expand Down Expand Up @@ -929,25 +962,25 @@ function handleIncomingOffer(args) {
// Store the Matter webRTCSessionID for correlation
var sessionsJson = args.supplements.transientData[TD_SESSIONS];
var sessions = parseSessions(sessionsJson);
var sessionId = findStreamingSessionId(sessions);
var metadata = {sessionId: sessionId || 'unknown'};

Comment on lines 963 to 967
if (sessions === null) {
// Corrupt session data: reset it, but still surface the remote SDP to the client.
return Sbmd.result()
.storage.setTransientData(TD_SESSIONS, '', 0)
.dataModel.updateResource(EP_WEBRTC, 'remoteSdp', sdp.toString())
.dataModel.updateResource(EP_WEBRTC, 'remoteSdp', sdp.toString(), metadata)
.success();
}

// Find the active streaming session and associate the Matter session ID.
var sessionId = findStreamingSessionId(sessions);

if (sessionId) {
sessions[sessionId].webRTCSessionID = webRTCSessionID;
}

return Sbmd.result()
.storage.setTransientData(TD_SESSIONS, JSON.stringify(sessions), ONE_HOUR_SECS)
.dataModel.updateResource(EP_WEBRTC, 'remoteSdp', sdp.toString())
.dataModel.updateResource(EP_WEBRTC, 'remoteSdp', sdp.toString(), metadata)
.success();
}

Expand All @@ -968,11 +1001,13 @@ function handleIncomingAnswer(args) {
return Sbmd.result().error('Answer command missing SDP');
}

// Store the camera-allocated webRTCSessionID for use by subsequent commands
// (ProvideICECandidates, EndSession)
var sessionsJson = args.supplements.transientData[TD_SESSIONS];
var sessions = parseSessions(sessionsJson);
var sessionId = findStreamingSessionId(sessions);
var metadata = {sessionId: sessionId || 'unknown'};
Comment on lines 1004 to +1007

// Store the camera-allocated webRTCSessionID for use by subsequent commands
// (ProvideICECandidates, EndSession)
if (sessions && webRTCSessionID !== undefined && webRTCSessionID !== null) {
var answerSessionId = findStreamingSessionId(sessions);

Expand All @@ -983,7 +1018,7 @@ function handleIncomingAnswer(args) {

return Sbmd.result()
.storage.setTransientData(TD_SESSIONS, JSON.stringify(sessions || {}), ONE_HOUR_SECS)
.dataModel.updateResource(EP_WEBRTC, 'remoteSdp', sdp.toString())
.dataModel.updateResource(EP_WEBRTC, 'remoteSdp', sdp.toString(), metadata)
.success();
}

Expand All @@ -997,12 +1032,19 @@ function handleIncomingIceCandidates(args) {
var decoded = Sbmd.Tlv.decode(tlvBase64);

// ICECandidates fields: webRTCSessionID (tag 0), ICECandidates (tag 1, array of structs)
var webRTCSessionID = decoded[0];
var candidateStructs = decoded[1];

if (!candidateStructs || !Array.isArray(candidateStructs)) {
return Sbmd.result().error('ICECandidates command missing candidates array');
}

var sessionsJson = args.supplements.transientData[TD_SESSIONS];
var sessions = parseSessions(sessionsJson);
var sessionId = findSessionIdByWebRTCSessionID(sessions, webRTCSessionID);

var metadata = {sessionId: sessionId || 'unknown'};

// Extract candidate strings from ICECandidateStruct array
// Each struct has: candidate (tag 0), SDPMid (tag 1), SDPMLineIndex (tag 2)
var candidates = [];
Expand All @@ -1013,18 +1055,21 @@ function handleIncomingIceCandidates(args) {
}

return Sbmd.result()
.dataModel.updateResource(EP_WEBRTC, 'remoteIceCandidates', JSON.stringify(candidates))
.dataModel.updateResource(EP_WEBRTC, 'remoteIceCandidates', JSON.stringify(candidates), metadata)
.success();
}

function handleIncomingEndSession(args) {
var tlvBase64 = args.command.tlvBase64;
var reason = 12; // UnknownReason default
var webRTCSessionID = null;

if (tlvBase64) {
var decoded = Sbmd.Tlv.decode(tlvBase64);

// End fields: webRTCSessionID (tag 0), reason (tag 1)
webRTCSessionID = decoded[0];

if (decoded[1] !== undefined) {
reason = decoded[1];
}
Expand All @@ -1046,7 +1091,10 @@ function handleIncomingEndSession(args) {
.success();
}

var sessionId = findStreamingSessionId(sessions);
// Use the camera-provided webRTCSessionID so the ended event is attributed to the exact
// session that the camera is ending. If we cannot resolve that id, still publish the ended
// event with sessionId='unknown' rather than guessing from the current streaming state.
var sessionId = findSessionIdByWebRTCSessionID(sessions, webRTCSessionID);

if (sessionId) {
delete sessions[sessionId];
Expand Down
44 changes: 40 additions & 4 deletions core/test/src/SbmdCameraWebrtcTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,9 @@ namespace
InvokeCommandHandler("handleIncomingOffer", CL_WEBRTC_TRANSPORT_REQUESTOR, CMD_OFFER, tlv, sessions);

ExpectSuccess(result);
ExpectUpdateResource(*result, "remoteSdp", "remote-offer-sdp");
const auto *ur = ExpectUpdateResource(*result, "remoteSdp", "remote-offer-sdp");
ASSERT_TRUE(ur->metadata.has_value());
EXPECT_TRUE(ur->metadata->find("\"sessionId\":\"1\"") != std::string::npos);
}

TEST_F(SbmdCameraWebrtcTest, HandleIncomingAnswerUpdatesRemoteSdp)
Expand All @@ -782,7 +784,9 @@ namespace
InvokeCommandHandler("handleIncomingAnswer", CL_WEBRTC_TRANSPORT_REQUESTOR, CMD_ANSWER, tlv, sessions);

ExpectSuccess(result);
ExpectUpdateResource(*result, "remoteSdp", "remote-answer-sdp");
const auto *ur = ExpectUpdateResource(*result, "remoteSdp", "remote-answer-sdp");
ASSERT_TRUE(ur->metadata.has_value());
EXPECT_TRUE(ur->metadata->find("\"sessionId\":\"1\"") != std::string::npos);
}

TEST_F(SbmdCameraWebrtcTest, HandleIncomingAnswerStoresWebRTCSessionID)
Expand Down Expand Up @@ -812,7 +816,24 @@ namespace
"handleIncomingIceCandidates", CL_WEBRTC_TRANSPORT_REQUESTOR, CMD_ICE_CANDIDATES, tlv, sessions);

ExpectSuccess(result);
ExpectUpdateResource(*result, "remoteIceCandidates");
const auto *ur = ExpectUpdateResource(*result, "remoteIceCandidates");
ASSERT_TRUE(ur->metadata.has_value());
EXPECT_TRUE(ur->metadata->find("\"sessionId\":\"1\"") != std::string::npos);
}

TEST_F(SbmdCameraWebrtcTest, HandleIncomingIceCandidatesWithoutWebRTCSessionIDEmitsUnknownMetadata)
{
std::string sessions = SessionsJson("1", "streaming", 42);
auto tlv = EncodeTlv("{webRTCSessionID:{tag:0,type:'uint16'}, ICECandidates:{tag:1,type:'array'}}",
"{webRTCSessionID: null, ICECandidates: [{0:'candidate:1 udp host', 1:null, 2:null}]}");

auto result = InvokeCommandHandler(
"handleIncomingIceCandidates", CL_WEBRTC_TRANSPORT_REQUESTOR, CMD_ICE_CANDIDATES, tlv, sessions);

ExpectSuccess(result);
const auto *ur = ExpectUpdateResource(*result, "remoteIceCandidates");
ASSERT_TRUE(ur->metadata.has_value());
EXPECT_TRUE(ur->metadata->find("\"sessionId\":\"unknown\"") != std::string::npos);
}

TEST_F(SbmdCameraWebrtcTest, HandleIncomingEndEmitsWebrtcErrorEnded)
Expand All @@ -827,10 +848,25 @@ namespace
ExpectSuccess(result);
const auto *ur = ExpectUpdateResource(*result, "webrtcError", "ended");
ASSERT_TRUE(ur->metadata.has_value());
EXPECT_TRUE(ur->metadata->find("sessionId") != std::string::npos);
EXPECT_TRUE(ur->metadata->find("\"sessionId\":\"1\"") != std::string::npos);
EXPECT_TRUE(ur->metadata->find("reason") != std::string::npos);
}

TEST_F(SbmdCameraWebrtcTest, HandleIncomingEndWithUnknownWebRTCSessionIDReturnsError)
{
std::string sessions = SessionsJson("1", "streaming", 42);
auto tlv = EncodeTlv("{webRTCSessionID:{tag:0,type:'uint16'}, reason:{tag:1,type:'enum8'}}",
"{webRTCSessionID: 99, reason: 2}");

auto result =
InvokeCommandHandler("handleIncomingEndSession", CL_WEBRTC_TRANSPORT_REQUESTOR, CMD_END, tlv, sessions);

ExpectSuccess(result);
const auto *ur = ExpectUpdateResource(*result, "webrtcError", "ended");
ASSERT_TRUE(ur->metadata.has_value());
EXPECT_TRUE(ur->metadata->find("\"sessionId\":\"unknown\"") != std::string::npos);
}

TEST_F(SbmdCameraWebrtcTest, ExecuteStreamReturnsProtocolAndEntryPoint)
{
std::string sessions = SessionsJson("1", "created");
Expand Down
Loading