MOBILE-4166 core: Fix VideoJS in books and destroy players
parent
9419db02a1
commit
9b011ba350
|
@ -22,6 +22,7 @@ import { CoreUrlUtils } from '@services/utils/url';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||||
import { CoreDom } from '@singletons/dom';
|
import { CoreDom } from '@singletons/dom';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import { VideoJSOptions } from '../../classes/videojs-ogvjs';
|
import { VideoJSOptions } from '../../classes/videojs-ogvjs';
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
||||||
const dataSetupString = mediaElement.getAttribute('data-setup') || mediaElement.getAttribute('data-setup-lazy') || '{}';
|
const dataSetupString = mediaElement.getAttribute('data-setup') || mediaElement.getAttribute('data-setup-lazy') || '{}';
|
||||||
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
|
const data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
|
||||||
|
|
||||||
videojs(mediaElement, {
|
const player = videojs(mediaElement, {
|
||||||
controls: true,
|
controls: true,
|
||||||
techOrder: ['OgvJS'],
|
techOrder: ['OgvJS'],
|
||||||
language: lang,
|
language: lang,
|
||||||
|
@ -98,6 +99,13 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
||||||
},
|
},
|
||||||
aspectRatio: data.aspectRatio,
|
aspectRatio: data.aspectRatio,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CoreEvents.trigger(CoreEvents.JS_PLAYER_CREATED, {
|
||||||
|
id: mediaElement.id,
|
||||||
|
element: mediaElement,
|
||||||
|
player,
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
export abstract class ElementController {
|
export abstract class ElementController {
|
||||||
|
|
||||||
protected enabled: boolean;
|
protected enabled: boolean;
|
||||||
|
protected destroyed = false;
|
||||||
|
|
||||||
constructor(enabled: boolean) {
|
constructor(enabled: boolean) {
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
|
@ -49,6 +50,19 @@ export abstract class ElementController {
|
||||||
this.onDisabled();
|
this.onDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the element.
|
||||||
|
*/
|
||||||
|
destroy(): void {
|
||||||
|
if (this.destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.destroyed = true;
|
||||||
|
|
||||||
|
this.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update underlying element to enable interactivity.
|
* Update underlying element to enable interactivity.
|
||||||
*/
|
*/
|
||||||
|
@ -59,4 +73,11 @@ export abstract class ElementController {
|
||||||
*/
|
*/
|
||||||
abstract onDisabled(): void;
|
abstract onDisabled(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy/dispose pertinent data.
|
||||||
|
*/
|
||||||
|
onDestroy(): void {
|
||||||
|
// By default, nothing to destroy.
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
|
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { ElementController } from './ElementController';
|
import { ElementController } from './ElementController';
|
||||||
|
import videojs from 'video.js';
|
||||||
|
import { CoreDom } from '@singletons/dom';
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper class to control the interactivity of a media element.
|
* Wrapper class to control the interactivity of a media element.
|
||||||
|
@ -25,6 +29,10 @@ export class MediaElementController extends ElementController {
|
||||||
private playing?: boolean;
|
private playing?: boolean;
|
||||||
private playListener?: () => void;
|
private playListener?: () => void;
|
||||||
private pauseListener?: () => void;
|
private pauseListener?: () => void;
|
||||||
|
private jsPlayer = new CorePromisedValue<VideoJSPlayer | null>();
|
||||||
|
private jsPlayerListener?: CoreEventObserver;
|
||||||
|
private shouldEnable = false;
|
||||||
|
private shouldDisable = false;
|
||||||
|
|
||||||
constructor(media: HTMLMediaElement, enabled: boolean) {
|
constructor(media: HTMLMediaElement, enabled: boolean) {
|
||||||
super(enabled);
|
super(enabled);
|
||||||
|
@ -34,48 +42,127 @@ export class MediaElementController extends ElementController {
|
||||||
|
|
||||||
media.autoplay = false;
|
media.autoplay = false;
|
||||||
|
|
||||||
|
if (CoreDom.mediaUsesJavascriptPlayer(media)) {
|
||||||
|
const player = this.searchJSPlayer();
|
||||||
|
if (player) {
|
||||||
|
this.jsPlayer.resolve(player);
|
||||||
|
} else {
|
||||||
|
this.jsPlayerListener = CoreEvents.on(CoreEvents.JS_PLAYER_CREATED, data => {
|
||||||
|
if (data.element === media) {
|
||||||
|
this.jsPlayerListener?.off();
|
||||||
|
this.jsPlayer.resolve(data.player as VideoJSPlayer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.jsPlayer.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
enabled && this.onEnabled();
|
enabled && this.onEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
onEnabled(): void {
|
async onEnabled(): Promise<void> {
|
||||||
|
this.shouldEnable = true;
|
||||||
|
this.shouldDisable = false;
|
||||||
|
|
||||||
|
const jsPlayer = await this.jsPlayer;
|
||||||
|
|
||||||
|
if (!this.shouldEnable || this.destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ready = this.playing ?? this.autoplay
|
const ready = this.playing ?? this.autoplay
|
||||||
? this.media.play()
|
? (jsPlayer ?? this.media).play()
|
||||||
: Promise.resolve();
|
: Promise.resolve();
|
||||||
|
|
||||||
ready
|
try {
|
||||||
.then(() => this.addPlaybackEventListeners())
|
await ready;
|
||||||
.catch(error => CoreUtils.logUnhandledError('Error enabling media element', error));
|
|
||||||
|
this.addPlaybackEventListeners(jsPlayer);
|
||||||
|
} catch (error) {
|
||||||
|
CoreUtils.logUnhandledError('Error enabling media element', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async onDisabled(): Promise<void> {
|
async onDisabled(): Promise<void> {
|
||||||
this.removePlaybackEventListeners();
|
this.shouldDisable = true;
|
||||||
|
this.shouldEnable = false;
|
||||||
|
|
||||||
this.media.pause();
|
const jsPlayer = await this.jsPlayer;
|
||||||
|
|
||||||
|
if (!this.shouldDisable || this.destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removePlaybackEventListeners(jsPlayer);
|
||||||
|
|
||||||
|
(jsPlayer ?? this.media).pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async onDestroy(): Promise<void> {
|
||||||
|
const jsPlayer = await this.jsPlayer;
|
||||||
|
|
||||||
|
this.removePlaybackEventListeners(jsPlayer);
|
||||||
|
jsPlayer?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start listening playback events.
|
* Start listening playback events.
|
||||||
|
*
|
||||||
|
* @param jsPlayer Javascript player instance (if any).
|
||||||
*/
|
*/
|
||||||
private addPlaybackEventListeners(): void {
|
private addPlaybackEventListeners(jsPlayer: VideoJSPlayer | null): void {
|
||||||
this.media.addEventListener('play', this.playListener = () => this.playing = true);
|
if (jsPlayer) {
|
||||||
this.media.addEventListener('pause', this.pauseListener = () => this.playing = false);
|
jsPlayer.on('play', this.playListener = () => this.playing = true);
|
||||||
|
jsPlayer.on('pause', this.pauseListener = () => this.playing = false);
|
||||||
|
} else {
|
||||||
|
this.media.addEventListener('play', this.playListener = () => this.playing = true);
|
||||||
|
this.media.addEventListener('pause', this.pauseListener = () => this.playing = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop listening playback events.
|
* Stop listening playback events.
|
||||||
|
*
|
||||||
|
* @param jsPlayer Javascript player instance (if any).
|
||||||
*/
|
*/
|
||||||
private removePlaybackEventListeners(): void {
|
private removePlaybackEventListeners(jsPlayer: VideoJSPlayer | null): void {
|
||||||
this.playListener && this.media.removeEventListener('play', this.playListener);
|
if (jsPlayer) {
|
||||||
this.pauseListener && this.media.removeEventListener('pause', this.pauseListener);
|
this.playListener && jsPlayer.off('play', this.playListener);
|
||||||
|
this.pauseListener && jsPlayer.off('pause', this.pauseListener);
|
||||||
|
} else {
|
||||||
|
this.playListener && this.media.removeEventListener('play', this.playListener);
|
||||||
|
this.pauseListener && this.media.removeEventListener('pause', this.pauseListener);
|
||||||
|
}
|
||||||
|
|
||||||
delete this.playListener;
|
delete this.playListener;
|
||||||
delete this.pauseListener;
|
delete this.pauseListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search JS player instance.
|
||||||
|
*
|
||||||
|
* @returns Player instance if found.
|
||||||
|
*/
|
||||||
|
private searchJSPlayer(): VideoJSPlayer | undefined {
|
||||||
|
return videojs.getPlayer(this.media.id) || videojs.getPlayer(this.media.id.replace('_html5_api', ''));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VideoJSPlayer = {
|
||||||
|
play: () => Promise<void>;
|
||||||
|
pause: () => Promise<void>;
|
||||||
|
on: (name: string, callback: (ev: Event) => void) => void;
|
||||||
|
off: (name: string, callback: (ev: Event) => void) => void;
|
||||||
|
dispose: () => void;
|
||||||
|
};
|
||||||
|
|
|
@ -149,6 +149,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.domElementPromise?.cancel();
|
this.domElementPromise?.cancel();
|
||||||
this.domPromises.forEach((promise) => { promise.cancel();});
|
this.domPromises.forEach((promise) => { promise.cancel();});
|
||||||
|
this.elementControllers.forEach(controller => controller.destroy());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -365,6 +366,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
|
||||||
// Move the children to the current element to be able to calculate the height.
|
// Move the children to the current element to be able to calculate the height.
|
||||||
CoreDomUtils.moveChildren(result.div, this.element);
|
CoreDomUtils.moveChildren(result.div, this.element);
|
||||||
|
|
||||||
|
this.elementControllers.forEach(controller => controller.destroy());
|
||||||
this.elementControllers = result.elementControllers;
|
this.elementControllers = result.elementControllers;
|
||||||
|
|
||||||
await CoreUtils.nextTick();
|
await CoreUtils.nextTick();
|
||||||
|
|
|
@ -64,6 +64,7 @@ export interface CoreEventsData {
|
||||||
[CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData;
|
[CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData;
|
||||||
[CoreEvents.COURSE_MODULE_VIEWED]: CoreEventCourseModuleViewed;
|
[CoreEvents.COURSE_MODULE_VIEWED]: CoreEventCourseModuleViewed;
|
||||||
[CoreEvents.COMPLETE_REQUIRED_PROFILE_DATA_FINISHED]: CoreEventCompleteRequiredProfileDataFinished;
|
[CoreEvents.COMPLETE_REQUIRED_PROFILE_DATA_FINISHED]: CoreEventCompleteRequiredProfileDataFinished;
|
||||||
|
[CoreEvents.JS_PLAYER_CREATED]: CoreEventJSVideoPlayerCreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -123,6 +124,7 @@ export class CoreEvents {
|
||||||
static readonly COMPLETE_REQUIRED_PROFILE_DATA_FINISHED = 'complete_required_profile_data_finished';
|
static readonly COMPLETE_REQUIRED_PROFILE_DATA_FINISHED = 'complete_required_profile_data_finished';
|
||||||
static readonly MAIN_HOME_LOADED = 'main_home_loaded';
|
static readonly MAIN_HOME_LOADED = 'main_home_loaded';
|
||||||
static readonly FULL_SCREEN_CHANGED = 'full_screen_changed';
|
static readonly FULL_SCREEN_CHANGED = 'full_screen_changed';
|
||||||
|
static readonly JS_PLAYER_CREATED = 'js_player_created';
|
||||||
|
|
||||||
protected static logger = CoreLogger.getInstance('CoreEvents');
|
protected static logger = CoreLogger.getInstance('CoreEvents');
|
||||||
protected static observables: { [eventName: string]: Subject<unknown> } = {};
|
protected static observables: { [eventName: string]: Subject<unknown> } = {};
|
||||||
|
@ -490,3 +492,12 @@ export type CoreEventCourseModuleViewed = {
|
||||||
export type CoreEventCompleteRequiredProfileDataFinished = {
|
export type CoreEventCompleteRequiredProfileDataFinished = {
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data passed to JS_PLAYER_CREATED event.
|
||||||
|
*/
|
||||||
|
export type CoreEventJSVideoPlayerCreated = {
|
||||||
|
id: string;
|
||||||
|
element: HTMLAudioElement | HTMLVideoElement;
|
||||||
|
player: unknown;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue