Skip to content
Open
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
88 changes: 88 additions & 0 deletions custom_components/webrtc/www/webrtc-camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ class WebRTCCamera extends VideoRTC {
* Step 1. Called by the Hass, when config changed.
* @param {Object} config
*/

constructor() {
super();
this._lastFrameTime = 0; // Timestamp of the last received video frame
this._liveCheckInterval = null; // Interval for checking video playback
}

/**
* Called to update the timestamp of the last received video frame.
*/
updateHeartbeat() {
this._lastFrameTime = Date.now();

// if the video is still there, ask for the next frame notification
if (this.video && this.video.requestVideoFrameCallback) {
this.video.requestVideoFrameCallback(() => this.updateHeartbeat());
}
}

setConfig(config) {
if (!config.url && !config.entity && !config.streams) throw new Error('Missing `url` or `entity` or `streams`');

Expand Down Expand Up @@ -38,6 +57,7 @@ class WebRTCCamera extends VideoRTC {
* ui: boolean,
* style: string,
* background: boolean,
* live_indicator: boolean,
*
* server: string,
*
Expand Down Expand Up @@ -112,6 +132,20 @@ class WebRTCCamera extends VideoRTC {
nextStream(reload) {
this.streamID = (this.streamID + 1) % this.config.streams.length;

// when stream has changed (substream), reset timer and stop heartbeat
if (this.streamID > 0 && this.config.live_indicator == true) { // ensures video element exists
// Reset the time for the watchdog
this._lastFrameTime = 0;

// Also instantly reset the live dot to red
if (this.shadowRoot) {
const dot = this.shadowRoot.querySelector('.live-dot');
if (dot) {
dot.classList.remove('live');
}
}
}

const stream = this.config.streams[this.streamID];
this.config.url = stream.url;
this.config.entity = stream.entity;
Expand Down Expand Up @@ -178,6 +212,38 @@ class WebRTCCamera extends VideoRTC {
}).catch(er => {
this.setStatus('error', er);
});

// when stream is connected, hook the video frame callback to track live indicator
if (this.video && this.video.requestVideoFrameCallback && this.config.live_indicator == true) {
this.video.requestVideoFrameCallback(() => this.updateHeartbeat());
}

// start watchdog timer to update live indicator
if (!this._liveCheckInterval && this.config.live_indicator == true) {
this._liveCheckInterval = setInterval(() => {
const dot = this.shadowRoot.querySelector('.live-dot');
if (!dot) return;

const now = Date.now();
// Check if the last frame arrived less than 500ms ago
const isPlaying = (now - this._lastFrameTime) < 500;

if (isPlaying) {
dot.classList.add('live');
} else {
dot.classList.remove('live');
}
}, 500);
}
}

ondisconnect() {
// when stream is disconnected, cleanup the live indicator watchdog timer
if (this._liveCheckInterval) {
clearInterval(this._liveCheckInterval);
this._liveCheckInterval = null;
}
super.ondisconnect();
}

onopen() {
Expand Down Expand Up @@ -249,13 +315,35 @@ class WebRTCCamera extends VideoRTC {
opacity: 0.6;
pointer-events: auto;
}
.live-dot-wrapper {
display: flex;
flex-grow: 1;
justify-content: end;
}
.live-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #D2122E;
align-self: center;
transition: background-color 0.3s, box-shadow 0.3s;
}
.live-dot.live {
background-color: #90EE90;
}
</style>
<ha-card class="card">
<div class="player">
<div class="ptz-transform"></div>
</div>
<div class="header">
<div class="status"></div>
${ this.config.live_indicator == true ? `
<div class="live-dot-wrapper">
<div class="live-dot"></div> <div class="status"></div>
</div>
` : ''
}
<div class="mode"></div>
</div>
</ha-card>
Expand Down