forked from CIT/Vmeda.Online
		
	MOBILE-4166 core: Use VideoJS in iOS for unsupported media
This commit is contained in:
		
							parent
							
								
									47e5158afe
								
							
						
					
					
						commit
						9419db02a1
					
				@ -28,6 +28,8 @@ const ASSETS = {
 | 
			
		||||
    '/node_modules/mathjax/jax/output/PreviewHTML': '/lib/mathjax/jax/output/PreviewHTML',
 | 
			
		||||
    '/node_modules/mathjax/localization': '/lib/mathjax/localization',
 | 
			
		||||
    '/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) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										762
									
								
								src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										762
									
								
								src/addons/filter/mediaplugin/classes/videojs-ogvjs.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 { CoreFilterDelegate } from '@features/filter/services/filter-delegate';
 | 
			
		||||
import { initializeVideoJSOgvJS } from './classes/videojs-ogvjs';
 | 
			
		||||
import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
@ -26,7 +27,11 @@ import { AddonFilterMediaPluginHandler } from './services/handlers/mediaplugin';
 | 
			
		||||
        {
 | 
			
		||||
            provide: APP_INITIALIZER,
 | 
			
		||||
            multi: true,
 | 
			
		||||
            useValue: () => CoreFilterDelegate.registerHandler(AddonFilterMediaPluginHandler.instance),
 | 
			
		||||
            useValue: () => {
 | 
			
		||||
                CoreFilterDelegate.registerHandler(AddonFilterMediaPluginHandler.instance);
 | 
			
		||||
 | 
			
		||||
                initializeVideoJSOgvJS();
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -13,11 +13,17 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { CoreExternalContentDirective } from '@directives/external-content';
 | 
			
		||||
 | 
			
		||||
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
 | 
			
		||||
import { CoreLang } from '@services/lang';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUrlUtils } from '@services/utils/url';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
 | 
			
		||||
import { CoreDom } from '@singletons/dom';
 | 
			
		||||
import videojs from 'video.js';
 | 
			
		||||
import { VideoJSOptions } from '../../classes/videojs-ogvjs';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to support the Multimedia filter.
 | 
			
		||||
@ -47,6 +53,53 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
 | 
			
		||||
        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.
 | 
			
		||||
     *
 | 
			
		||||
@ -59,7 +112,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
        if (!youtubeUrl) {
 | 
			
		||||
@ -81,10 +134,3 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 { CoreNetwork } from '@services/network';
 | 
			
		||||
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.
 | 
			
		||||
@ -50,7 +53,7 @@ import { Translate } from '@singletons';
 | 
			
		||||
@Directive({
 | 
			
		||||
    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() component?: string; // Component to link the file to.
 | 
			
		||||
@ -67,11 +70,14 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
    protected initialized = false;
 | 
			
		||||
    protected fileEventObserver?: CoreEventObserver;
 | 
			
		||||
    protected onReadyPromise = new CorePromisedValue<void>();
 | 
			
		||||
 | 
			
		||||
    constructor(element: ElementRef) {
 | 
			
		||||
 | 
			
		||||
        this.element = element.nativeElement;
 | 
			
		||||
        this.logger = CoreLogger.getInstance('CoreExternalContentDirective');
 | 
			
		||||
 | 
			
		||||
        CoreDirectivesRegistry.register(this.element, this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -157,15 +163,21 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
            this.invalid = true;
 | 
			
		||||
            this.onReadyPromise.resolve();
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Avoid handling data url's.
 | 
			
		||||
        if (url && url.indexOf('data:') === 0) {
 | 
			
		||||
            this.invalid = true;
 | 
			
		||||
            if (tagName === 'SOURCE') {
 | 
			
		||||
                // Restoring original src.
 | 
			
		||||
                this.addSource(url);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.onLoad.emit();
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
            this.onReadyPromise.resolve();
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@ -182,6 +194,8 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
 | 
			
		||||
                    this.loaded = true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.onReadyPromise.resolve();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -266,13 +280,11 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let urls = inlineStyles.match(/https?:\/\/[^"') ;]*/g);
 | 
			
		||||
        if (!urls || !urls.length) {
 | 
			
		||||
        const urls = CoreUtils.uniqueArray(Array.from(inlineStyles.match(/https?:\/\/[^"') ;]*/g) ?? []));
 | 
			
		||||
        if (!urls.length) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        urls = CoreUtils.uniqueArray(urls); // Remove duplicates.
 | 
			
		||||
 | 
			
		||||
        const promises = urls.map(async (url) => {
 | 
			
		||||
            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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ready(): Promise<void> {
 | 
			
		||||
        return this.onReadyPromise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -266,6 +266,24 @@ export class CoreAppProvider {
 | 
			
		||||
        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.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import extToMime from '@/assets/exttomime.json';
 | 
			
		||||
import mimeToExt from '@/assets/mimetoext.json';
 | 
			
		||||
import { CoreFileEntry, CoreFileHelper } from '@services/file-helper';
 | 
			
		||||
import { CoreUrl } from '@singletons/url';
 | 
			
		||||
 | 
			
		||||
interface MimeTypeInfo {
 | 
			
		||||
    type: string;
 | 
			
		||||
@ -302,18 +303,13 @@ export class CoreMimetypeUtilsProvider {
 | 
			
		||||
     * @returns The lowercased extension without the dot, or undefined.
 | 
			
		||||
     */
 | 
			
		||||
    guessExtensionFromUrl(fileUrl: string): string | undefined {
 | 
			
		||||
        const split = fileUrl.split('.');
 | 
			
		||||
        const split = CoreUrl.removeUrlAnchor(fileUrl).split('.');
 | 
			
		||||
        let extension: string | undefined;
 | 
			
		||||
 | 
			
		||||
        if (split.length > 1) {
 | 
			
		||||
            let candidate = split[split.length - 1].toLowerCase();
 | 
			
		||||
            // Remove params if any.
 | 
			
		||||
            let position = candidate.indexOf('?');
 | 
			
		||||
            if (position > -1) {
 | 
			
		||||
                candidate = candidate.substring(0, position);
 | 
			
		||||
            }
 | 
			
		||||
            // Remove anchor if any.
 | 
			
		||||
            position = candidate.indexOf('#');
 | 
			
		||||
            const position = candidate.indexOf('?');
 | 
			
		||||
            if (position > -1) {
 | 
			
		||||
                candidate = candidate.substring(0, position);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ import { CoreUrl } from '@singletons/url';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CorePath } from '@singletons/path';
 | 
			
		||||
import { CorePlatform } from '@services/platform';
 | 
			
		||||
import { CoreDom } from '@singletons/dom';
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * "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).
 | 
			
		||||
        return !CoreConstants.CONFIG.disableTokenFile && !!accessKey && !url.match(/[&?]file=/) && (
 | 
			
		||||
            url.indexOf(CorePath.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 ||
 | 
			
		||||
            url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0);
 | 
			
		||||
            url.indexOf(CorePath.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0) &&
 | 
			
		||||
            !CoreDom.sourceUsesJavascriptPlayer({ src: url });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,9 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { CoreCancellablePromise } from '@classes/cancellable-promise';
 | 
			
		||||
import { CoreApp } from '@services/app';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreMimetypeUtils } from '@services/utils/mimetype';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CoreEventObserver } from '@singletons/events';
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    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" />
 | 
			
		||||
 | 
			
		||||
    <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 -->
 | 
			
		||||
    <meta name="apple-mobile-web-app-capable" content="yes" />
 | 
			
		||||
 | 
			
		||||
@ -1809,3 +1809,63 @@ ion-modal.core-modal-no-background {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * VideoJS modifications.
 | 
			
		||||
 * Styles extracted from the end of https://github.com/moodle/moodle/blob/master/media/player/videojs/styles.css
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
/* Audio: Remove big play button (leave only the button in controls). */
 | 
			
		||||
.video-js.vjs-audio .vjs-big-play-button {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
/* Audio: Make the controlbar visible by default */
 | 
			
		||||
.video-js.vjs-audio .vjs-control-bar {
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    display: -webkit-flex;
 | 
			
		||||
    display: -ms-flexbox;
 | 
			
		||||
    display: flex;
 | 
			
		||||
}
 | 
			
		||||
/* Make player height minimum to the controls height so when we hide video/poster area the controls are displayed correctly. */
 | 
			
		||||
.video-js.vjs-audio {
 | 
			
		||||
    min-height: 3em;
 | 
			
		||||
}
 | 
			
		||||
/* In case of error reset height to the default (otherwise no aspect ratio is available and height becomes 0). */
 | 
			
		||||
.video-js.vjs-error {
 | 
			
		||||
    height: 150px !important;
 | 
			
		||||
}
 | 
			
		||||
/* Minimum height for videos should not be less than the size of play button. */
 | 
			
		||||
.mediaplugin_videojs video {
 | 
			
		||||
    min-height: 32px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* MDL-61020: VideoJS timeline progress bar should not be flipped in RTL mode. */
 | 
			
		||||
 | 
			
		||||
/* Prevent the progress bar from being flipped in RTL. */
 | 
			
		||||
/*rtl:ignore*/
 | 
			
		||||
.video-js .vjs-progress-holder .vjs-play-progress,
 | 
			
		||||
.video-js .vjs-progress-holder .vjs-load-progress,
 | 
			
		||||
.video-js .vjs-progress-holder .vjs-load-progress div {
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: auto;
 | 
			
		||||
}
 | 
			
		||||
/* Keep the video scrubber button at the end of the progress bar in RTL. */
 | 
			
		||||
/*rtl:ignore*/
 | 
			
		||||
.video-js .vjs-play-progress:before {
 | 
			
		||||
    left: auto;
 | 
			
		||||
    right: -0.5em;
 | 
			
		||||
}
 | 
			
		||||
/* Prevent the volume slider from being flipped in RTL. */
 | 
			
		||||
/*rtl:ignore*/
 | 
			
		||||
.video-js .vjs-volume-level {
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: auto;
 | 
			
		||||
}
 | 
			
		||||
/* Keep the volume slider handle at the end of the volume slider in RTL. */
 | 
			
		||||
/*rtl:ignore*/
 | 
			
		||||
.vjs-slider-horizontal .vjs-volume-level:before {
 | 
			
		||||
    left: auto;
 | 
			
		||||
    right: -0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Finish VideoJS modifications. **/
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user