MOBILE-4166 videojs: Support fullscreen and improve types
This commit is contained in:
		
							parent
							
								
									9b011ba350
								
							
						
					
					
						commit
						884827afb6
					
				| @ -14,7 +14,7 @@ | |||||||
| 
 | 
 | ||||||
| import { CorePlatform } from '@services/platform'; | import { CorePlatform } from '@services/platform'; | ||||||
| import { OGVPlayer, OGVCompat, OGVLoader } from 'ogv'; | 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'); | export const Tech = videojs.getComponent('Tech'); | ||||||
| 
 | 
 | ||||||
| @ -95,6 +95,10 @@ export class VideoJSOgvJS extends Tech { | |||||||
|         'volumechange', |         'volumechange', | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|  |     protected playerId?: string; | ||||||
|  |     protected parentElement: HTMLElement | null = null; | ||||||
|  |     protected placeholderElement = document.createElement('div'); | ||||||
|  | 
 | ||||||
|     // Variables/functions defined in parent classes.
 |     // Variables/functions defined in parent classes.
 | ||||||
|     protected el_!: OGVPlayerEl; // eslint-disable-line @typescript-eslint/naming-convention
 |     protected el_!: OGVPlayerEl; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||||
|     protected options_!: VideoJSOptions; // 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 options The key/value store of player options. | ||||||
|      * @param ready Callback function to call when the `OgvJS` Tech is ready. |      * @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); |         super(options, ready); | ||||||
| 
 | 
 | ||||||
|         this.el_.src = options.src || options.source?.src || options.sources?.[0]?.src || this.el_.src; |         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_, 'loop', options.loop); | ||||||
|         VideoJSOgvJS.setIfAvailable(this.el_, 'poster', options.poster); |         VideoJSOgvJS.setIfAvailable(this.el_, 'poster', options.poster); | ||||||
|         VideoJSOgvJS.setIfAvailable(this.el_, 'preload', options.preload); |         VideoJSOgvJS.setIfAvailable(this.el_, 'preload', options.preload); | ||||||
|  |         this.playerId = options.playerId; | ||||||
| 
 | 
 | ||||||
