Connect go2rtc for door camera live view
Door camera live view on a virtual intercom requires low-latency video in the browser. OpenApp integrates go2rtc for RTSP → WebRTC paths. On a live intercom session, get_public_session_streams tells the answering resident which camera devices belong to the door.
This guide is for building operators and integrators wiring lobby cameras to Virtual Access intercom flows. Setup: go2rtc integration.
Architecture
Section titled “Architecture”NVR / camera (RTSP) → go2rtc integration (discovered stream entities) → Virtual intercom session (voice / video) → GET .../sessions/{id}/streams?token=<callee_token> → camera device_ids for the resident's UIOne-time setup
Section titled “One-time setup”- Add go2rtc integration with NVR host, credentials, and channel discovery (getting started).
- Link the Virtual Access door to its opener, and add the go2rtc camera devices to the door’s cameras (stored on the portal device as
go2rtc_camera_device_ids). Cameras from another org are ignored. - Start a portal session with
"mode": "video"— see Virtual intercom flow. Video sessions count against thevideo_sessionsquota.
Start a video session
Section titled “Start a video session”SDK
session = await client.public_access.portal_start_session( portal_id, target_entity_id=entity_id, mode="video",)session_id = session["session_id"]const session = await fetch( `${apiBase}/public/access/portals/${portalId}/sessions`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ target_entity_id: entityId, mode: "video", }), },).then((r) => r.json() as Promise<{ session_id: string }>);
const sessionId = session.session_id;use openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
let session = client .public_access() .portal_start_session( portal_id, &json!({ "target_entity_id": entity_id, "mode": "video" }), ) .await?;let session_id = session["session_id"].as_str().unwrap_or_default();body := *openapiclient.NewPublicPortalCreateSessionRequest("video", entityID)session, httpResp, err := client.PublicAccessAPI.PostPublicPortalSessions(ctx, portalID). PublicPortalCreateSessionRequest(body). Execute()if err != nil { return err}defer httpResp.Body.Close()sessionID := session.GetSessionId()The caller receives session_id and caller_token. It can’t read the streams: that route takes the callee token.
HTTP API (curl)
export OPENAPP_API_BASE='https://openapp.house/api/v1'export PUBLIC_PORTAL_ID='01HPUBLICPORTAL000000000000'export TARGET_ENTITY_ID='01HENTITY000000000000000000'
curl -sS -X POST \ -H "Content-Type: application/json" \ -d '{ "target_entity_id": "'"${TARGET_ENTITY_ID}"'", "mode": "video" }' \ "${OPENAPP_API_BASE}/public/access/portals/${PUBLIC_PORTAL_ID}/sessions"Save session_id from the response.
Fetch the door’s cameras (resident side)
Section titled “Fetch the door’s cameras (resident side)”The answering resident’s client calls this with the callee token from the incoming-call notification, as Authorization: Bearer <callee_token> or ?token=. A caller token is refused (403).
SDK
# session_streams() can't send the callee token; call the route through the client transport.streams = await client._request( "GET", f"/public/access/sessions/{session_id}/streams", query=[("token", callee_token)],)camera_device_ids = streams["device_ids"]const streams = await fetch( `${apiBase}/public/access/sessions/${sessionId}/streams?token=${encodeURIComponent(calleeToken)}`,).then((r) => r.json() as Promise<{ device_ids: string[] }>);
const cameraDeviceIds = streams.device_ids;use openapp_sdk::{Client, transport::RequestSpec};use reqwest::Method;
let client = Client::builder() .api_key("https://openapp.house_openapp_YOUR_SECRET") .build()?;
// session_streams() can't send the callee token; call the route through the transport.let path = format!("/public/access/sessions/{session_id}/streams");let streams: serde_json::Value = client .transport() .request_json::<(), _>(RequestSpec { method: Method::GET, path: &path, query: &[("token", Some(callee_token.to_string()))], ..Default::default() }) .await?;let camera_device_ids = &streams["device_ids"];streams, httpResp, err := client.PublicAccessAPI.GetPublicSessionStreams(ctx, sessionID). Token(calleeToken). Execute()if err != nil { return err}defer httpResp.Body.Close()cameraDeviceIDs := streams.GetDeviceIds()HTTP API (curl)
export SESSION_ID='01HSESSION000000000000000000'export CALLEE_TOKEN='token-from-the-incoming-call-notification'
curl -sS \ -H "Authorization: Bearer ${CALLEE_TOKEN}" \ "${OPENAPP_API_BASE}/public/access/sessions/${SESSION_ID}/streams"Response shape: PublicSessionStreamsResponse — { "device_ids": ["01H…"] }, the go2rtc camera device ids configured on the door. Stream URLs are resolved client-side from those devices. Full reference: Public Access.
Agent notes
Section titled “Agent notes”- Configure go2rtc stream profiles in the integration so the door camera channel is the one you list on the door.
- Session routes are public and authorized by session tokens, not by org roles.
- Test reachability with
GET .../portals/{id}/reachablebefore go-live.