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 { CoreDirectivesRegistry } from '@singletons/directives-registry';
|
||||
import { CoreDom } from '@singletons/dom';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import videojs from 'video.js';
|
||||
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 data = CoreTextUtils.parseJSON<VideoJSOptions>(dataSetupString, {});
|
||||
|
||||
videojs(mediaElement, {
|
||||
const player = videojs(mediaElement, {
|
||||
controls: true,
|
||||
techOrder: ['OgvJS'],
|
||||
language: lang,
|
||||
|
@ -98,6 +99,13 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
|||
},
|
||||
aspectRatio: data.aspectRatio,
|
||||
});
|
||||
|
||||
CoreEvents.trigger(CoreEvents.JS_PLAYER_CREATED, {
|
||||
id: mediaElement.id,
|
||||
element: mediaElement,
|
||||
player,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
export abstract class ElementController {
|
||||
|
||||
protected enabled: boolean;
|
||||
protected destroyed = false;
|
||||
|
||||
constructor(enabled: boolean) {
|
||||
this.enabled = enabled;
|
||||
|
@ -49,6 +50,19 @@ export abstract class ElementController {
|
|||
this.onDisabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the element.
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.destroyed = true;
|
||||
|
||||
this.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update underlying element to enable interactivity.
|
||||
*/
|
||||
|
@ -59,4 +73,11 @@ export abstract class ElementController {
|
|||
*/
|
||||
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 { 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.
|
||||
|
@ -25,6 +29,10 @@ export class MediaElementController extends ElementController {
|
|||
private playing?: boolean;
|
||||
private playListener?: () => void;
|
||||
private pauseListener?: () => void;
|
||||
private jsPlayer = new CorePromisedValue<VideoJSPlayer | null>();
|
||||
private jsPlayerListener?: CoreEventObserver;
|
||||
private shouldEnable = false;
|
||||
private shouldDisable = false;
|
||||
|
||||
constructor(media: HTMLMediaElement, enabled: boolean) {
|
||||
super(enabled);
|
||||
|
@ -34,48 +42,127 @@ export class MediaElementController extends ElementController {
|
|||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
? this.media.play()
|
||||
? (jsPlayer ?? this.media).play()
|
||||
: Promise.resolve();
|
||||
|
||||
ready
|
||||
.then(() => this.addPlaybackEventListeners())
|
||||
.catch(error => CoreUtils.logUnhandledError('Error enabling media element', error));
|
||||
try {
|
||||
await ready;
|
||||
|
||||
this.addPlaybackEventListeners(jsPlayer);
|
||||
} catch (error) {
|
||||
CoreUtils.logUnhandledError('Error enabling media element', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param jsPlayer Javascript player instance (if any).
|
||||
*/
|
||||
private addPlaybackEventListeners(): void {
|
||||
this.media.addEventListener('play', this.playListener = () => this.playing = true);
|
||||
this.media.addEventListener('pause', this.pauseListener = () => this.playing = false);
|
||||
private addPlaybackEventListeners(jsPlayer: VideoJSPlayer | null): void {
|
||||
if (jsPlayer) {
|
||||
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.
|
||||
*
|
||||
* @param jsPlayer Javascript player instance (if any).
|
||||
*/
|
||||
private removePlaybackEventListeners(): void {
|
||||
this.playListener && this.media.removeEventListener('play', this.playListener);
|
||||
this.pauseListener && this.media.removeEventListener('pause', this.pauseListener);
|
||||
private removePlaybackEventListeners(jsPlayer: VideoJSPlayer | null): void {
|
||||
if (jsPlayer) {
|
||||
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.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 {
|
||||
this.domElementPromise?.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.
|
||||
CoreDomUtils.moveChildren(result.div, this.element);
|
||||
|
||||
this.elementControllers.forEach(controller => controller.destroy());
|
||||
this.elementControllers = result.elementControllers;
|
||||
|
||||
await CoreUtils.nextTick();
|
||||
|
|
|
@ -64,6 +64,7 @@ export interface CoreEventsData {
|
|||
[CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData;
|
||||
[CoreEvents.COURSE_MODULE_VIEWED]: CoreEventCourseModuleViewed;
|
||||
[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 MAIN_HOME_LOADED = 'main_home_loaded';
|
||||
static readonly FULL_SCREEN_CHANGED = 'full_screen_changed';
|
||||
static readonly JS_PLAYER_CREATED = 'js_player_created';
|
||||
|
||||
protected static logger = CoreLogger.getInstance('CoreEvents');
|
||||
protected static observables: { [eventName: string]: Subject<unknown> } = {};
|
||||
|
@ -490,3 +492,12 @@ export type CoreEventCourseModuleViewed = {
|
|||
export type CoreEventCompleteRequiredProfileDataFinished = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data passed to JS_PLAYER_CREATED event.
|
||||
*/
|
||||
export type CoreEventJSVideoPlayerCreated = {
|
||||
id: string;
|
||||
element: HTMLAudioElement | HTMLVideoElement;
|
||||
player: unknown;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue