happy-dom
Version:
Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.
907 lines (788 loc) • 22.7 kB
text/typescript
import Event from '../../event/Event.js';
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
import HTMLElement from '../html-element/HTMLElement.js';
import TimeRanges from './TimeRanges.js';
import DOMTokenList from '../../dom/DOMTokenList.js';
import RemotePlayback from './RemotePlayback.js';
import MediaStream from './MediaStream.js';
import TextTrackList from './TextTrackList.js';
import TextTrack from './TextTrack.js';
import TextTrackKindEnum from './TextTrackKindEnum.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import ElementEventAttributeUtility from '../element/ElementEventAttributeUtility.js';
interface IMediaError {
code: number;
message: string;
}
/**
* HTML Media Element.
*
* Reference:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement
*
*/
export default class HTMLMediaElement extends HTMLElement {
// Public properties
public declare cloneNode: (deep?: boolean) => HTMLMediaElement;
// Internal Properties
public [PropertySymbol.volume] = 1;
public [PropertySymbol.paused] = true;
public [PropertySymbol.currentTime] = 0;
public [PropertySymbol.playbackRate] = 1;
public [PropertySymbol.defaultPlaybackRate] = 1;
public [PropertySymbol.muted] = false;
public [PropertySymbol.defaultMuted] = false;
public [PropertySymbol.preservesPitch] = true;
public [PropertySymbol.buffered] = new TimeRanges(PropertySymbol.illegalConstructor);
public [PropertySymbol.duration] = NaN;
public [PropertySymbol.error]: IMediaError = null;
public [PropertySymbol.ended] = false;
public [PropertySymbol.networkState] = 0;
public [PropertySymbol.readyState] = 0;
public [PropertySymbol.seeking] = false;
public [PropertySymbol.seekable] = new TimeRanges(PropertySymbol.illegalConstructor);
public [PropertySymbol.sinkId]: string = '';
public [PropertySymbol.played] = new TimeRanges(PropertySymbol.illegalConstructor);
public [PropertySymbol.remote] = new this[PropertySymbol.window].RemotePlayback();
public [PropertySymbol.controlsList]: DOMTokenList | null = null;
public [PropertySymbol.mediaKeys]: object | null = null;
public [PropertySymbol.srcObject]: MediaStream | null = null;
public [PropertySymbol.textTracks]: TextTrack[] = [];
// Events
/* eslint-disable jsdoc/require-jsdoc */
public get onabort(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onabort');
}
public set onabort(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onabort', value);
}
public get oncanplay(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'oncanplay');
}
public set oncanplay(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('oncanplay', value);
}
public get oncanplaythrough(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'oncanplaythrough');
}
public set oncanplaythrough(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('oncanplaythrough', value);
}
public get ondurationchange(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'ondurationchange');
}
public set ondurationchange(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('ondurationchange', value);
}
public get onemptied(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onemptied');
}
public set onemptied(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onemptied', value);
}
public get onended(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onended');
}
public set onended(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onended', value);
}
public get onloadeddata(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onloadeddata');
}
public set onloadeddata(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onloadeddata', value);
}
public get onloadedmetadata(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onloadedmetadata');
}
public set onloadedmetadata(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onloadedmetadata', value);
}
public get onloadstart(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onloadstart');
}
public set onloadstart(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onloadstart', value);
}
public get onpause(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onpause');
}
public set onpause(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onpause', value);
}
public get onplay(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onplay');
}
public set onplay(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onplay', value);
}
public get onplaying(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onplaying');
}
public set onplaying(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onplaying', value);
}
public get onprogress(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onprogress');
}
public set onprogress(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onprogress', value);
}
public get onratechange(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onratechange');
}
public set onratechange(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onratechange', value);
}
public get onresize(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onresize');
}
public set onresize(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onresize', value);
}
public get onseeked(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onseeked');
}
public set onseeked(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onseeked', value);
}
public get onseeking(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onseeking');
}
public set onseeking(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onseeking', value);
}
public get onstalled(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onstalled');
}
public set onstalled(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onstalled', value);
}
public get onsuspend(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onsuspend');
}
public set onsuspend(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onsuspend', value);
}
public get ontimeupdate(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'ontimeupdate');
}
public set ontimeupdate(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('ontimeupdate', value);
}
public get onvolumechange(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onvolumechange');
}
public set onvolumechange(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onvolumechange', value);
}
public get onwaiting(): ((event: Event) => void) | null {
return ElementEventAttributeUtility.getEventListener(this, 'onwaiting');
}
public set onwaiting(value: ((event: Event) => void) | null) {
this[PropertySymbol.propertyEventListeners].set('onwaiting', value);
}
/* eslint-enable jsdoc/require-jsdoc */
/**
* Returns buffered.
*
* @returns Buffered.
*/
public get buffered(): TimeRanges {
return this[PropertySymbol.buffered];
}
/**
* Returns duration.
*
* @returns Duration.
*/
public get duration(): number {
return this[PropertySymbol.duration];
}
/**
* Returns error.
*
* @returns Error.
*/
public get error(): IMediaError {
return this[PropertySymbol.error];
}
/**
* Returns ended.
*
* @returns Ended.
*/
public get ended(): boolean {
return this[PropertySymbol.ended];
}
/**
* Returns networkState.
*
* @returns NetworkState.
*/
public get networkState(): number {
return this[PropertySymbol.networkState];
}
/**
* Returns readyState.
*
* @returns ReadyState.
*/
public get readyState(): number {
return this[PropertySymbol.readyState];
}
/**
* Return a RemotePlayback object instance associated with the media element.
*
* @returns RemotePlayback.
*/
public get remote(): RemotePlayback {
return this[PropertySymbol.remote];
}
/**
* Returns seeking.
*
* @returns Seeking.
*/
public get seeking(): boolean {
return this[PropertySymbol.seeking];
}
/**
* Returns seekable.
*
* @returns Seekable.
*/
public get seekable(): TimeRanges {
return this[PropertySymbol.seekable];
}
/**
* Returns sinkId.
*
* @returns SinkId.
*/
public get sinkId(): string {
return this[PropertySymbol.sinkId];
}
/**
* Returns played.
*
* @returns Played.
*/
public get played(): TimeRanges {
return this[PropertySymbol.played];
}
/**
* Returns autoplay.
*
* @returns Autoplay.
*/
public get autoplay(): boolean {
return this.getAttribute('autoplay') !== null;
}
/**
* Sets autoplay.
*
* @param autoplay Autoplay.
*/
public set autoplay(autoplay: boolean) {
if (!autoplay) {
this.removeAttribute('autoplay');
} else {
this.setAttribute('autoplay', '');
}
}
/**
* Returns controls.
*
* @returns Controls.
*/
public get controls(): boolean {
return this.getAttribute('controls') !== null;
}
/**
* Sets controls.
*
* @param controls Controls.
*/
public set controls(controls: boolean) {
if (!controls) {
this.removeAttribute('controls');
} else {
this.setAttribute('controls', '');
}
}
/**
* Returns loop.
*
* @returns Loop.
*/
public get loop(): boolean {
return this.getAttribute('loop') !== null;
}
/**
* Sets loop.
*
* @param loop Loop.
*/
public set loop(loop: boolean) {
if (!loop) {
this.removeAttribute('loop');
} else {
this.setAttribute('loop', '');
}
}
/**
* Returns preload.
*
* @returns preload.
*/
public get preload(): string {
return this.getAttribute('preload') || 'auto';
}
/**
* Sets preload.
*
* @param preload preload.
*/
public set preload(preload: string) {
this.setAttribute('preload', preload);
}
/**
* Returns src.
*
* @returns Src.
*/
public get src(): string {
if (!this.hasAttribute('src')) {
return '';
}
try {
return new URL(this.getAttribute('src'), this[PropertySymbol.ownerDocument].location.href)
.href;
} catch (e) {
return this.getAttribute('src');
}
}
/**
* Sets src.
*
* @param src Src.
*/
public set src(src: string) {
this.setAttribute('src', src);
if (Boolean(src)) {
this.dispatchEvent(new Event('canplay', { bubbles: false, cancelable: false }));
this.dispatchEvent(new Event('durationchange', { bubbles: false, cancelable: false }));
}
}
/**
* Returns controlsList.
*
* @returns ControlsList.
*/
public get controlsList(): DOMTokenList {
if (this[PropertySymbol.controlsList] === null) {
this[PropertySymbol.controlsList] = new DOMTokenList(
PropertySymbol.illegalConstructor,
this,
'controlslist'
);
}
return this[PropertySymbol.controlsList];
}
/**
* Sets controlsList.
*
* @param value Value.
*/
public set controlsList(value: string) {
this.setAttribute('controlslist', value);
}
/**
* Returns mediaKeys.
*
* @returns MediaKeys.
*/
public get mediaKeys(): object | null {
return this[PropertySymbol.mediaKeys];
}
/**
* Returns muted.
*
* @returns Muted.
*/
public get muted(): boolean {
if (this[PropertySymbol.muted]) {
return this[PropertySymbol.muted];
}
if (!this[PropertySymbol.defaultMuted]) {
return this.getAttribute('muted') !== null;
}
return false;
}
/**
* Sets muted.
*
* @param muted Muted.
*/
public set muted(muted: boolean) {
this[PropertySymbol.muted] = !!muted;
if (!muted && !this[PropertySymbol.defaultMuted]) {
this.removeAttribute('muted');
} else {
this.setAttribute('muted', '');
}
}
/**
* Returns defaultMuted.
*
* @returns DefaultMuted.
*/
public get defaultMuted(): boolean {
return this[PropertySymbol.defaultMuted];
}
/**
* Sets defaultMuted.
*
* @param defaultMuted DefaultMuted.
*/
public set defaultMuted(defaultMuted: boolean) {
this[PropertySymbol.defaultMuted] = !!defaultMuted;
if (!this[PropertySymbol.defaultMuted] && !this[PropertySymbol.muted]) {
this.removeAttribute('muted');
} else {
this.setAttribute('muted', '');
}
}
/**
* Returns disableRemotePlayback.
*
* @returns DisableRemotePlayback.
*/
public get disableRemotePlayback(): boolean {
return this.getAttribute('disableremoteplayback') !== null;
}
/**
* Sets disableRemotePlayback.
*
* @param disableRemotePlayback DisableRemotePlayback.
*/
public set disableRemotePlayback(disableRemotePlayback: boolean) {
if (!disableRemotePlayback) {
this.removeAttribute('disableremoteplayback');
} else {
this.setAttribute('disableremoteplayback', '');
}
}
/**
* A MediaStream representing the media to play or that has played in the current HTMLMediaElement, or null if not assigned.
*
* @returns MediaStream.
*/
public get srcObject(): MediaStream | null {
return this[PropertySymbol.srcObject];
}
/**
* Sets src object.
*
* @param srcObject SrcObject.
*/
public set srcObject(srcObject: MediaStream | null) {
if (srcObject !== null && !(srcObject instanceof MediaStream)) {
throw new this[PropertySymbol.window].TypeError(
`Failed to set the 'srcObject' property on 'HTMLMediaElement': The provided value is not of type 'MediaStream'.`
);
}
this[PropertySymbol.srcObject] = srcObject;
}
/**
* Returns text track list.
*
* @returns Text track list.
*/
public get textTracks(): TextTrackList {
const items = [];
for (const track of this[PropertySymbol.textTracks]) {
items.push(track);
}
for (const track of this.querySelectorAll('track')[PropertySymbol.items]) {
items.push(track.track);
}
return new this[PropertySymbol.window].TextTrackList(PropertySymbol.illegalConstructor, items);
}
/**
* Returns currentSrc.
*
* @returns CurrentrSrc.
*/
public get currentSrc(): string {
const src = this.src;
if (src) {
return src;
}
const sourceElement = this.querySelector('source');
return sourceElement ? sourceElement.src : '';
}
/**
* Returns volume.
*
* @returns Volume.
*/
public get volume(): number {
return this[PropertySymbol.volume];
}
/**
* Sets volume.
*
* @param volume Volume.
*/
public set volume(volume: number | string) {
const parsedVolume = Number(volume);
if (isNaN(parsedVolume)) {
throw new this[PropertySymbol.window].TypeError(
`Failed to set the 'volume' property on 'HTMLMediaElement': The provided double value is non-finite.`
);
}
if (parsedVolume < 0 || parsedVolume > 1) {
throw new this[PropertySymbol.window].DOMException(
`Failed to set the 'volume' property on 'HTMLMediaElement': The volume provided (${parsedVolume}) is outside the range [0, 1].`,
DOMExceptionNameEnum.indexSizeError
);
}
// TODO: volumechange event https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volumechange_event
this[PropertySymbol.volume] = parsedVolume;
}
/**
* Returns crossOrigin.
*
* @returns CrossOrigin.
*/
public get crossOrigin(): string {
const crossOrigin = this.getAttribute('crossorigin');
if (crossOrigin === 'use-credentials') {
return 'use-credentials';
}
if (crossOrigin !== null) {
return 'anonymous';
}
return null;
}
/**
* Sets crossOrigin.
*
* @param crossOrigin CrossOrigin.
*/
public set crossOrigin(crossOrigin: string | null) {
this.setAttribute('crossorigin', crossOrigin);
}
/**
* Returns currentTime.
*
* @returns CurrentTime.
*/
public get currentTime(): number {
return this[PropertySymbol.currentTime];
}
/**
* Sets currentTime.
*
* @param currentTime CurrentTime.
*/
public set currentTime(currentTime: number | string) {
const parsedCurrentTime = Number(currentTime);
if (isNaN(parsedCurrentTime)) {
throw new this[PropertySymbol.window].TypeError(
`Failed to set the 'currentTime' property on 'HTMLMediaElement': The provided double value is non-finite.`
);
}
this[PropertySymbol.currentTime] = parsedCurrentTime;
}
/**
* Returns playbackRate.
*
* @returns PlaybackRate.
*/
public get playbackRate(): number {
return this[PropertySymbol.playbackRate];
}
/**
* Sets playbackRate.
*
* @param playbackRate PlaybackRate.
*/
public set playbackRate(playbackRate: number | string) {
const parsedPlaybackRate = Number(playbackRate);
if (isNaN(parsedPlaybackRate)) {
throw new this[PropertySymbol.window].TypeError(
`Failed to set the 'playbackRate' property on 'HTMLMediaElement': The provided double value is non-finite.`
);
}
this[PropertySymbol.playbackRate] = parsedPlaybackRate;
}
/**
* Returns defaultPlaybackRate.
*
* @returns DefaultPlaybackRate.
*/
public get defaultPlaybackRate(): number {
return this[PropertySymbol.defaultPlaybackRate];
}
/**
* Sets defaultPlaybackRate.
*
* @param defaultPlaybackRate DefaultPlaybackRate.
*/
public set defaultPlaybackRate(defaultPlaybackRate: number | string) {
const parsedDefaultPlaybackRate = Number(defaultPlaybackRate);
if (isNaN(parsedDefaultPlaybackRate)) {
throw new this[PropertySymbol.window].TypeError(
`Failed to set the 'defaultPlaybackRate' property on 'HTMLMediaElement': The provided double value is non-finite.`
);
}
this[PropertySymbol.defaultPlaybackRate] = parsedDefaultPlaybackRate;
}
/**
* Returns preservesPitch.
*
* @returns PlaybackRate.
*/
public get preservesPitch(): boolean {
return this[PropertySymbol.preservesPitch];
}
/**
* Sets preservesPitch.
*
* @param preservesPitch PreservesPitch.
*/
public set preservesPitch(preservesPitch: boolean) {
this[PropertySymbol.preservesPitch] = Boolean(preservesPitch);
}
/**
* Returns paused.
*
* @returns Paused.
*/
public get paused(): boolean {
return this[PropertySymbol.paused];
}
/**
* @override
*/
public override get tabIndex(): number {
const tabIndex = this.getAttribute('tabindex');
if (tabIndex !== null) {
const parsed = Number(tabIndex);
return isNaN(parsed) ? 0 : parsed;
}
return 0;
}
/**
* @override
*/
public override set tabIndex(tabIndex: number) {
super.tabIndex = tabIndex;
}
/**
* Adds a new text track to the media element.
*
* @param kind The kind of text track.
* @param label The label of the text track.
* @param language The language of the text track data.
*/
public addTextTrack(kind: TextTrackKindEnum, label?: string, language?: string): TextTrack {
const window = this[PropertySymbol.window];
if (arguments.length === 0) {
throw new window.TypeError(
`Failed to execute 'addTextTrack' on 'HTMLMediaElement': 1 argument required, but only 0 present.`
);
}
if (!TextTrackKindEnum[kind]) {
throw new window.TypeError(
`Failed to execute 'addTextTrack' on 'HTMLMediaElement': The provided value '${kind}' is not a valid enum value of type TextTrackKind.`
);
}
const track = new window.TextTrack(PropertySymbol.illegalConstructor);
track[PropertySymbol.kind] = kind;
track[PropertySymbol.label] = label || '';
track[PropertySymbol.language] = language || '';
this[PropertySymbol.textTracks].push(track);
return track;
}
/**
* Pause played media.
*/
public pause(): void {
if (this[PropertySymbol.paused]) {
return;
}
this[PropertySymbol.paused] = true;
this.dispatchEvent(new Event('pause', { bubbles: false, cancelable: false }));
}
/**
* Start playing media.
*/
public async play(): Promise<void> {
if (!this[PropertySymbol.paused]) {
return;
}
this[PropertySymbol.paused] = false;
this.dispatchEvent(new Event('play', { bubbles: false, cancelable: false }));
this.dispatchEvent(new Event('playing', { bubbles: false, cancelable: false }));
}
/**
* Reports how likely it is that the current browser will be able to play media of a given MIME type.
*
* @param _type MIME type.
* @returns Can play type.
*/
public canPlayType(_type: string): string {
// TODO: Implement this method
return '';
}
/**
* Quickly seeks the media to the new time with precision tradeoff.
*
* @param _time Time.
*/
public fastSeek(_time: number): void {
// TODO: Implement this method
}
/**
* Load media.
*/
public load(): void {
this.dispatchEvent(new Event('emptied', { bubbles: false, cancelable: false }));
}
/**
* Sets media keys.
*
* @param mediaKeys MediaKeys.
* @returns Promise.
*/
public async setMediaKeys(mediaKeys: object | null): Promise<void> {
this[PropertySymbol.mediaKeys] = mediaKeys;
}
/**
* Sets sink id.
*
* @param sinkId SinkId.
* @returns Promise.
*/
public async setSinkId(sinkId: string): Promise<void> {
this[PropertySymbol.sinkId] = sinkId;
}
/**
* Returns MediaStream, captures a stream of the media content.
*
* @returns MediaStream.
*/
public captureStream(): MediaStream {
return new this[PropertySymbol.window].MediaStream();
}
/**
* @override
*/
public override [PropertySymbol.cloneNode](deep = false): HTMLMediaElement {
return <HTMLMediaElement>super[PropertySymbol.cloneNode](deep);
}
}