MOBILE-4166 videojs: Support fullscreen and improve types
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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -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";
|
||||||
|
|
|
@ -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…
Reference in New Issue