Try both HLS and webRTC and pick best stream (#22585)

This commit is contained in:
Bram Kragten
2024-10-29 21:38:22 +01:00
committed by GitHub
parent 7a36cf67e3
commit 67852125e5
6 changed files with 213 additions and 89 deletions

View File

@@ -3,21 +3,22 @@ import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature";
import {
CameraEntity,
CAMERA_SUPPORT_STREAM,
CameraCapabilities,
CameraEntity,
computeMJPEGStreamUrl,
fetchStreamUrl,
fetchCameraCapabilities,
fetchThumbnailUrlWithCache,
STREAM_TYPE_HLS,
STREAM_TYPE_WEB_RTC,
StreamType,
} from "../data/camera";
import { HomeAssistant } from "../types";
import "./ha-hls-player";
@@ -41,14 +42,16 @@ export class HaCameraStream extends LitElement {
// Video background image before its loaded
@state() private _posterUrl?: string;
// We keep track if we should force MJPEG if there was a failure
// to get the HLS stream url. This is reset if we change entities.
@state() private _forceMJPEG?: string;
@state() private _url?: string;
@state() private _connected = false;
@state() private _capabilities?: CameraCapabilities;
@state() private _streamType?: StreamType;
@state() private _hlsStreams?: { hasAudio: boolean; hasVideo: boolean };
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
public willUpdate(changedProps: PropertyValues): void {
if (
changedProps.has("stateObj") &&
@@ -57,12 +60,8 @@ export class HaCameraStream extends LitElement {
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
this.stateObj.entity_id
) {
this._getCapabilities();
this._getPosterUrl();
if (this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_HLS) {
this._forceMJPEG = undefined;
this._url = undefined;
this._getStreamUrl();
}
}
}
@@ -87,54 +86,79 @@ export class HaCameraStream extends LitElement {
: this._connected
? computeMJPEGStreamUrl(this.stateObj)
: ""}
.alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
/>`;
}
if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS) {
return this._url
? html`<ha-hls-player
autoplay
playsinline
.allowExoPlayer=${this.allowExoPlayer}
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.url=${this._url}
.posterUrl=${this._posterUrl}
></ha-hls-player>`
: nothing;
return html`${this._streamType === STREAM_TYPE_HLS ||
(!this._streamType &&
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_HLS))
? html`<ha-hls-player
autoplay
playsinline
.allowExoPlayer=${this.allowExoPlayer}
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
class=${!this._streamType && this._webRtcStreams ? "hidden" : ""}
></ha-hls-player>`
: nothing}
${this._streamType === STREAM_TYPE_WEB_RTC ||
(!this._streamType &&
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_WEB_RTC))
? html`<ha-web-rtc-player
autoplay
playsinline
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
class=${this._streamType !== STREAM_TYPE_WEB_RTC &&
!this._webRtcStreams
? "hidden"
: ""}
></ha-web-rtc-player>`
: nothing}`;
}
private async _getCapabilities() {
this._capabilities = undefined;
this._hlsStreams = undefined;
this._webRtcStreams = undefined;
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
return;
}
if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC) {
return html`<ha-web-rtc-player
autoplay
playsinline
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
></ha-web-rtc-player>`;
this._capabilities = await fetchCameraCapabilities(
this.hass!,
this.stateObj!.entity_id
);
if (this._capabilities.frontend_stream_types.length === 1) {
this._streamType = this._capabilities.frontend_stream_types[0];
}
return nothing;
}
private get _shouldRenderMJPEG() {
if (this._forceMJPEG === this.stateObj!.entity_id) {
// Fallback when unable to fetch stream url
return true;
}
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
// Steaming is not supported by the camera so fallback to MJPEG stream
return true;
}
if (
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC
this._capabilities &&
(!this._capabilities.frontend_stream_types.includes(STREAM_TYPE_HLS) ||
this._hlsStreams?.hasVideo === false) &&
(!this._capabilities.frontend_stream_types.includes(
STREAM_TYPE_WEB_RTC
) ||
this._webRtcStreams?.hasVideo === false)
) {
// Browser support required for WebRTC
return typeof RTCPeerConnection === "undefined";
// No video in HLS stream and no video in WebRTC stream
return true;
}
// Server side stream component required for HLS
return !isComponentLoaded(this.hass!, "stream");
return false;
}
private async _getPosterUrl(): Promise<void> {
@@ -151,20 +175,28 @@ export class HaCameraStream extends LitElement {
}
}
private async _getStreamUrl(): Promise<void> {
try {
const { url } = await fetchStreamUrl(
this.hass!,
this.stateObj!.entity_id
);
private _handleHlsStreams(ev: CustomEvent) {
this._hlsStreams = ev.detail;
this._pickStreamType();
}
this._url = url;
} catch (err: any) {
// Fails if we were unable to get a stream
// eslint-disable-next-line
console.error(err);
private _handleWebRtcStreams(ev: CustomEvent) {
this._webRtcStreams = ev.detail;
this._pickStreamType();
}
this._forceMJPEG = this.stateObj!.entity_id;
private _pickStreamType() {
if (!this._hlsStreams || !this._webRtcStreams) {
return;
}
if (
this._hlsStreams.hasVideo &&
this._hlsStreams.hasAudio &&
!this._webRtcStreams.hasAudio
) {
this._streamType = STREAM_TYPE_HLS;
} else if (this._webRtcStreams.hasVideo) {
this._streamType = STREAM_TYPE_WEB_RTC;
}
}
@@ -178,6 +210,10 @@ export class HaCameraStream extends LitElement {
img {
width: 100%;
}
.hidden {
display: none;
}
`;
}
}
@@ -186,4 +222,12 @@ declare global {
interface HTMLElementTagNameMap {
"ha-camera-stream": HaCameraStream;
}
interface HASSDomEvents {
load: undefined;
streams: {
hasAudio: boolean;
hasVideo: boolean;
codecs?: string[];
};
}
}