|         this.on('loadedmetadata', () => { |         this.on('loadedmetadata', () => { | ||||||
|             if (CoreApp.isIPhone()) { |             if (CoreApp.isIPhone()) { | ||||||
| @ -654,8 +659,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|      * @returns Whether it supports full screen. |      * @returns Whether it supports full screen. | ||||||
|      */ |      */ | ||||||
|     supportsFullScreen(): boolean { |     supportsFullScreen(): boolean { | ||||||
|         // iOS devices have some problem with HTML5 fullscreen api so we need to fallback to fullWindow mode.
 |         return !!this.playerId; | ||||||
|         return !CoreApp.isIOS(); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -667,6 +671,50 @@ export class VideoJSOgvJS extends Tech { | |||||||
|         return this.el_.error; |         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); |     Tech.registerTech('OgvJS', VideoJSOgvJS); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export type VideoJSOptions = { |  | ||||||
|     aspectRatio?: string; |  | ||||||
|     audioOnlyMode?: boolean; |  | ||||||
|     audioPosterMode?: boolean; |  | ||||||
|     autoplay?: boolean | string; |  | ||||||
|     autoSetup?: boolean; |  | ||||||
|     base?: string; |  | ||||||
|     breakpoints?: Record<string, number>; |  | ||||||
|     children?: string[] | Record<string, Record<string, unknown>>; |  | ||||||
|     controlBar?: { |  | ||||||
|         remainingTimeDisplay?: { |  | ||||||
|             displayNegative?: boolean; |  | ||||||
|         }; |  | ||||||
|     }; |  | ||||||
|     controls?: boolean; |  | ||||||
|     fluid?: boolean; |  | ||||||
|     fullscreen?: { |  | ||||||
|         options?: Record<string, unknown>; |  | ||||||
|     }; |  | ||||||
|     height?: string | number; |  | ||||||
|     id?: string; |  | ||||||
|     inactivityTimeout?: number; |  | ||||||
|     language?: string; |  | ||||||
|     languages?: Record<string, Record<string, string>>; |  | ||||||
|     liveui?: boolean; |  | ||||||
|     liveTracker?: { |  | ||||||
|         trackingThreshold?: number; |  | ||||||
|         liveTolerance?: number; |  | ||||||
|     }; |  | ||||||
|     loop?: boolean; |  | ||||||
|     muted?: boolean; |  | ||||||
|     nativeControlsForTouch?: boolean; |  | ||||||
|     normalizeAutoplay?: boolean; |  | ||||||
|     notSupportedMessage?: string; |  | ||||||
|     noUITitleAttributes?: boolean; |  | ||||||
|     playbackRates?: number[]; |  | ||||||
|     plugins?: Record<string, Record<string, unknown>>; |  | ||||||
|     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) & { | type OGVPlayerEl = (HTMLAudioElement | HTMLVideoElement) & { | ||||||
|     stop: () => void; |     stop: () => void; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * VideoJS Tech options. It includes some options added by VideoJS internally. | ||||||
|  |  */ | ||||||
|  | type VideoJSTechOptions = VideoJSOptions & { | ||||||
|  |     playerId?: string; | ||||||
|  | }; | ||||||
|  | |||||||
| @ -21,10 +21,9 @@ import { CoreTextUtils } from '@services/utils/text'; | |||||||
| import { CoreUrlUtils } from '@services/utils/url'; | import { CoreUrlUtils } from '@services/utils/url'; | ||||||
| import { makeSingleton } from '@singletons'; | import { makeSingleton } from '@singletons'; | ||||||
| import { CoreDirectivesRegistry } from '@singletons/directives-registry'; | import { CoreDirectivesRegistry } from '@singletons/directives-registry'; | ||||||
| import { CoreDom } from '@singletons/dom'; |  | ||||||
| import { CoreEvents } from '@singletons/events'; | import { CoreEvents } from '@singletons/events'; | ||||||
| import videojs from 'video.js'; | import { CoreMedia } from '@singletons/media'; | ||||||
| import { VideoJSOptions } from '../../classes/videojs-ogvjs'; | import videojs, { VideoJSOptions, VideoJSPlayer } from 'video.js'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Handler to support the Multimedia filter. |  * Handler to support the Multimedia filter. | ||||||
| @ -48,7 +47,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl | |||||||
|         const videos = Array.from(this.template.content.querySelectorAll('video')); |         const videos = Array.from(this.template.content.querySelectorAll('video')); | ||||||
| 
 | 
 | ||||||
|         videos.forEach((video) => { |         videos.forEach((video) => { | ||||||
|             this.treatVideoFilters(video); |             this.treatYoutubeVideos(video); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         return this.template.innerHTML; |         return this.template.innerHTML; | ||||||
| @ -61,7 +60,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl | |||||||
|         const mediaElements = Array.from(container.querySelectorAll<HTMLVideoElement | HTMLAudioElement>('video, audio')); |         const mediaElements = Array.from(container.querySelectorAll<HTMLVideoElement | HTMLAudioElement>('video, audio')); | ||||||
| 
 | 
 | ||||||
|         mediaElements.forEach((mediaElement) => { |         mediaElements.forEach((mediaElement) => { | ||||||
|             if (CoreDom.mediaUsesJavascriptPlayer(mediaElement)) { |             if (CoreMedia.mediaUsesJavascriptPlayer(mediaElement)) { | ||||||
|                 this.useVideoJS(mediaElement); |                 this.useVideoJS(mediaElement); | ||||||
|             } else { |             } else { | ||||||
|                 // Remove the VideoJS classes and data if present.
 |                 // Remove the VideoJS classes and data if present.
 | ||||||
| @ -93,11 +92,14 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl | |||||||
|             controls: true, |             controls: true, | ||||||
|             techOrder: ['OgvJS'], |             techOrder: ['OgvJS'], | ||||||
|             language: lang, |             language: lang, | ||||||
|             fluid: true, |  | ||||||
|             controlBar: { |             controlBar: { | ||||||
|                 fullscreenToggle: false, |                 pictureInPictureToggle: false, | ||||||
|             }, |             }, | ||||||
|             aspectRatio: data.aspectRatio, |             aspectRatio: data.aspectRatio, | ||||||
|  |         }, () => { | ||||||
|  |             if (mediaElement.tagName === 'VIDEO') { | ||||||
|  |                 this.fixVideoJSPlayerSize(player); | ||||||
|  |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         CoreEvents.trigger(CoreEvents.JS_PLAYER_CREATED, { |         CoreEvents.trigger(CoreEvents.JS_PLAYER_CREATED, { | ||||||
| @ -105,16 +107,34 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl | |||||||
|             element: mediaElement, |             element: mediaElement, | ||||||
|             player, |             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. |      * @param video Video element. | ||||||
|      */ |      */ | ||||||
|     protected treatVideoFilters(video: HTMLElement): void { |     protected treatYoutubeVideos(video: HTMLElement): void { | ||||||
|         // Treat Video JS Youtube video links and translate them to iframes.
 |  | ||||||
|         if (!video.classList.contains('video-js')) { |         if (!video.classList.contains('video-js')) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -14,10 +14,10 @@ | |||||||
| 
 | 
 | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { ElementController } from './ElementController'; | import { ElementController } from './ElementController'; | ||||||
| import videojs from 'video.js'; | import videojs, { VideoJSPlayer } from 'video.js'; | ||||||
| import { CoreDom } from '@singletons/dom'; |  | ||||||
| import { CorePromisedValue } from '@classes/promised-value'; | import { CorePromisedValue } from '@classes/promised-value'; | ||||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||||
|  | import { CoreMedia } from '@singletons/media'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Wrapper class to control the interactivity of a media element. |  * Wrapper class to control the interactivity of a media element. | ||||||
| @ -42,7 +42,7 @@ export class MediaElementController extends ElementController { | |||||||
| 
 | 
 | ||||||
|         media.autoplay = false; |         media.autoplay = false; | ||||||
| 
 | 
 | ||||||
|         if (CoreDom.mediaUsesJavascriptPlayer(media)) { |         if (CoreMedia.mediaUsesJavascriptPlayer(media)) { | ||||||
|             const player = this.searchJSPlayer(); |             const player = this.searchJSPlayer(); | ||||||
|             if (player) { |             if (player) { | ||||||
|                 this.jsPlayer.resolve(player); |                 this.jsPlayer.resolve(player); | ||||||
| @ -50,7 +50,7 @@ export class MediaElementController extends ElementController { | |||||||
|                 this.jsPlayerListener = CoreEvents.on(CoreEvents.JS_PLAYER_CREATED, data => { |                 this.jsPlayerListener = CoreEvents.on(CoreEvents.JS_PLAYER_CREATED, data => { | ||||||
|                     if (data.element === media) { |                     if (data.element === media) { | ||||||
|                         this.jsPlayerListener?.off(); |                         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. |      * @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', '')); |         return videojs.getPlayer(this.media.id) || videojs.getPlayer(this.media.id.replace('_html5_api', '')); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| type VideoJSPlayer = { |  | ||||||
|     play: () => Promise<void>; |  | ||||||
|     pause: () => Promise<void>; |  | ||||||
|     on: (name: string, callback: (ev: Event) => void) => void; |  | ||||||
|     off: (name: string, callback: (ev: Event) => void) => void; |  | ||||||
|     dispose: () => void; |  | ||||||
| }; |  | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ import { CoreUrl } from '@singletons/url'; | |||||||
| import { CoreSites } from '@services/sites'; | import { CoreSites } from '@services/sites'; | ||||||
| import { CorePath } from '@singletons/path'; | import { CorePath } from '@singletons/path'; | ||||||
| import { CorePlatform } from '@services/platform'; | import { CorePlatform } from '@services/platform'; | ||||||
| import { CoreDom } from '@singletons/dom'; | import { CoreMedia } from '@singletons/media'; | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
|  * "Utils" service with helper functions for URLs. |  * "Utils" service with helper functions for URLs. | ||||||
| @ -122,7 +122,7 @@ export class CoreUrlUtilsProvider { | |||||||
|         return !CoreConstants.CONFIG.disableTokenFile && !!accessKey && !url.match(/[&?]file=/) && ( |         return !CoreConstants.CONFIG.disableTokenFile && !!accessKey && !url.match(/[&?]file=/) && ( | ||||||
|             url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 || |             url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 || | ||||||
|             url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0) && |             url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0) && | ||||||
|             !CoreDom.sourceUsesJavascriptPlayer({ src: url }); |             !CoreMedia.sourceUsesJavascriptPlayer({ src: url }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -15,7 +15,6 @@ | |||||||
| import { CoreCancellablePromise } from '@classes/cancellable-promise'; | import { CoreCancellablePromise } from '@classes/cancellable-promise'; | ||||||
| import { CoreApp } from '@services/app'; | import { CoreApp } from '@services/app'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreMimetypeUtils } from '@services/utils/mimetype'; |  | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { CoreEventObserver } from '@singletons/events'; | import { CoreEventObserver } from '@singletons/events'; | ||||||
| 
 | 
 | ||||||
| @ -569,6 +568,7 @@ export class CoreDom { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | <<<<<<< HEAD | ||||||
|     /** |     /** | ||||||
|      * Get all source URLs and types for a video or audio. |      * 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)); |         return sources.some(source => CoreDom.sourceUsesJavascriptPlayer(source)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | ======= | ||||||
|  | >>>>>>> f42ea632ca (a) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ import { CoreFilepoolComponentFileEventData } from '@services/filepool'; | |||||||
| import { CoreRedirectPayload } from '@services/navigator'; | import { CoreRedirectPayload } from '@services/navigator'; | ||||||
| import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; | import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; | ||||||
| import { CoreScreenOrientation } from '@services/screen'; | import { CoreScreenOrientation } from '@services/screen'; | ||||||
|  | import { VideoJSPlayer } from 'video.js'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Observer instance to stop listening to an event. |  * Observer instance to stop listening to an event. | ||||||
| @ -499,5 +500,5 @@ export type CoreEventCompleteRequiredProfileDataFinished = { | |||||||
| export type CoreEventJSVideoPlayerCreated = { | export type CoreEventJSVideoPlayerCreated = { | ||||||
|     id: string; |     id: string; | ||||||
|     element: HTMLAudioElement | HTMLVideoElement; |     element: HTMLAudioElement | HTMLVideoElement; | ||||||
|     player: unknown; |     player: VideoJSPlayer; | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										104
									
								
								src/core/singletons/media.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/core/singletons/media.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||||
|  | }; | ||||||
							
								
								
									
										93
									
								
								src/theme/components/videojs.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/theme/components/videojs.scss
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||||
|  | } | ||||||
| @ -1809,63 +1809,3 @@ ion-modal.core-modal-no-background { | |||||||
|         display: none; |         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. **/ |  | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ | |||||||
| @import "./components/rubrics.scss"; | @import "./components/rubrics.scss"; | ||||||
| @import "./components/mod-label.scss"; | @import "./components/mod-label.scss"; | ||||||
| @import "../core/components/error-info/error-info.scss"; | @import "../core/components/error-info/error-info.scss"; | ||||||
|  | @import "./components/videojs.scss"; | ||||||
| 
 | 
 | ||||||
| /* Some styles from 3rd party libraries. */ | /* Some styles from 3rd party libraries. */ | ||||||
| @import "./bootstrap.scss"; | @import "./bootstrap.scss"; | ||||||
|  | |||||||
							
								
								
									
										115
									
								
								src/types/videojs.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/types/videojs.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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<void>; | ||||||
|  |         pause: () => Promise<void>; | ||||||
|  |         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<string, number>; | ||||||
|  |         children?: string[] | Record<string, Record<string, unknown>>; | ||||||
|  |         controlBar?: { | ||||||
|  |             fullscreenToggle?: boolean; | ||||||
|  |             pictureInPictureToggle?: boolean; | ||||||
|  |             remainingTimeDisplay?: { | ||||||
|  |                 displayNegative?: boolean; | ||||||
|  |             }; | ||||||
|  |         }; | ||||||
|  |         controls?: boolean; | ||||||
|  |         fluid?: boolean; | ||||||
|  |         fullscreen?: { | ||||||
|  |             options?: Record<string, unknown>; | ||||||
|  |         }; | ||||||
|  |         height?: string | number; | ||||||
|  |         id?: string; | ||||||
|  |         inactivityTimeout?: number; | ||||||
|  |         language?: string; | ||||||
|  |         languages?: Record<string, Record<string, string>>; | ||||||
|  |         liveui?: boolean; | ||||||
|  |         liveTracker?: { | ||||||
|  |             trackingThreshold?: number; | ||||||
|  |             liveTolerance?: number; | ||||||
|  |         }; | ||||||
|  |         loop?: boolean; | ||||||
|  |         muted?: boolean; | ||||||
|  |         nativeControlsForTouch?: boolean; | ||||||
|  |         normalizeAutoplay?: boolean; | ||||||
|  |         notSupportedMessage?: string; | ||||||
|  |         noUITitleAttributes?: boolean; | ||||||
|  |         playbackRates?: number[]; | ||||||
|  |         plugins?: Record<string, Record<string, unknown>>; | ||||||
|  |         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'; | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user