Live Video Streaming
Ring provides live video streaming through WebRTC connectivity using the WHEP (WebRTC HTTP Egress Protocol) standard. This enables low-latency video streaming from Ring devices to partner applications.
Overview
- Video only: Live streams deliver video content only — audio is not available
- WebRTC-based: Industry standard for real-time communication
- WHEP protocol: Simplified WebRTC session establishment via HTTP
- Receive only: Partners receive video from the device (
recvonly) — no bidirectional media - Session duration limits: Battery-powered devices up to 30 seconds; line-powered up to 60 seconds
Starting a Live Video Session
Session Initiation
Create an SDP offer using WebRTC and send it to the WHEP endpoint:
POST https://api.amazonvision.com/v1/devices/{device_id}/media/streaming/whep/sessions
Authorization: Bearer <access_token>
Content-Type: application/sdp
[SDP Protocol Offer — generated by WebRTC peer connection]
Successful Response
HTTP/1.1 201 Created
Content-Type: application/sdp
Location: https://api.amazonvision.com/v1/devices/{device_id}/media/streaming/whep/sessions/{session_id}
[SDP Protocol Answer — contains WebRTC connection details]
Key response elements:
- HTTP 201: Successful session creation
- Location header: Session management URL for closing the stream
- SDP Protocol Answer: Connection details for establishing the WebRTC session
Closing a Session
Properly close streaming sessions to optimize battery life:
DELETE https://api.amazonvision.com/v1/devices/{device_id}/media/streaming/whep/sessions/{session_id}
Authorization: Bearer <access_token>
Session Duration Limits
| Device Power Type | Maximum Stream Duration |
|---|---|
| Battery-powered | 30 seconds |
| Line-powered | 60 seconds |
After the maximum duration, the session is automatically terminated. Partners should monitor duration and implement reconnection logic if extended viewing is needed.
Complete WebRTC Implementation
ontrack, onicecandidate, onconnectionstatechange) must be attached to the RTCPeerConnection before calling createOffer() or setRemoteDescription(). Attaching them after may cause missed events and silent failures (e.g., no video displayed).async function startLiveVideo(deviceId, accessToken, videoElement) {
const whepEndpoint =
`https://api.amazonvision.com/v1/devices/${deviceId}/media/streaming/whep/sessions`;
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// Set up event handlers BEFORE createOffer
peerConnection.ontrack = (event) => {
if (event.streams && event.streams[0]) {
videoElement.srcObject = event.streams[0];
} else {
videoElement.srcObject = new MediaStream([event.track]);
}
videoElement.play().catch((err) =>
console.warn('Autoplay blocked:', err)
);
};
peerConnection.onconnectionstatechange = () => {
if (peerConnection.connectionState === 'failed') {
console.error('WebRTC connection failed');
}
};
// Video-only receive transceiver (no audio)
peerConnection.addTransceiver('video', { direction: 'recvonly' });
// Create and send SDP offer
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// Wait for ICE gathering to complete
await new Promise((resolve) => {
if (peerConnection.iceGatheringState === 'complete') {
resolve();
} else {
peerConnection.addEventListener('icegatheringstatechange', () => {
if (peerConnection.iceGatheringState === 'complete') resolve();
});
}
});
// Send to Ring WHEP endpoint
const response = await fetch(whepEndpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/sdp'
},
body: peerConnection.localDescription.sdp
});
if (response.status !== 201) {
peerConnection.close();
throw new Error(`WHEP session failed (${response.status})`);
}
// Set remote SDP answer
await peerConnection.setRemoteDescription({
type: 'answer',
sdp: await response.text()
});
const sessionUrl = response.headers.get('Location');
return { sessionUrl, peerConnection };
}
async function closeLiveVideo(sessionUrl, peerConnection, accessToken, videoElement) {
if (videoElement) videoElement.srcObject = null;
if (peerConnection) peerConnection.close();
if (sessionUrl) {
await fetch(sessionUrl, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${accessToken}` }
});
}
}
Error Handling
Common Error Responses
Device Offline (503)
{
"errors": [{ "status": "503", "title": "Device Unavailable", "detail": "Device is currently offline" }]
}
Invalid Authentication (401)
{
"errors": [{ "status": "401", "title": "Invalid Client" }]
}
Device Not Found (404)
{
"errors": [{ "status": "404", "title": "Not Found" }]
}
Common Mistakes
"Incompatible send direction" SDP error
Cause: An audio transceiver was added. Live streams are video only.
Fix: Only add a video transceiver:
// CORRECT:
peerConnection.addTransceiver('video', { direction: 'recvonly' });
// WRONG — do NOT add audio:
// peerConnection.addTransceiver('audio', { direction: 'recvonly' });
No video appearing despite "connected" state
Cause: ontrack handler was attached after setRemoteDescription().
Fix: Always set up ontrack before calling createOffer() or setRemoteDescription().
Texture size [0x0] errors when processing video frames
Cause: Frame processing started before the video element had valid dimensions.
Fix:
function waitForVideoReady(videoElement) {
return new Promise((resolve) => {
const check = () => {
if (videoElement.videoWidth > 0 && videoElement.videoHeight > 0) {
resolve();
} else {
requestAnimationFrame(check);
}
};
check();
});
}
Best Practices
- Always close sessions: Use DELETE endpoint when streaming ends
- Set up event handlers first: Attach handlers before
createOffer() - Wait for ICE gathering: Only send SDP offer when
iceGatheringState === 'complete' - Minimize session duration: Close streams when not actively viewing
- Monitor connection state: Watch
connectionStateto detect failures - Handle reconnection: Implement reconnection logic for extended viewing
Next: Media Clips →

