mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 10:59:50 +00:00
Try both HLS and webRTC and pick best stream (#22585)
This commit is contained in:
@@ -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[];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user