From 884827afb6a150438b985530f843daca56f4a713 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 3 Feb 2023 08:55:32 +0100 Subject: [PATCH] MOBILE-4166 videojs: Support fullscreen and improve types --- .../mediaplugin/classes/videojs-ogvjs.ts | 132 ++++++++---------- .../services/handlers/mediaplugin.ts | 42 ++++-- .../MediaElementController.ts | 18 +-- src/core/services/utils/url.ts | 4 +- src/core/singletons/dom.ts | 4 +- src/core/singletons/events.ts | 3 +- src/core/singletons/media.ts | 104 ++++++++++++++ src/theme/components/videojs.scss | 93 ++++++++++++ src/theme/theme.base.scss | 60 -------- src/theme/theme.scss | 1 + src/types/videojs.d.ts | 115 +++++++++++++++ 11 files changed, 415 insertions(+), 161 deletions(-) create mode 100644 src/core/singletons/media.ts create mode 100644 src/theme/components/videojs.scss create mode 100644 src/types/videojs.d.ts diff --git a/src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts b/src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts index 19707463a..c86ad7531 100644 --- a/src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts +++ b/src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts @@ -14,7 +14,7 @@ import { CorePlatform } from '@services/platform'; import { OGVPlayer, OGVCompat, OGVLoader } from 'ogv'; -import videojs from 'video.js'; +import videojs, { PreloadOption, TechSourceObject, VideoJSOptions } from 'video.js'; export const Tech = videojs.getComponent('Tech'); @@ -95,6 +95,10 @@ export class VideoJSOgvJS extends Tech { 'volumechange', ]; + protected playerId?: string; + protected parentElement: HTMLElement | null = null; + protected placeholderElement = document.createElement('div'); + // Variables/functions defined in parent classes. protected el_!: OGVPlayerEl; // eslint-disable-line @typescript-eslint/naming-convention protected options_!: VideoJSOptions; // eslint-disable-line @typescript-eslint/naming-convention @@ -108,7 +112,7 @@ export class VideoJSOgvJS extends Tech { * @param options The key/value store of player options. * @param ready Callback function to call when the `OgvJS` Tech is ready. */ - constructor(options: VideoJSOptions, ready: () => void) { + constructor(options: VideoJSTechOptions, ready: () => void) { super(options, ready); this.el_.src = options.src || options.source?.src || options.sources?.[0]?.src || this.el_.src; @@ -116,6 +120,7 @@ export class VideoJSOgvJS extends Tech { VideoJSOgvJS.setIfAvailable(this.el_, 'loop', options.loop); VideoJSOgvJS.setIfAvailable(this.el_, 'poster', options.poster); VideoJSOgvJS.setIfAvailable(this.el_, 'preload', options.preload); + this.playerId = options.playerId; this.on('loadedmetadata', () => { if (CoreApp.isIPhone()) { @@ -654,8 +659,7 @@ export class VideoJSOgvJS extends Tech { * @returns Whether it supports full screen. */ supportsFullScreen(): boolean { - // iOS devices have some problem with HTML5 fullscreen api so we need to fallback to fullWindow mode. - return !CoreApp.isIOS(); + return !!this.playerId; } /** @@ -667,6 +671,50 @@ export class VideoJSOgvJS extends Tech { return this.el_.error; } + /** + * Enter full screen mode. + */ + enterFullScreen(): void { + // Use a "fake" full screen mode, moving the player to a different place in DOM to be able to use full screen size. + const player = videojs.getPlayer(this.playerId ?? ''); + if (!player) { + return; + } + + const container = player.el(); + this.parentElement = container.parentElement; + if (!this.parentElement) { + // Shouldn't happen, it means the element is not in DOM. Do not support full screen in this case. + return; + } + + this.parentElement.replaceChild(this.placeholderElement, container); + document.body.appendChild(container); + container.classList.add('vjs-ios-moodleapp-fs'); + + player.isFullscreen(true); + } + + /** + * Exit full screen mode. + */ + exitFullScreen(): void { + if (!this.parentElement) { + return; + } + + const player = videojs.getPlayer(this.playerId ?? ''); + if (!player) { + return; + } + + const container = player.el(); + this.parentElement.replaceChild(container, this.placeholderElement); + container.classList.remove('vjs-ios-moodleapp-fs'); + + player.isFullscreen(false); + } + } [ @@ -688,75 +736,13 @@ export const initializeVideoJSOgvJS = (): void => { Tech.registerTech('OgvJS', VideoJSOgvJS); }; -export type VideoJSOptions = { - aspectRatio?: string; - audioOnlyMode?: boolean; - audioPosterMode?: boolean; - autoplay?: boolean | string; - autoSetup?: boolean; - base?: string; - breakpoints?: Record; - children?: string[] | Record>; - controlBar?: { - remainingTimeDisplay?: { - displayNegative?: boolean; - }; - }; - controls?: boolean; - fluid?: boolean; - fullscreen?: { - options?: Record; - }; - height?: string | number; - id?: string; - inactivityTimeout?: number; - language?: string; - languages?: Record>; - liveui?: boolean; - liveTracker?: { - trackingThreshold?: number; - liveTolerance?: number; - }; - loop?: boolean; - muted?: boolean; - nativeControlsForTouch?: boolean; - normalizeAutoplay?: boolean; - notSupportedMessage?: string; - noUITitleAttributes?: boolean; - playbackRates?: number[]; - plugins?: Record>; - poster?: string; - preferFullWindow?: boolean; - preload?: PreloadOption; - responsive?: boolean; - restoreEl?: boolean | HTMLElement; - source?: TechSourceObject; - sources?: TechSourceObject[]; - src?: string; - suppressNotSupportedError?: boolean; - tag?: HTMLElement; - techCanOverridePoster?: boolean; - techOrder?: string[]; - userActions?: { - click?: boolean | ((ev: MouseEvent) => void); - doubleClick?: boolean | ((ev: MouseEvent) => void); - hotkeys?: boolean | ((ev: KeyboardEvent) => void) | { - fullscreenKey?: (ev: KeyboardEvent) => void; - muteKey?: (ev: KeyboardEvent) => void; - playPauseKey?: (ev: KeyboardEvent) => void; - }; - }; - 'vtt.js'?: string; - width?: string | number; -}; - -type TechSourceObject = { - src: string; // Source URL. - type: string; // Mimetype. -}; - -type PreloadOption = '' | 'none' | 'metadata' | 'auto'; - type OGVPlayerEl = (HTMLAudioElement | HTMLVideoElement) & { stop: () => void; }; + +/** + * VideoJS Tech options. It includes some options added by VideoJS internally. + */ +type VideoJSTechOptions = VideoJSOptions & { + playerId?: string; +}; diff --git a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts index 0de318e04..503233620 100644 --- a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts +++ b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts @@ -21,10 +21,9 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreUrlUtils } from '@services/utils/url'; import { makeSingleton } from '@singletons'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; -import { CoreDom } from '@singletons/dom'; import { CoreEvents } from '@singletons/events'; -import videojs from 'video.js'; -import { VideoJSOptions } from '../../classes/videojs-ogvjs'; +import { CoreMedia } from '@singletons/media'; +import videojs, { VideoJSOptions, VideoJSPlayer } from 'video.js'; /** * Handler to support the Multimedia filter. @@ -48,7 +47,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl const videos = Array.from(this.template.content.querySelectorAll('video')); videos.forEach((video) => { - this.treatVideoFilters(video); + this.treatYoutubeVideos(video); }); return this.template.innerHTML; @@ -61,7 +60,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl const mediaElements = Array.from(container.querySelectorAll('video, audio')); mediaElements.forEach((mediaElement) => { - if (CoreDom.mediaUsesJavascriptPlayer(mediaElement)) { + if (CoreMedia.mediaUsesJavascriptPlayer(mediaElement)) { this.useVideoJS(mediaElement); } else { // Remove the VideoJS classes and data if present. @@ -93,11 +92,14 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl controls: true, techOrder: ['OgvJS'], language: lang, - fluid: true, controlBar: { - fullscreenToggle: false, + pictureInPictureToggle: false, }, aspectRatio: data.aspectRatio, + }, () => { + if (mediaElement.tagName === 'VIDEO') { + this.fixVideoJSPlayerSize(player); + } }); CoreEvents.trigger(CoreEvents.JS_PLAYER_CREATED, { @@ -105,16 +107,34 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl element: mediaElement, player, }); - } /** - * Treat video filters. Currently only treating youtube video using video JS. + * Fix VideoJS player size. + * If video width is wider than available width, video is cut off. Fix the dimensions in this case. + * + * @param player Player instance. + */ + protected fixVideoJSPlayerSize(player: VideoJSPlayer): void { + const videoWidth = player.videoWidth(); + const videoHeight = player.videoHeight(); + const playerDimensions = player.currentDimensions(); + if (!videoWidth || !videoHeight || !playerDimensions.width || videoWidth === playerDimensions.width) { + return; + } + + const candidateHeight = playerDimensions.width * videoHeight / videoWidth; + if (!playerDimensions.height || Math.abs(candidateHeight - playerDimensions.height) > 1) { + player.dimension('height', candidateHeight); + } + } + + /** + * Treat Video JS Youtube video links and translate them to iframes. * * @param video Video element. */ - protected treatVideoFilters(video: HTMLElement): void { - // Treat Video JS Youtube video links and translate them to iframes. + protected treatYoutubeVideos(video: HTMLElement): void { if (!video.classList.contains('video-js')) { return; } diff --git a/src/core/classes/element-controllers/MediaElementController.ts b/src/core/classes/element-controllers/MediaElementController.ts index 1f8b31648..247c3985e 100644 --- a/src/core/classes/element-controllers/MediaElementController.ts +++ b/src/core/classes/element-controllers/MediaElementController.ts @@ -14,10 +14,10 @@ import { CoreUtils } from '@services/utils/utils'; import { ElementController } from './ElementController'; -import videojs from 'video.js'; -import { CoreDom } from '@singletons/dom'; +import videojs, { VideoJSPlayer } from 'video.js'; import { CorePromisedValue } from '@classes/promised-value'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreMedia } from '@singletons/media'; /** * Wrapper class to control the interactivity of a media element. @@ -42,7 +42,7 @@ export class MediaElementController extends ElementController { media.autoplay = false; - if (CoreDom.mediaUsesJavascriptPlayer(media)) { + if (CoreMedia.mediaUsesJavascriptPlayer(media)) { const player = this.searchJSPlayer(); if (player) { this.jsPlayer.resolve(player); @@ -50,7 +50,7 @@ export class MediaElementController extends ElementController { this.jsPlayerListener = CoreEvents.on(CoreEvents.JS_PLAYER_CREATED, data => { if (data.element === media) { this.jsPlayerListener?.off(); - this.jsPlayer.resolve(data.player as VideoJSPlayer); + this.jsPlayer.resolve(data.player); } }); } @@ -153,16 +153,8 @@ export class MediaElementController extends ElementController { * * @returns Player instance if found. */ - private searchJSPlayer(): VideoJSPlayer | undefined { + private searchJSPlayer(): VideoJSPlayer | null { return videojs.getPlayer(this.media.id) || videojs.getPlayer(this.media.id.replace('_html5_api', '')); } } - -type VideoJSPlayer = { - play: () => Promise; - pause: () => Promise; - on: (name: string, callback: (ev: Event) => void) => void; - off: (name: string, callback: (ev: Event) => void) => void; - dispose: () => void; -}; diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index aa8a0c9e4..c7c63dec4 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -22,7 +22,7 @@ import { CoreUrl } from '@singletons/url'; import { CoreSites } from '@services/sites'; import { CorePath } from '@singletons/path'; import { CorePlatform } from '@services/platform'; -import { CoreDom } from '@singletons/dom'; +import { CoreMedia } from '@singletons/media'; /* * "Utils" service with helper functions for URLs. @@ -122,7 +122,7 @@ export class CoreUrlUtilsProvider { return !CoreConstants.CONFIG.disableTokenFile && !!accessKey && !url.match(/[&?]file=/) && ( url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 || url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0) && - !CoreDom.sourceUsesJavascriptPlayer({ src: url }); + !CoreMedia.sourceUsesJavascriptPlayer({ src: url }); } /** diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts index 08c408d32..bfcbae0b1 100644 --- a/src/core/singletons/dom.ts +++ b/src/core/singletons/dom.ts @@ -15,7 +15,6 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreApp } from '@services/app'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreUtils } from '@services/utils/utils'; import { CoreEventObserver } from '@singletons/events'; @@ -569,6 +568,7 @@ export class CoreDom { } } +<<<<<<< HEAD /** * Get all source URLs and types for a video or audio. * @@ -637,6 +637,8 @@ export class CoreDom { return sources.some(source => CoreDom.sourceUsesJavascriptPlayer(source)); } +======= +>>>>>>> f42ea632ca (a) } /** diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index 34d949097..10be74a3e 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -20,6 +20,7 @@ import { CoreFilepoolComponentFileEventData } from '@services/filepool'; import { CoreRedirectPayload } from '@services/navigator'; import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; import { CoreScreenOrientation } from '@services/screen'; +import { VideoJSPlayer } from 'video.js'; /** * Observer instance to stop listening to an event. @@ -499,5 +500,5 @@ export type CoreEventCompleteRequiredProfileDataFinished = { export type CoreEventJSVideoPlayerCreated = { id: string; element: HTMLAudioElement | HTMLVideoElement; - player: unknown; + player: VideoJSPlayer; }; diff --git a/src/core/singletons/media.ts b/src/core/singletons/media.ts new file mode 100644 index 000000000..cdf487cac --- /dev/null +++ b/src/core/singletons/media.ts @@ -0,0 +1,104 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CorePlatform } from '@services/platform'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; + +/** + * Singleton with helper functions for media. + */ +export class CoreMedia { + + // Avoid creating singleton instances. + private constructor() { + // Nothing to do. + } + + /** + * Get all source URLs and types for a video or audio. + * + * @param mediaElement Audio or video element. + * @returns List of sources. + */ + static getMediaSources(mediaElement: HTMLVideoElement | HTMLAudioElement): CoreMediaSource[] { + const sources = Array.from(mediaElement.querySelectorAll('source')).map(source => ({ + src: source.src || source.getAttribute('target-src') || '', + type: source.type, + })); + + if (mediaElement.src) { + sources.push({ + src: mediaElement.src, + type: '', + }); + } + + return sources; + } + + /** + * Check if a source needs to be converted to be able to reproduce it. + * + * @param source Source. + * @returns Whether needs conversion. + */ + static sourceNeedsConversion(source: CoreMediaSource): boolean { + if (!CorePlatform.isIOS()) { + return false; + } + + let extension = source.type ? CoreMimetypeUtils.getExtension(source.type) : undefined; + if (!extension) { + extension = CoreMimetypeUtils.guessExtensionFromUrl(source.src); + } + + return !!extension && ['ogv', 'webm', 'oga', 'ogg'].includes(extension); + } + + /** + * Check if JS player should be used for a certain source. + * + * @param source Source. + * @returns Whether JS player should be used. + */ + static sourceUsesJavascriptPlayer(source: CoreMediaSource): boolean { + // For now, only use JS player if the source needs to be converted. + return CoreMedia.sourceNeedsConversion(source); + } + + /** + * Check if JS player should be used for a certain audio or video. + * + * @param mediaElement Media element. + * @returns Whether JS player should be used. + */ + static mediaUsesJavascriptPlayer(mediaElement: HTMLVideoElement | HTMLAudioElement): boolean { + if (!CorePlatform.isIOS()) { + return false; + } + + const sources = CoreMedia.getMediaSources(mediaElement); + + return sources.some(source => CoreMedia.sourceUsesJavascriptPlayer(source)); + } + +} + +/** + * Source of a media element. + */ +export type CoreMediaSource = { + src: string; + type?: string; +}; diff --git a/src/theme/components/videojs.scss b/src/theme/components/videojs.scss new file mode 100644 index 000000000..13dd8f6a8 --- /dev/null +++ b/src/theme/components/videojs.scss @@ -0,0 +1,93 @@ +/** + * VideoJS modifications. + **/ + +/** + * App specific modifications. + **/ + +// In case of error reset height to the default (otherwise no aspect ratio is available and height becomes 0). +.video-js.vjs-error { + height: 150px !important; +} + +// Fake full screen mode. +.vjs-ios-moodleapp-fs { + z-index: 100000 !important; + + canvas { + max-width: 100%; + max-height: 100%; + left: 0px; + right: 0px; + margin-left: auto; + margin-right: auto; + padding-bottom: var(--ion-safe-area-bottom, 0px); + } + + .vjs-control-bar { + height: calc(3em + var(--ion-safe-area-bottom, 0px)); + + .vjs-progress-control { + padding-bottom: var(--ion-safe-area-bottom, 0px); + } + } +} + +/** + * Styles extracted from: + * https://github.com/moodle/moodle/blob/3c5423d8c0bae003e6eb20119ca657c0c6760309/media/player/videojs/styles.css#L1773 + **/ + +/* Audio: Remove big play button (leave only the button in controls). */ +.video-js.vjs-audio .vjs-big-play-button { + display: none; +} +/* Audio: Make the controlbar visible by default */ +.video-js.vjs-audio .vjs-control-bar { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +/* Make player height minimum to the controls height so when we hide video/poster area the controls are displayed correctly. */ +.video-js.vjs-audio { + min-height: 3em; +} +/* In case of error reset height to the default (otherwise no aspect ratio is available and height becomes 0). */ +.video-js.vjs-error { + height: 150px; +} +/* Minimum height for videos should not be less than the size of play button. */ +.mediaplugin_videojs video { + min-height: 32px; +} + +/* MDL-61020: VideoJS timeline progress bar should not be flipped in RTL mode. */ + +/* Prevent the progress bar from being flipped in RTL. */ +/*rtl:ignore*/ +.video-js .vjs-progress-holder .vjs-play-progress, +.video-js .vjs-progress-holder .vjs-load-progress, +.video-js .vjs-progress-holder .vjs-load-progress div { + left: 0; + right: auto; +} +/* Keep the video scrubber button at the end of the progress bar in RTL. */ +/*rtl:ignore*/ +.video-js .vjs-play-progress:before { + left: auto; + right: -0.5em; +} +/* Prevent the volume slider from being flipped in RTL. */ +/*rtl:ignore*/ +.video-js .vjs-volume-level { + left: 0; + right: auto; +} +/* Keep the volume slider handle at the end of the volume slider in RTL. */ +/*rtl:ignore*/ +.vjs-slider-horizontal .vjs-volume-level:before { + left: auto; + right: -0.5em; +} diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 1283e6499..26c7fbdd6 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -1809,63 +1809,3 @@ ion-modal.core-modal-no-background { display: none; } } - -/** - * VideoJS modifications. - * Styles extracted from the end of https://github.com/moodle/moodle/blob/master/media/player/videojs/styles.css - **/ - -/* Audio: Remove big play button (leave only the button in controls). */ -.video-js.vjs-audio .vjs-big-play-button { - display: none; -} -/* Audio: Make the controlbar visible by default */ -.video-js.vjs-audio .vjs-control-bar { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; -} -/* Make player height minimum to the controls height so when we hide video/poster area the controls are displayed correctly. */ -.video-js.vjs-audio { - min-height: 3em; -} -/* In case of error reset height to the default (otherwise no aspect ratio is available and height becomes 0). */ -.video-js.vjs-error { - height: 150px !important; -} -/* Minimum height for videos should not be less than the size of play button. */ -.mediaplugin_videojs video { - min-height: 32px; -} - -/* MDL-61020: VideoJS timeline progress bar should not be flipped in RTL mode. */ - -/* Prevent the progress bar from being flipped in RTL. */ -/*rtl:ignore*/ -.video-js .vjs-progress-holder .vjs-play-progress, -.video-js .vjs-progress-holder .vjs-load-progress, -.video-js .vjs-progress-holder .vjs-load-progress div { - left: 0; - right: auto; -} -/* Keep the video scrubber button at the end of the progress bar in RTL. */ -/*rtl:ignore*/ -.video-js .vjs-play-progress:before { - left: auto; - right: -0.5em; -} -/* Prevent the volume slider from being flipped in RTL. */ -/*rtl:ignore*/ -.video-js .vjs-volume-level { - left: 0; - right: auto; -} -/* Keep the volume slider handle at the end of the volume slider in RTL. */ -/*rtl:ignore*/ -.vjs-slider-horizontal .vjs-volume-level:before { - left: auto; - right: -0.5em; -} - -/** Finish VideoJS modifications. **/ diff --git a/src/theme/theme.scss b/src/theme/theme.scss index c13ffdfd5..5baa55e42 100644 --- a/src/theme/theme.scss +++ b/src/theme/theme.scss @@ -26,6 +26,7 @@ @import "./components/rubrics.scss"; @import "./components/mod-label.scss"; @import "../core/components/error-info/error-info.scss"; +@import "./components/videojs.scss"; /* Some styles from 3rd party libraries. */ @import "./bootstrap.scss"; diff --git a/src/types/videojs.d.ts b/src/types/videojs.d.ts new file mode 100644 index 000000000..53ab829c2 --- /dev/null +++ b/src/types/videojs.d.ts @@ -0,0 +1,115 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +declare module 'video.js' { + function videojs( + elementOrId: string | HTMLElement, + options?: VideoJSOptions, + readyCallback?: () => void, + ): VideoJSPlayer; + + namespace videojs { + function getPlayer(id: string): VideoJSPlayer | null; + function log(...args): void; + function getComponent(name: string): any; // eslint-disable-line @typescript-eslint/no-explicit-any + } + + export default videojs; + + export type VideoJSPlayer = { + play: () => Promise; + pause: () => Promise; + on: (name: string, callback: (ev: Event) => void) => void; + off: (name: string, callback: (ev: Event) => void) => void; + dispose: () => void; + el: () => HTMLElement; + fluid: (val?: boolean) => void | boolean; + isFullscreen: (val?: boolean) => void | boolean; + videoHeight: () => number; + videoWidth: () => number; + currentDimensions: () => { width: number; height: number }; + dimension: (dimension: string, value: number) => void; + }; + + export type VideoJSOptions = { + aspectRatio?: string; + audioOnlyMode?: boolean; + audioPosterMode?: boolean; + autoplay?: boolean | string; + autoSetup?: boolean; + base?: string; + breakpoints?: Record; + children?: string[] | Record>; + controlBar?: { + fullscreenToggle?: boolean; + pictureInPictureToggle?: boolean; + remainingTimeDisplay?: { + displayNegative?: boolean; + }; + }; + controls?: boolean; + fluid?: boolean; + fullscreen?: { + options?: Record; + }; + height?: string | number; + id?: string; + inactivityTimeout?: number; + language?: string; + languages?: Record>; + liveui?: boolean; + liveTracker?: { + trackingThreshold?: number; + liveTolerance?: number; + }; + loop?: boolean; + muted?: boolean; + nativeControlsForTouch?: boolean; + normalizeAutoplay?: boolean; + notSupportedMessage?: string; + noUITitleAttributes?: boolean; + playbackRates?: number[]; + plugins?: Record>; + poster?: string; + preferFullWindow?: boolean; + preload?: PreloadOption; + responsive?: boolean; + restoreEl?: boolean | HTMLElement; + source?: TechSourceObject; + sources?: TechSourceObject[]; + src?: string; + suppressNotSupportedError?: boolean; + tag?: HTMLElement; + techCanOverridePoster?: boolean; + techOrder?: string[]; + userActions?: { + click?: boolean | ((ev: MouseEvent) => void); + doubleClick?: boolean | ((ev: MouseEvent) => void); + hotkeys?: boolean | ((ev: KeyboardEvent) => void) | { + fullscreenKey?: (ev: KeyboardEvent) => void; + muteKey?: (ev: KeyboardEvent) => void; + playPauseKey?: (ev: KeyboardEvent) => void; + }; + }; + 'vtt.js'?: string; + width?: string | number; + }; + + export type TechSourceObject = { + src: string; // Source URL. + type: string; // Mimetype. + }; + + export type PreloadOption = '' | 'none' | 'metadata' | 'auto'; +}