fix: Increase reliability of media auto actions (e.g. play) (#1934)

Refactors media player handling entirely to make the code more
consistent and less boilerplate. Add testing for media player actions.

- Closes #1921
- Closes #1916
This commit is contained in:
Dermot Duffy
2025-03-03 20:59:01 -08:00
committed by GitHub
parent d4650eae8a
commit 3b77b11196
58 changed files with 1972 additions and 1165 deletions
+5 -4
View File
@@ -4,10 +4,11 @@ export interface AudioProperties {
}
// There is currently no consistent cross-browser modern way to determine if a
// viden has audio tracks. The below will work in ~24% of browsers, but notably
// not in Chrome. There used to be a usable `webkitAudioDecodedByteCount`
// property, but this now seems to be consistently 0 in Chrome. This generously
// defaults to assuming there is audio when we cannot rule it out.
// video element has audio tracks. The below will work in ~24% of browsers, but
// notably not in Chrome. There used to be a usable
// `webkitAudioDecodedByteCount` property, but this now seems to be consistently
// 0 in Chrome. This generously defaults to assuming there is audio when we
// cannot rule it out.
export const mayHaveAudio = (video: HTMLVideoElement & AudioProperties): boolean => {
if (video.mozHasAudio !== undefined) {
return video.mozHasAudio;
@@ -1,4 +1,3 @@
import { AdvancedCameraCardMediaPlayer } from '../types';
import { Timer } from './timer';
// The number of seconds to hide the video controls for after loading (in order
@@ -59,30 +58,3 @@ export const hideMediaControlsTemporarily = (
setControlsOnVideo(video, oldValue);
});
};
/**
* @param player The Advanced Camera Card Media Player object.
* @param video An underlying video or media player upon which to call play.
*/
export const playMediaMutingIfNecessary = async (
player: AdvancedCameraCardMediaPlayer,
video?: HTMLVideoElement | AdvancedCameraCardMediaPlayer,
): Promise<void> => {
// If the play call fails, and the media is not already muted, mute it first
// and then try again. This works around some browsers that prevent
// auto-play unless the video is muted.
if (video?.play) {
try {
await video.play();
} catch (err: unknown) {
if ((err as Error).name === 'NotAllowedError' && !player.isMuted()) {
await player.mute();
try {
await video.play();
} catch (_) {
// Pass.
}
}
}
}
};
+4 -4
View File
@@ -1,7 +1,7 @@
import {
AdvancedCameraCardMediaPlayer,
MediaLoadedCapabilities,
MediaLoadedInfo,
MediaPlayerController,
MediaTechnology,
} from '../types.js';
import { dispatchAdvancedCameraCardEvent } from './basic.js';
@@ -17,7 +17,7 @@ const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF;
export function createMediaLoadedInfo(
source: Event | HTMLElement,
options?: {
player?: AdvancedCameraCardMediaPlayer;
mediaPlayerController?: MediaPlayerController;
capabilities?: MediaLoadedCapabilities;
technology?: MediaTechnology[];
},
@@ -45,7 +45,7 @@ export function createMediaLoadedInfo(
return {
width: (target as HTMLCanvasElement).width,
height: (target as HTMLCanvasElement).height,
player: options?.player,
mediaPlayerController: options?.mediaPlayerController,
...options,
};
}
@@ -61,7 +61,7 @@ export function dispatchMediaLoadedEvent(
target: HTMLElement,
source: Event | HTMLElement,
options?: {
player?: AdvancedCameraCardMediaPlayer;
mediaPlayerController?: MediaPlayerController;
capabilities?: MediaLoadedCapabilities;
technology?: MediaTechnology[];
},
+16 -4
View File
@@ -1,16 +1,28 @@
import { format } from 'date-fns';
import { View } from '../view/view';
export const screenshotMedia = (video: HTMLVideoElement): string | null => {
export const screenshotImage = (image: HTMLImageElement): string | null => {
return screenshotElement(image, image.naturalWidth, image.naturalHeight);
};
export const screenshotVideo = (video: HTMLVideoElement): string | null => {
return screenshotElement(video, video.videoWidth, video.videoHeight);
};
const screenshotElement = (
src: CanvasImageSource,
width: number,
height: number,
): string | null => {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
return null;
}
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
ctx.drawImage(src, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL('image/jpeg');
};