fix: Always respect the returned MIME type (#2473)

- Closes #2449
This commit is contained in:
Dermot Duffy
2026-06-30 17:45:12 -07:00
committed by dermotduffy
parent 591f466b92
commit 31d0c9322f
10 changed files with 88 additions and 105 deletions
-10
View File
@@ -6,7 +6,6 @@ import {
EventViewMedia,
RecordingViewMedia,
ReviewViewMedia,
VideoContentType,
ViewMedia,
ViewMediaType,
} from '../../view/item';
@@ -60,9 +59,6 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia {
// progress.
return !this.getEndTime();
}
public getVideoContentType(): VideoContentType | null {
return VideoContentType.HLS;
}
public getID(): string {
return this._event.id;
}
@@ -141,9 +137,6 @@ export class FrigateRecordingViewMedia extends ViewMedia implements RecordingVie
// progress.
return !this.getEndTime();
}
public getVideoContentType(): VideoContentType | null {
return VideoContentType.HLS;
}
public getContentID(): string | null {
return this._contentID;
}
@@ -186,9 +179,6 @@ export class FrigateReviewViewMedia extends ViewMedia implements ReviewViewMedia
public inProgress(): boolean | null {
return !this.getEndTime();
}
public getVideoContentType(): VideoContentType | null {
return VideoContentType.HLS;
}
public getContentID(): string | null {
return this._contentID;
}
+17 -15
View File
@@ -22,13 +22,14 @@ import { ViewerConfig } from '../../config/schema/viewer.js';
import { canonicalizeHAURL } from '../../ha/canonical-url.js';
import { isHARelativeURL } from '../../ha/is-ha-relative-url.js';
import { ResolvedMediaCache, resolveMedia } from '../../ha/resolved-media.js';
import { HomeAssistant } from '../../ha/types.js';
import { HomeAssistant, ResolvedMedia } from '../../ha/types.js';
import { localize } from '../../localize/localize.js';
import '../../patches/ha-hls-player.js';
import viewerProviderStyle from '../../scss/viewer-provider.scss';
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../../types.js';
import { classifyMimeType } from '../../utils/mime-type.js';
import { ViewItemClassifier } from '../../view/item-classifier.js';
import { VideoContentType, ViewMedia } from '../../view/item.js';
import { ViewMedia } from '../../view/item.js';
import { UnifiedQueryTransformer } from '../../view/unified-query-transformer.js';
import '../image-player.js';
import { renderNotificationBlockFromText } from '../notification/block.js';
@@ -62,23 +63,23 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
private _refProvider: Ref<MediaPlayerElement> = createRef();
private _lazyLoadController: LazyLoadController = new LazyLoadController(this);
private _resolvedMediaURL: string | null = null;
private _resolvedMedia: ResolvedMedia | null = null;
private _signedURLController = new SignedURLController(this, () => {
if (!this.hass || !this._resolvedMediaURL) {
if (!this.hass || !this._resolvedMedia) {
return {};
}
// HA-relative URLs need no proxying or signing.
if (isHARelativeURL(this._resolvedMediaURL)) {
if (isHARelativeURL(this._resolvedMedia.url)) {
return {
endpoint: { endpoint: canonicalizeHAURL(this.hass, this._resolvedMediaURL) },
endpoint: { endpoint: canonicalizeHAURL(this.hass, this._resolvedMedia.url) },
};
}
const cameraID = this.media?.getCameraID();
const camera = cameraID ? this.cameraManager?.getStore().getCamera(cameraID) : null;
return {
hass: this.hass,
endpoint: { endpoint: this._resolvedMediaURL },
endpoint: { endpoint: this._resolvedMedia.url },
proxyConfig: camera?.getMediaProxyConfig(),
};
});
@@ -128,19 +129,18 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
private async _resolveURL(): Promise<void> {
const contentID = this.media?.getContentID();
if (!contentID || !this.hass || !this._lazyLoadController?.isLoaded()) {
this._resolvedMediaURL = null;
this._resolvedMedia = null;
return;
}
// Clear immediately so the SignedURLController doesn't see a stale URL
// from the previous media item during the async gap.
this._resolvedMediaURL = null;
this._resolvedMedia = null;
const resolved =
this._resolvedMedia =
this.resolvedMediaCache?.get(contentID) ??
(await resolveMedia(this.hass, contentID, this.resolvedMediaCache));
this._resolvedMediaURL = resolved?.url ?? null;
(await resolveMedia(this.hass, contentID, this.resolvedMediaCache)) ??
null;
this.requestUpdate();
}
@@ -251,9 +251,11 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
// Note: crossorigin="anonymous" is required on <video> below in order to
// allow screenshot of motionEye videos which currently go cross-origin.
const mediaID = this.media.getID() ?? undefined;
const { isHLS, isVideo } = classifyMimeType(this._resolvedMedia?.mime_type);
return this._renderContainer(html`
${ViewItemClassifier.isVideo(this.media)
? this.media.getVideoContentType() === VideoContentType.HLS
${isVideo
? isHLS
? html`<advanced-camera-card-ha-hls-player
${ref(this._refProvider)}
allow-exoplayer
-4
View File
@@ -5,7 +5,6 @@ import { FolderConfig } from '../../config/schema/folders';
import { formatDateAndTime } from '../../utils/basic';
import {
EventViewMedia,
VideoContentType,
ViewFolder,
ViewMedia,
ViewMediaSourceOptions,
@@ -78,9 +77,6 @@ export class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMed
public getEndTime(): Date | null {
return this._browseMedia._metadata?.endDate ?? null;
}
public getVideoContentType(): VideoContentType | null {
return this._mediaType === ViewMediaType.Clip ? VideoContentType.MP4 : null;
}
public getID(): string {
return this._id;
}
+19
View File
@@ -0,0 +1,19 @@
interface MimeTypeClassification {
isHLS: boolean;
isVideo: boolean;
}
/**
* Classifies a MIME type for player selection.
*
* RFC 6838 declares MIME types case-insensitive (e.g. Frigate emits
* `application/x-mpegURL`), so input is normalized before comparison.
*/
export const classifyMimeType = (mimeType?: string): MimeTypeClassification => {
const normalized = mimeType?.toLowerCase();
const isHLS =
normalized === 'application/vnd.apple.mpegurl' ||
normalized === 'application/x-mpegurl';
const isVideo = isHLS || !!normalized?.startsWith('video/');
return { isHLS, isVideo };
};
-8
View File
@@ -10,11 +10,6 @@ export enum ViewMediaType {
Review = 'review',
}
export enum VideoContentType {
MP4 = 'mp4',
HLS = 'hls',
}
export interface ViewMediaSourceOptions {
cameraID?: string;
folder?: FolderConfig;
@@ -40,9 +35,6 @@ export class ViewMedia {
public getMediaType(): ViewMediaType {
return this._mediaType;
}
public getVideoContentType(): VideoContentType | null {
return null;
}
public getID(): string | null {
return null;
}