MOBILE-4166 core: Use VideoJS in iOS for unsupported media
parent
47e5158afe
commit
9419db02a1
|
@ -28,6 +28,8 @@ const ASSETS = {
|
||||||
'/node_modules/mathjax/jax/output/PreviewHTML': '/lib/mathjax/jax/output/PreviewHTML',
|
'/node_modules/mathjax/jax/output/PreviewHTML': '/lib/mathjax/jax/output/PreviewHTML',
|
||||||
'/node_modules/mathjax/localization': '/lib/mathjax/localization',
|
'/node_modules/mathjax/localization': '/lib/mathjax/localization',
|
||||||
'/src/core/features/h5p/assets': '/lib/h5p',
|
'/src/core/features/h5p/assets': '/lib/h5p',
|
||||||
|
'/node_modules/ogv/dist': '/lib/ogv',
|
||||||
|
'/node_modules/video.js/dist/video-js.min.css': '/lib/video.js/video-js.min.css',
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function(ctx) {
|
||||||
|
|
|
@ -0,0 +1,762 @@
|
||||||
|
// (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 { OGVPlayer, OGVCompat, OGVLoader } from 'ogv';
|
||||||
|
import videojs from 'video.js';
|
||||||
|
|
||||||
|
export const Tech = videojs.getComponent('Tech');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object.defineProperty but "lazy", which means that the value is only set after
|
||||||
|
* it retrieved the first time, rather than being set right away.
|
||||||
|
*
|
||||||
|
* @param obj The object to set the property on.
|
||||||
|
* @param key The key for the property to set.
|
||||||
|
* @param getValue The function used to get the value when it is needed.
|
||||||
|
* @param setter Whether a setter should be allowed or not.
|
||||||
|
* @returns Object.
|
||||||
|
*/
|
||||||
|
const defineLazyProperty = <T>(obj: T, key: string, getValue: () => unknown, setter = true): T => {
|
||||||
|
const set = (value: unknown): void => {
|
||||||
|
Object.defineProperty(obj, key, { value, enumerable: true, writable: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: PropertyDescriptor = {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
get() {
|
||||||
|
const value = getValue();
|
||||||
|
|
||||||
|
set(value);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (setter) {
|
||||||
|
options.set = set;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.defineProperty(obj, key, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OgvJS Media Controller for VideoJS - Wrapper for ogv.js Media API.
|
||||||
|
*
|
||||||
|
* Code adapted from https://github.com/HuongNV13/videojs-ogvjs/blob/f9b12bd53018d967bb305f02725834a98f20f61f/src/plugin.js
|
||||||
|
* Modified in the following ways:
|
||||||
|
* - Adapted to Typescript.
|
||||||
|
* - Use our own functions to detect the platform instead of using getDeviceOS.
|
||||||
|
* - Add an initialize static function.
|
||||||
|
* - In the play function, reset the media if it already ended to fix problems with replaying media.
|
||||||
|
* - Allow full screen in iOS devices, and implement enterFullScreen and exitFullScreen to use a fake full screen.
|
||||||
|
*/
|
||||||
|
export class VideoJSOgvJS extends Tech {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of available events of the media player.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
static readonly Events = [
|
||||||
|
'loadstart',
|
||||||
|
'suspend',
|
||||||
|
'abort',
|
||||||
|
'error',
|
||||||
|
'emptied',
|
||||||
|
'stalled',
|
||||||
|
'loadedmetadata',
|
||||||
|
'loadeddata',
|
||||||
|
'canplay',
|
||||||
|
'canplaythrough',
|
||||||
|
'playing',
|
||||||
|
'waiting',
|
||||||
|
'seeking',
|
||||||
|
'seeked',
|
||||||
|
'ended',
|
||||||
|
'durationchange',
|
||||||
|
'timeupdate',
|
||||||
|
'progress',
|
||||||
|
'play',
|
||||||
|
'pause',
|
||||||
|
'ratechange',
|
||||||
|
'resize',
|
||||||
|
'volumechange',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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
|
||||||
|
protected currentSource_?: TechSourceObject; // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
protected triggerReady!: () => void;
|
||||||
|
protected on!: (name: string, callback: (e?: Event) => void) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of this 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) {
|
||||||
|
super(options, ready);
|
||||||
|
|
||||||
|
this.el_.src = options.src || options.source?.src || options.sources?.[0]?.src || this.el_.src;
|
||||||
|
VideoJSOgvJS.setIfAvailable(this.el_, 'autoplay', options.autoplay);
|
||||||
|
VideoJSOgvJS.setIfAvailable(this.el_, 'loop', options.loop);
|
||||||
|
VideoJSOgvJS.setIfAvailable(this.el_, 'poster', options.poster);
|
||||||
|
VideoJSOgvJS.setIfAvailable(this.el_, 'preload', options.preload);
|
||||||
|
|
||||||
|
this.on('loadedmetadata', () => {
|
||||||
|
if (CoreApp.isIPhone()) {
|
||||||
|
// iPhoneOS add some inline styles to the canvas, we need to remove it.
|
||||||
|
const canvas = this.el_.getElementsByTagName('canvas')[0];
|
||||||
|
|
||||||
|
canvas.style.removeProperty('width');
|
||||||
|
canvas.style.removeProperty('margin');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.triggerReady();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value for the player is it has that property.
|
||||||
|
*
|
||||||
|
* @param el HTML player.
|
||||||
|
* @param name Name of the property.
|
||||||
|
* @param value Value to set.
|
||||||
|
*/
|
||||||
|
static setIfAvailable(el: HTMLElement, name: string, value: unknown): void {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (el.hasOwnProperty(name)) {
|
||||||
|
el[name] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if browser/device is supported by Ogv.JS.
|
||||||
|
*
|
||||||
|
* @returns Whether it's supported.
|
||||||
|
*/
|
||||||
|
static isSupported(): boolean {
|
||||||
|
return OGVCompat.supported('OGVPlayer');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the tech can support the given type.
|
||||||
|
*
|
||||||
|
* @param type The mimetype to check.
|
||||||
|
* @returns 'probably', 'maybe', or '' (empty string).
|
||||||
|
*/
|
||||||
|
static canPlayType(type: string): string {
|
||||||
|
return (type.indexOf('/ogg') !== -1 || type.indexOf('/webm')) ? 'maybe' : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the tech can support the given source.
|
||||||
|
*
|
||||||
|
* @param srcObj The source object.
|
||||||
|
* @returns The options passed to the tech.
|
||||||
|
*/
|
||||||
|
static canPlaySource(srcObj: TechSourceObject): string {
|
||||||
|
return VideoJSOgvJS.canPlayType(srcObj.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the volume can be changed in this browser/device.
|
||||||
|
* Volume cannot be changed in a lot of mobile devices.
|
||||||
|
* Specifically, it can't be changed from 1 on iOS.
|
||||||
|
*
|
||||||
|
* @returns True if volume can be controlled.
|
||||||
|
*/
|
||||||
|
static canControlVolume(): boolean {
|
||||||
|
if (CoreApp.isIPhone()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = new OGVPlayer();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
return player.hasOwnProperty('volume');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the volume can be muted in this browser/device.
|
||||||
|
*
|
||||||
|
* @returns True if volume can be muted.
|
||||||
|
*/
|
||||||
|
static canMuteVolume(): boolean {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the playback rate can be changed in this browser/device.
|
||||||
|
*
|
||||||
|
* @returns True if playback rate can be controlled.
|
||||||
|
*/
|
||||||
|
static canControlPlaybackRate(): boolean {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if native 'TextTracks' are supported by this browser/device.
|
||||||
|
*
|
||||||
|
* @returns True if native 'TextTracks' are supported.
|
||||||
|
*/
|
||||||
|
static supportsNativeTextTracks(): boolean {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the fullscreen resize is supported by this browser/device.
|
||||||
|
*
|
||||||
|
* @returns True if the fullscreen resize is supported.
|
||||||
|
*/
|
||||||
|
static supportsFullscreenResize(): boolean {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the progress events is supported by this browser/device.
|
||||||
|
*
|
||||||
|
* @returns True if the progress events is supported.
|
||||||
|
*/
|
||||||
|
static supportsProgressEvents(): boolean {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the time update events is supported by this browser/device.
|
||||||
|
*
|
||||||
|
* @returns True if the time update events is supported.
|
||||||
|
*/
|
||||||
|
static supportsTimeupdateEvents(): boolean {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the 'OgvJS' Tech's DOM element.
|
||||||
|
*
|
||||||
|
* @returns The element that gets created.
|
||||||
|
*/
|
||||||
|
createEl(): OGVPlayerEl {
|
||||||
|
const options = this.options_;
|
||||||
|
|
||||||
|
if (options.base) {
|
||||||
|
OGVLoader.base = options.base;
|
||||||
|
} else if (!OGVLoader.base) {
|
||||||
|
throw new Error('Please specify the base for the ogv.js library');
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = new OGVPlayer(options);
|
||||||
|
|
||||||
|
el.className += ' vjs-tech';
|
||||||
|
options.tag = el;
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start playback.
|
||||||
|
*/
|
||||||
|
play(): void {
|
||||||
|
if (this.ended()) {
|
||||||
|
// Reset the player, otherwise the Replay button doesn't work.
|
||||||
|
this.el_.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.el_.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current playback speed.
|
||||||
|
*
|
||||||
|
* @returns Playback speed.
|
||||||
|
*/
|
||||||
|
playbackRate(): number {
|
||||||
|
return this.el_.playbackRate || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the playback speed.
|
||||||
|
*
|
||||||
|
* @param val Speed for the player to play.
|
||||||
|
*/
|
||||||
|
setPlaybackRate(val: number): void {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (this.el_.hasOwnProperty('playbackRate')) {
|
||||||
|
this.el_.playbackRate = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a TimeRanges object that represents the ranges of the media resource that the user agent has played.
|
||||||
|
*
|
||||||
|
* @returns The range of points on the media timeline that has been reached through normal playback.
|
||||||
|
*/
|
||||||
|
played(): TimeRanges {
|
||||||
|
return this.el_.played;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause playback.
|
||||||
|
*/
|
||||||
|
pause(): void {
|
||||||
|
this.el_.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the player paused or not.
|
||||||
|
*
|
||||||
|
* @returns Whether is paused.
|
||||||
|
*/
|
||||||
|
paused(): boolean {
|
||||||
|
return this.el_.paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current playing time.
|
||||||
|
*
|
||||||
|
* @returns Current time.
|
||||||
|
*/
|
||||||
|
currentTime(): number {
|
||||||
|
return this.el_.currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set current playing time.
|
||||||
|
*
|
||||||
|
* @param seconds Current time of audio/video.
|
||||||
|
*/
|
||||||
|
setCurrentTime(seconds: number): void {
|
||||||
|
try {
|
||||||
|
this.el_.currentTime = seconds;
|
||||||
|
} catch (e) {
|
||||||
|
videojs.log(e, 'Media is not ready. (Video.JS)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get media's duration.
|
||||||
|
*
|
||||||
|
* @returns Duration.
|
||||||
|
*/
|
||||||
|
duration(): number {
|
||||||
|
if (this.el_.duration && this.el_.duration !== Infinity) {
|
||||||
|
return this.el_.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a TimeRange object that represents the intersection
|
||||||
|
* of the time ranges for which the user agent has all
|
||||||
|
* relevant media.
|
||||||
|
*
|
||||||
|
* @returns Time ranges.
|
||||||
|
*/
|
||||||
|
buffered(): TimeRanges {
|
||||||
|
return this.el_.buffered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current volume level.
|
||||||
|
*
|
||||||
|
* @returns Volume.
|
||||||
|
*/
|
||||||
|
volume(): number {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
return this.el_.hasOwnProperty('volume') ? this.el_.volume : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set current playing volume level.
|
||||||
|
*
|
||||||
|
* @param percentAsDecimal Volume percent as a decimal.
|
||||||
|
*/
|
||||||
|
setVolume(percentAsDecimal: number): void {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (!CoreApp.isIPhone() && this.el_.hasOwnProperty('volume')) {
|
||||||
|
this.el_.volume = percentAsDecimal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the player muted or not.
|
||||||
|
*
|
||||||
|
* @returns Whether it's muted.
|
||||||
|
*/
|
||||||
|
muted(): boolean {
|
||||||
|
return this.el_.muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute the player.
|
||||||
|
*
|
||||||
|
* @param muted True to mute the player.
|
||||||
|
*/
|
||||||
|
setMuted(muted: boolean): void {
|
||||||
|
this.el_.muted = !!muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the player muted by default or not.
|
||||||
|
*
|
||||||
|
* @returns Whether it's muted by default.
|
||||||
|
*/
|
||||||
|
defaultMuted(): boolean {
|
||||||
|
return this.el_.defaultMuted || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the player width.
|
||||||
|
*
|
||||||
|
* @returns Width.
|
||||||
|
*/
|
||||||
|
width(): number {
|
||||||
|
return this.el_.offsetWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the player height.
|
||||||
|
*
|
||||||
|
* @returns Height.
|
||||||
|
*/
|
||||||
|
height(): number {
|
||||||
|
return this.el_.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the video width.
|
||||||
|
*
|
||||||
|
* @returns Video width.
|
||||||
|
*/
|
||||||
|
videoWidth(): number {
|
||||||
|
return (<HTMLVideoElement> this.el_).videoWidth ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the video height.
|
||||||
|
*
|
||||||
|
* @returns Video heigth.
|
||||||
|
*/
|
||||||
|
videoHeight(): number {
|
||||||
|
return (<HTMLVideoElement> this.el_).videoHeight ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set media source.
|
||||||
|
*
|
||||||
|
* @param src Source.
|
||||||
|
* @returns Source when getting it, undefined when setting it.
|
||||||
|
*/
|
||||||
|
src(src?: string): string | undefined {
|
||||||
|
if (typeof src === 'undefined') {
|
||||||
|
return this.el_.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.el_.src = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the media into the player.
|
||||||
|
*/
|
||||||
|
load(): void {
|
||||||
|
this.el_.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current media source.
|
||||||
|
*
|
||||||
|
* @returns Current source.
|
||||||
|
*/
|
||||||
|
currentSrc(): string {
|
||||||
|
if (this.currentSource_) {
|
||||||
|
return this.currentSource_.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.el_.currentSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get media poster URL.
|
||||||
|
*
|
||||||
|
* @returns Poster.
|
||||||
|
*/
|
||||||
|
poster(): string {
|
||||||
|
return 'poster' in this.el_ ? this.el_.poster : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set media poster URL.
|
||||||
|
*
|
||||||
|
* @param url The poster image's url.
|
||||||
|
*/
|
||||||
|
setPoster(url: string): void {
|
||||||
|
(<HTMLVideoElement> this.el_).poster = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the media preloaded or not.
|
||||||
|
*
|
||||||
|
* @returns Whether it's preloaded.
|
||||||
|
*/
|
||||||
|
preload(): PreloadOption {
|
||||||
|
return <PreloadOption> this.el_.preload || 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the media preload method.
|
||||||
|
*
|
||||||
|
* @param val Value for preload attribute.
|
||||||
|
*/
|
||||||
|
setPreload(val: PreloadOption): void {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (this.el_.hasOwnProperty('preload')) {
|
||||||
|
this.el_.preload = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the media auto-played or not.
|
||||||
|
*
|
||||||
|
* @returns Whether it's auto-played.
|
||||||
|
*/
|
||||||
|
autoplay(): boolean {
|
||||||
|
return this.el_.autoplay || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set media autoplay method.
|
||||||
|
*
|
||||||
|
* @param val Value for autoplay attribute.
|
||||||
|
*/
|
||||||
|
setAutoplay(val: boolean): void {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (this.el_.hasOwnProperty('autoplay')) {
|
||||||
|
this.el_.autoplay = !!val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the media has controls or not.
|
||||||
|
*
|
||||||
|
* @returns Whether it has controls.
|
||||||
|
*/
|
||||||
|
controls(): boolean {
|
||||||
|
return this.el_.controls || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the media controls method.
|
||||||
|
*
|
||||||
|
* @param val Value for controls attribute.
|
||||||
|
*/
|
||||||
|
setControls(val: boolean): void {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (this.el_.hasOwnProperty('controls')) {
|
||||||
|
this.el_.controls = !!val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the media looped or not.
|
||||||
|
*
|
||||||
|
* @returns Whether it's looped.
|
||||||
|
*/
|
||||||
|
loop(): boolean {
|
||||||
|
return this.el_.loop || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the media loop method.
|
||||||
|
*
|
||||||
|
* @param val Value for loop attribute.
|
||||||
|
*/
|
||||||
|
setLoop(val: boolean): void {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (this.el_.hasOwnProperty('loop')) {
|
||||||
|
this.el_.loop = !!val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a TimeRanges object that represents the
|
||||||
|
* ranges of the media resource to which it is possible
|
||||||
|
* for the user agent to seek.
|
||||||
|
*
|
||||||
|
* @returns Time ranges.
|
||||||
|
*/
|
||||||
|
seekable(): TimeRanges {
|
||||||
|
return this.el_.seekable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is player in the "seeking" state or not.
|
||||||
|
*
|
||||||
|
* @returns Whether is in the seeking state.
|
||||||
|
*/
|
||||||
|
seeking(): boolean {
|
||||||
|
return this.el_.seeking;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the media ended or not.
|
||||||
|
*
|
||||||
|
* @returns Whether it's ended.
|
||||||
|
*/
|
||||||
|
ended(): boolean {
|
||||||
|
return this.el_.ended;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current state of network activity
|
||||||
|
* NETWORK_EMPTY (numeric value 0)
|
||||||
|
* NETWORK_IDLE (numeric value 1)
|
||||||
|
* NETWORK_LOADING (numeric value 2)
|
||||||
|
* NETWORK_NO_SOURCE (numeric value 3)
|
||||||
|
*
|
||||||
|
* @returns Network state.
|
||||||
|
*/
|
||||||
|
networkState(): number {
|
||||||
|
return this.el_.networkState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current state of the player.
|
||||||
|
* HAVE_NOTHING (numeric value 0)
|
||||||
|
* HAVE_METADATA (numeric value 1)
|
||||||
|
* HAVE_CURRENT_DATA (numeric value 2)
|
||||||
|
* HAVE_FUTURE_DATA (numeric value 3)
|
||||||
|
* HAVE_ENOUGH_DATA (numeric value 4)
|
||||||
|
*
|
||||||
|
* @returns Ready state.
|
||||||
|
*/
|
||||||
|
readyState(): number {
|
||||||
|
return this.el_.readyState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the player support native fullscreen mode or not. (Mobile devices)
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get media player error.
|
||||||
|
*
|
||||||
|
* @returns Error.
|
||||||
|
*/
|
||||||
|
error(): MediaError | null {
|
||||||
|
return this.el_.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[
|
||||||
|
['featuresVolumeControl', 'canControlVolume'],
|
||||||
|
['featuresMuteControl', 'canMuteVolume'],
|
||||||
|
['featuresPlaybackRate', 'canControlPlaybackRate'],
|
||||||
|
['featuresNativeTextTracks', 'supportsNativeTextTracks'],
|
||||||
|
['featuresFullscreenResize', 'supportsFullscreenResize'],
|
||||||
|
['featuresProgressEvents', 'supportsProgressEvents'],
|
||||||
|
['featuresTimeupdateEvents', 'supportsTimeupdateEvents'],
|
||||||
|
].forEach(([key, fn]) => {
|
||||||
|
defineLazyProperty(VideoJSOgvJS.prototype, key, () => VideoJSOgvJS[fn](), true);
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Initialize the controller.
|
||||||
|
*/
|
||||||
|
export const initializeVideoJSOgvJS = (): void => {
|
||||||
|
OGVLoader.base = 'assets/lib/ogv';
|
||||||
|
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) & {
|
||||||
|
stop: () => void;
|
||||||
|
};
|
|
@ -15,6 +15,7 @@
|
||||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { CoreFilterDelegate } from '@features/filter/services/filter-delegate';
|
import { CoreFilterDelegate } from '@features/filter/services/filter-delegate';
|
||||||
|
import { initializeVideoJSOgvJS } from './classes/videojs-ogvjs';
|
||||||
import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin';
|
import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -26,7 +27,11 @@ import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin';
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
multi: true,
|
multi: true,
|
||||||
useValue: () => CoreFilterDelegate.registerHandler(AddonFilterMediaPluginHandler.instance),
|
useValue: () => {
|
||||||
|
CoreFilterDelegate.registerHandler(AddonFilterMediaPluginHandler.instance);
|
||||||
|
|
||||||
|
initializeVideoJSOgvJS();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,11 +13,17 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreExternalContentDirective } from '@directives/external-content';
|
||||||
|
|
||||||
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
|
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
|
||||||
|
import { CoreLang } from '@services/lang';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
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 { CoreDom } from '@singletons/dom';
|
||||||
|
import videojs from 'video.js';
|
||||||
|
import { VideoJSOptions } from '../../classes/videojs-ogvjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to support the Multimedia filter.
|
* Handler to support the Multimedia filter.
|
||||||
|
@ -47,6 +53,53 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
||||||
return this.template.innerHTML;
|
return this.template.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
handleHtml(container: HTMLElement): void {
|
||||||
|
const mediaElements = Array.from(container.querySelectorAll<HTMLVideoElement | HTMLAudioElement>('video, audio'));
|
||||||
|
|
||||||
|
mediaElements.forEach((mediaElement) => {
|
||||||
|
if (CoreDom.mediaUsesJavascriptPlayer(mediaElement)) {
|
||||||
|
this.useVideoJS(mediaElement);
|
||||||
|
} else {
|
||||||
|
// Remove the VideoJS classes and data if present.
|
||||||
|
mediaElement.classList.remove('video-js');
|
||||||
|
mediaElement.removeAttribute('data-setup');
|
||||||
|
mediaElement.removeAttribute('data-setup-lazy');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use video JS in a certain video or audio.
|
||||||
|
*
|
||||||
|
* @param mediaElement Media element.
|
||||||
|
*/
|
||||||
|
protected async useVideoJS(mediaElement: HTMLVideoElement | HTMLAudioElement): Promise<void> {
|
||||||
|
const lang = await CoreLang.getCurrentLanguage();
|
||||||
|
|
||||||
|
// Wait for external-content to finish in the element and its sources.
|
||||||
|
await Promise.all([
|
||||||
|
CoreDirectivesRegistry.waitDirectivesReady(mediaElement, undefined, CoreExternalContentDirective),
|
||||||
|
CoreDirectivesRegistry.waitDirectivesReady(mediaElement, 'source', CoreExternalContentDirective),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dataSetupString = mediaElement.getAttribute('data-setup') || mediaElement.getAttribute('data-setup-lazy') || '{}';
|
||||||
|
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
|
||||||
|
|
||||||
|
videojs(mediaElement, {
|
||||||
|
controls: true,
|
||||||
|
techOrder: ['OgvJS'],
|
||||||
|
language: lang,
|
||||||
|
fluid: true,
|
||||||
|
controlBar: {
|
||||||
|
fullscreenToggle: false,
|
||||||
|
},
|
||||||
|
aspectRatio: data.aspectRatio,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Treat video filters. Currently only treating youtube video using video JS.
|
* Treat video filters. Currently only treating youtube video using video JS.
|
||||||
*
|
*
|
||||||
|
@ -59,7 +112,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}';
|
const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}';
|
||||||
const data = <VideoDataSetup> CoreTextUtils.parseJSON(dataSetupString, {});
|
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
|
||||||
const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src);
|
const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrlUtils.getYoutubeEmbedUrl(data.sources?.[0]?.src);
|
||||||
|
|
||||||
if (!youtubeUrl) {
|
if (!youtubeUrl) {
|
||||||
|
@ -81,10 +134,3 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddonFilterMediaPluginHandler = makeSingleton(AddonFilterMediaPluginHandlerService);
|
export const AddonFilterMediaPluginHandler = makeSingleton(AddonFilterMediaPluginHandlerService);
|
||||||
|
|
||||||
type VideoDataSetup = {
|
|
||||||
techOrder?: string[];
|
|
||||||
sources?: {
|
|
||||||
src?: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
|
@ -36,6 +36,9 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreConstants } from '../constants';
|
import { CoreConstants } from '../constants';
|
||||||
import { CoreNetwork } from '@services/network';
|
import { CoreNetwork } from '@services/network';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
|
import { AsyncDirective } from '@classes/async-directive';
|
||||||
|
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directive to handle external content.
|
* Directive to handle external content.
|
||||||
|
@ -50,7 +53,7 @@ import { Translate } from '@singletons';
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[core-external-content]',
|
selector: '[core-external-content]',
|
||||||
})
|
})
|
||||||
export class CoreExternalContentDirective implements AfterViewInit, OnChanges, OnDestroy {
|
export class CoreExternalContentDirective implements AfterViewInit, OnChanges, OnDestroy, AsyncDirective {
|
||||||
|
|
||||||
@Input() siteId?: string; // Site ID to use.
|
@Input() siteId?: string; // Site ID to use.
|
||||||
@Input() component?: string; // Component to link the file to.
|
@Input() component?: string; // Component to link the file to.
|
||||||
|
@ -67,11 +70,14 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
protected initialized = false;
|
protected initialized = false;
|
||||||
protected fileEventObserver?: CoreEventObserver;
|
protected fileEventObserver?: CoreEventObserver;
|
||||||
|
protected onReadyPromise = new CorePromisedValue<void>();
|
||||||
|
|
||||||
constructor(element: ElementRef) {
|
constructor(element: ElementRef) {
|
||||||
|
|
||||||
this.element = element.nativeElement;
|
this.element = element.nativeElement;
|
||||||
this.logger = CoreLogger.getInstance('CoreExternalContentDirective');
|
this.logger = CoreLogger.getInstance('CoreExternalContentDirective');
|
||||||
|
|
||||||
|
CoreDirectivesRegistry.register(this.element, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,15 +163,21 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.invalid = true;
|
this.invalid = true;
|
||||||
|
this.onReadyPromise.resolve();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid handling data url's.
|
// Avoid handling data url's.
|
||||||
if (url && url.indexOf('data:') === 0) {
|
if (url && url.indexOf('data:') === 0) {
|
||||||
this.invalid = true;
|
if (tagName === 'SOURCE') {
|
||||||
|
// Restoring original src.
|
||||||
|
this.addSource(url);
|
||||||
|
}
|
||||||
|
|
||||||
this.onLoad.emit();
|
this.onLoad.emit();
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
|
this.onReadyPromise.resolve();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -182,6 +194,8 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this.onReadyPromise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,13 +280,11 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let urls = inlineStyles.match(/https?:\/\/[^"') ;]*/g);
|
const urls = CoreUtils.uniqueArray(Array.from(inlineStyles.match(/https?:\/\/[^"') ;]*/g) ?? []));
|
||||||
if (!urls || !urls.length) {
|
if (!urls.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
urls = CoreUtils.uniqueArray(urls); // Remove duplicates.
|
|
||||||
|
|
||||||
const promises = urls.map(async (url) => {
|
const promises = urls.map(async (url) => {
|
||||||
const finalUrl = await CoreFilepool.getSrcByUrl(siteId, url, this.component, this.componentId, 0, true, true);
|
const finalUrl = await CoreFilepool.getSrcByUrl(siteId, url, this.component, this.componentId, 0, true, true);
|
||||||
|
|
||||||
|
@ -462,4 +474,11 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
|
||||||
this.fileEventObserver?.off();
|
this.fileEventObserver?.off();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ready(): Promise<void> {
|
||||||
|
return this.onReadyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -266,6 +266,24 @@ export class CoreAppProvider {
|
||||||
return CorePlatform.isMobile() && !CorePlatform.is('android');
|
return CorePlatform.isMobile() && !CorePlatform.is('android');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the app is running in an iPad device.
|
||||||
|
*
|
||||||
|
* @returns Whether the app is running in an iPad device.
|
||||||
|
*/
|
||||||
|
isIPad(): boolean {
|
||||||
|
return CoreApp.isIOS() && CorePlatform.is('ipad');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the app is running in an iPhone device.
|
||||||
|
*
|
||||||
|
* @returns Whether the app is running in an iPhone device.
|
||||||
|
*/
|
||||||
|
isIPhone(): boolean {
|
||||||
|
return CoreApp.isIOS() && CorePlatform.is('iphone');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the keyboard is closing.
|
* Check if the keyboard is closing.
|
||||||
*
|
*
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import extToMime from '@/assets/exttomime.json';
|
import extToMime from '@/assets/exttomime.json';
|
||||||
import mimeToExt from '@/assets/mimetoext.json';
|
import mimeToExt from '@/assets/mimetoext.json';
|
||||||
import { CoreFileEntry, CoreFileHelper } from '@services/file-helper';
|
import { CoreFileEntry, CoreFileHelper } from '@services/file-helper';
|
||||||
|
import { CoreUrl } from '@singletons/url';
|
||||||
|
|
||||||
interface MimeTypeInfo {
|
interface MimeTypeInfo {
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -302,18 +303,13 @@ export class CoreMimetypeUtilsProvider {
|
||||||
* @returns The lowercased extension without the dot, or undefined.
|
* @returns The lowercased extension without the dot, or undefined.
|
||||||
*/
|
*/
|
||||||
guessExtensionFromUrl(fileUrl: string): string | undefined {
|
guessExtensionFromUrl(fileUrl: string): string | undefined {
|
||||||
const split = fileUrl.split('.');
|
const split = CoreUrl.removeUrlAnchor(fileUrl).split('.');
|
||||||
let extension: string | undefined;
|
let extension: string | undefined;
|
||||||
|
|
||||||
if (split.length > 1) {
|
if (split.length > 1) {
|
||||||
let candidate = split[split.length - 1].toLowerCase();
|
let candidate = split[split.length - 1].toLowerCase();
|
||||||
// Remove params if any.
|
// Remove params if any.
|
||||||
let position = candidate.indexOf('?');
|
const position = candidate.indexOf('?');
|
||||||
if (position > -1) {
|
|
||||||
candidate = candidate.substring(0, position);
|
|
||||||
}
|
|
||||||
// Remove anchor if any.
|
|
||||||
position = candidate.indexOf('#');
|
|
||||||
if (position > -1) {
|
if (position > -1) {
|
||||||
candidate = candidate.substring(0, position);
|
candidate = candidate.substring(0, position);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +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';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* "Utils" service with helper functions for URLs.
|
* "Utils" service with helper functions for URLs.
|
||||||
|
@ -120,7 +121,8 @@ export class CoreUrlUtilsProvider {
|
||||||
// Also, only use it for "core" pluginfile endpoints. Some plugins can implement their own endpoint (like customcert).
|
// Also, only use it for "core" pluginfile endpoints. Some plugins can implement their own endpoint (like customcert).
|
||||||
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,7 +13,9 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
||||||
|
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';
|
||||||
|
|
||||||
|
@ -567,6 +569,74 @@ export class CoreDom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (!CoreApp.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 CoreDom.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 (!CoreApp.isIOS()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sources = CoreDom.getMediaSources(mediaElement);
|
||||||
|
|
||||||
|
return sources.some(source => CoreDom.sourceUsesJavascriptPlayer(source));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -585,3 +655,11 @@ export type CoreScrollOptions = {
|
||||||
addYAxis?: number;
|
addYAxis?: number;
|
||||||
addXAxis?: number;
|
addXAxis?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source of a media element.
|
||||||
|
*/
|
||||||
|
export type CoreMediaSource = {
|
||||||
|
src: string;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<meta name="msapplication-tap-highlight" content="no" />
|
<meta name="msapplication-tap-highlight" content="no" />
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="assets/icon/favicon.png" />
|
<link rel="icon" type="image/png" href="assets/icon/favicon.png" />
|
||||||
|
<link rel="stylesheet" href="assets/lib/video.js/video-js.min.css">
|
||||||
|
|
||||||
<!-- add to homescreen for ios -->
|
<!-- add to homescreen for ios -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
|
|
@ -1809,3 +1809,63 @@ 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. **/
|
||||||
|
|
Loading…
Reference in New Issue