
Deactivate tab when new version in installed Fix messages grouping Fix downloading photos from media viewer DIspatch notification to better tab
991 lines
29 KiB
TypeScript
991 lines
29 KiB
TypeScript
/*
|
||
* https://github.com/morethanwords/tweb
|
||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||
*/
|
||
|
||
import rootScope from "../lib/rootScope";
|
||
import { MyDocument } from "../lib/appManagers/appDocsManager";
|
||
import deferredPromise, { CancellablePromise } from "../helpers/cancellablePromise";
|
||
import { IS_APPLE, IS_SAFARI } from "../environment/userAgent";
|
||
import { MOUNT_CLASS_TO } from "../config/debug";
|
||
import simulateEvent from "../helpers/dom/dispatchEvent";
|
||
import type { SearchSuperContext } from "./appSearchSuper.";
|
||
import { Document, DocumentAttribute, Message, PhotoSize } from "../layer";
|
||
import IS_TOUCH_SUPPORTED from "../environment/touchSupport";
|
||
import I18n from "../lib/langPack";
|
||
import SearchListLoader from "../helpers/searchListLoader";
|
||
import copy from "../helpers/object/copy";
|
||
import deepEqual from "../helpers/object/deepEqual";
|
||
import ListenerSetter from "../helpers/listenerSetter";
|
||
import { AppManagers } from "../lib/appManagers/managers";
|
||
import getMediaFromMessage from "../lib/appManagers/utils/messages/getMediaFromMessage";
|
||
import getPeerTitle from "./wrappers/getPeerTitle";
|
||
import appDownloadManager from "../lib/appManagers/appDownloadManager";
|
||
import onMediaLoad from "../helpers/onMediaLoad";
|
||
import EventListenerBase from "../helpers/eventListenerBase";
|
||
|
||
// TODO: Safari: проверить стрим, включить его и сразу попробовать включить видео или другую песню
|
||
// TODO: Safari: попробовать замаскировать подгрузку последнего чанка
|
||
// TODO: Safari: пофиксить момент, когда заканчивается песня и пытаешься включить её заново - прогресс сразу в конце
|
||
|
||
export type MediaItem = {mid: number, peerId: PeerId};
|
||
|
||
type HTMLMediaElement = HTMLAudioElement | HTMLVideoElement;
|
||
|
||
const SHOULD_USE_SAFARI_FIX = (() => {
|
||
try {
|
||
return IS_SAFARI && +navigator.userAgent.match(/ Version\/(\d+)/)[1] < 14;
|
||
} catch(err) {
|
||
return false;
|
||
}
|
||
})();
|
||
|
||
const SEEK_OFFSET = 10;
|
||
|
||
export type MediaSearchContext = SearchSuperContext & Partial<{
|
||
isScheduled: boolean,
|
||
useSearch: boolean
|
||
}>;
|
||
|
||
type MediaDetails = {
|
||
peerId: PeerId,
|
||
mid: number,
|
||
docId: DocId,
|
||
doc: MyDocument,
|
||
message: Message.message,
|
||
clean?: boolean,
|
||
isScheduled?: boolean,
|
||
isSingle?: boolean
|
||
};
|
||
|
||
export type PlaybackMediaType = 'voice' | 'video' | 'audio';
|
||
|
||
export class AppMediaPlaybackController extends EventListenerBase<{
|
||
play: (details: ReturnType<AppMediaPlaybackController['getPlayingDetails']>) => void,
|
||
pause: () => void,
|
||
playbackParams: (params: ReturnType<AppMediaPlaybackController['getPlaybackParams']>) => void,
|
||
stop: () => void,
|
||
}> {
|
||
private container: HTMLElement;
|
||
private media: Map<PeerId, Map<number, HTMLMediaElement>> = new Map();
|
||
private scheduled: AppMediaPlaybackController['media'] = new Map();
|
||
private mediaDetails: Map<HTMLMediaElement, MediaDetails> = new Map();
|
||
private playingMedia: HTMLMediaElement;
|
||
private playingMediaType: PlaybackMediaType;
|
||
|
||
private waitingMediaForLoad: Map<PeerId, Map<number, CancellablePromise<void>>> = new Map();
|
||
private waitingScheduledMediaForLoad: AppMediaPlaybackController['waitingMediaForLoad'] = new Map();
|
||
private waitingDocumentsForLoad: {[docId: string]: Set<HTMLMediaElement>} = {};
|
||
|
||
public willBePlayedMedia: HTMLMediaElement;
|
||
private searchContext: MediaSearchContext;
|
||
|
||
private listLoader: SearchListLoader<MediaItem>;
|
||
|
||
public volume: number;
|
||
public muted: boolean;
|
||
public playbackRate: number;
|
||
public loop: boolean;
|
||
public round: boolean;
|
||
private _volume: number;
|
||
private _muted: boolean;
|
||
private _playbackRate: number;
|
||
private _loop: boolean;
|
||
private _round: boolean;
|
||
private lockedSwitchers: boolean;
|
||
private playbackRates: Record<PlaybackMediaType, number> = {
|
||
voice: 1,
|
||
video: 1,
|
||
audio: 1
|
||
};
|
||
|
||
private pip: HTMLVideoElement;
|
||
private managers: AppManagers;
|
||
private skipMediaPlayEvent: boolean;
|
||
|
||
construct(managers: AppManagers) {
|
||
this.managers = managers;
|
||
this.container = document.createElement('div');
|
||
//this.container.style.cssText = 'position: absolute; top: -10000px; left: -10000px;';
|
||
this.container.style.cssText = 'display: none;';
|
||
document.body.append(this.container);
|
||
|
||
if(navigator.mediaSession) {
|
||
const actions: {[action in MediaSessionAction]?: MediaSessionActionHandler} = {
|
||
play: this.browserPlay,
|
||
pause: this.browserPause,
|
||
stop: this.browserStop,
|
||
seekbackward: this.browserSeekBackward,
|
||
seekforward: this.browserSeekForward,
|
||
seekto: this.browserSeekTo,
|
||
previoustrack: this.browserPrevious,
|
||
nexttrack: this.browserNext
|
||
};
|
||
|
||
for(const action in actions) {
|
||
try {
|
||
navigator.mediaSession.setActionHandler(action as MediaSessionAction, actions[action as MediaSessionAction]);
|
||
} catch(err) {
|
||
console.warn('MediaSession action is not supported:', action);
|
||
}
|
||
}
|
||
}
|
||
|
||
rootScope.addEventListener('document_downloaded', (docId) => {
|
||
const set = this.waitingDocumentsForLoad[docId];
|
||
if(set) {
|
||
for(const media of set) {
|
||
this.onMediaDocumentLoad(media);
|
||
}
|
||
}
|
||
});
|
||
|
||
rootScope.addEventListener('media_play', () => {
|
||
if(this.skipMediaPlayEvent) {
|
||
this.skipMediaPlayEvent = false;
|
||
return;
|
||
}
|
||
|
||
if(!this.pause() && this.pip) {
|
||
this.pip.pause();
|
||
}
|
||
});
|
||
|
||
const properties: {[key: PropertyKey]: PropertyDescriptor} = {};
|
||
const keys = [
|
||
'volume' as const,
|
||
'muted' as const,
|
||
'playbackRate' as const,
|
||
'loop' as const,
|
||
'round' as const
|
||
];
|
||
keys.forEach((key) => {
|
||
const _key = ('_' + key) as `_${typeof key}`;
|
||
properties[key] = {
|
||
get: () => this[_key],
|
||
set: (value: number | boolean) => {
|
||
if(this[_key] === value) {
|
||
return;
|
||
}
|
||
|
||
// @ts-ignore
|
||
this[_key] = value;
|
||
if(this.playingMedia && (key !== 'loop' || this.playingMediaType === 'audio') && key !== 'round') {
|
||
// @ts-ignore
|
||
this.playingMedia[key] = value;
|
||
}
|
||
|
||
if(key === 'playbackRate' && this.playingMediaType !== undefined) {
|
||
this.playbackRates[this.playingMediaType] = value as number;
|
||
}
|
||
|
||
this.dispatchPlaybackParams();
|
||
}
|
||
};
|
||
});
|
||
Object.defineProperties(this, properties);
|
||
}
|
||
|
||
private dispatchPlaybackParams() {
|
||
this.dispatchEvent('playbackParams', this.getPlaybackParams());
|
||
}
|
||
|
||
public getPlaybackParams() {
|
||
const {volume, muted, playbackRate, playbackRates, loop, round} = this;
|
||
return {
|
||
volume,
|
||
muted,
|
||
playbackRate,
|
||
playbackRates,
|
||
loop,
|
||
round
|
||
};
|
||
}
|
||
|
||
public setPlaybackParams(params: ReturnType<AppMediaPlaybackController['getPlaybackParams']>) {
|
||
this.playbackRates = params.playbackRates;
|
||
this._volume = params.volume;
|
||
this._muted = params.muted;
|
||
this._playbackRate = params.playbackRate;
|
||
this._loop = params.loop;
|
||
this._round = params.round;
|
||
}
|
||
|
||
public seekBackward = (details: MediaSessionActionDetails, media = this.playingMedia) => {
|
||
if(media) {
|
||
media.currentTime = Math.max(0, media.currentTime - (details.seekOffset || SEEK_OFFSET));
|
||
}
|
||
};
|
||
|
||
public seekForward = (details: MediaSessionActionDetails, media = this.playingMedia) => {
|
||
if(media) {
|
||
media.currentTime = Math.min(media.duration, media.currentTime + (details.seekOffset || SEEK_OFFSET));
|
||
}
|
||
};
|
||
|
||
public seekTo = (details: MediaSessionActionDetails, media = this.playingMedia) => {
|
||
if(media) {
|
||
media.currentTime = details.seekTime;
|
||
}
|
||
};
|
||
|
||
public addMedia(message: Message.message, autoload: boolean, clean?: boolean): HTMLMediaElement {
|
||
const {peerId, mid} = message;
|
||
|
||
const isScheduled = !!message.pFlags.is_scheduled;
|
||
const s = isScheduled ? this.scheduled : this.media;
|
||
let storage = s.get(message.peerId);
|
||
if(!storage) {
|
||
s.set(message.peerId, storage = new Map());
|
||
}
|
||
|
||
let media = storage.get(mid);
|
||
if(media) {
|
||
return media;
|
||
}
|
||
|
||
const doc = getMediaFromMessage(message) as Document.document;
|
||
storage.set(mid, media = document.createElement(doc.type === 'round' || doc.type === 'video' ? 'video' : 'audio'));
|
||
//const source = document.createElement('source');
|
||
//source.type = doc.type === 'voice' && !opusDecodeController.isPlaySupported() ? 'audio/wav' : doc.mime_type;
|
||
|
||
if(doc.type === 'round') {
|
||
media.setAttribute('playsinline', 'true');
|
||
//media.muted = true;
|
||
}
|
||
|
||
const details: MediaDetails = {
|
||
peerId,
|
||
mid,
|
||
docId: doc.id,
|
||
doc,
|
||
message,
|
||
clean,
|
||
isScheduled: message.pFlags.is_scheduled
|
||
};
|
||
|
||
this.mediaDetails.set(media, details);
|
||
|
||
//media.autoplay = true;
|
||
media.volume = 1;
|
||
//media.append(source);
|
||
|
||
this.container.append(media);
|
||
|
||
media.addEventListener('play', this.onPlay);
|
||
media.addEventListener('pause', this.onPause);
|
||
media.addEventListener('ended', this.onEnded);
|
||
|
||
if(doc.type !== 'audio' && message?.pFlags.media_unread && message.fromId !== rootScope.myId) {
|
||
media.addEventListener('timeupdate', () => {
|
||
this.managers.appMessagesManager.readMessages(peerId, [mid]);
|
||
}, {once: true});
|
||
}
|
||
|
||
/* const onError = (e: Event) => {
|
||
//console.log('appMediaPlaybackController: video onError', e);
|
||
|
||
if(this.nextMid === mid) {
|
||
this.loadSiblingsMedia(peerId, doc.type as MediaType, mid).then(() => {
|
||
if(this.nextMid && storage[this.nextMid]) {
|
||
storage[this.nextMid].play();
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
media.addEventListener('error', onError); */
|
||
|
||
const deferred = deferredPromise<void>();
|
||
if(autoload) {
|
||
deferred.resolve();
|
||
} else {
|
||
const w = message.pFlags.is_scheduled ? this.waitingScheduledMediaForLoad : this.waitingMediaForLoad;
|
||
let waitingStorage = w.get(peerId);
|
||
if(!waitingStorage) {
|
||
w.set(peerId, waitingStorage = new Map());
|
||
}
|
||
|
||
waitingStorage.set(mid, deferred);
|
||
}
|
||
|
||
deferred.then(async() => {
|
||
//media.autoplay = true;
|
||
//console.log('will set media url:', media, doc, doc.type, doc.url);
|
||
|
||
if(doc.supportsStreaming || (await this.managers.thumbsStorage.getCacheContext(doc)).url) {
|
||
this.onMediaDocumentLoad(media);
|
||
} else {
|
||
let set = this.waitingDocumentsForLoad[doc.id];
|
||
if(!set) {
|
||
set = this.waitingDocumentsForLoad[doc.id] = new Set();
|
||
}
|
||
|
||
set.add(media);
|
||
appDownloadManager.downloadMediaURL({media: doc});
|
||
}
|
||
}/* , onError */);
|
||
|
||
return media;
|
||
}
|
||
|
||
public getMedia(peerId: PeerId, mid: number, isScheduled?: boolean) {
|
||
const s = (isScheduled ? this.scheduled : this.media).get(peerId);
|
||
return s?.get(mid);
|
||
}
|
||
|
||
private onMediaDocumentLoad = async(media: HTMLMediaElement) => {
|
||
const details = this.mediaDetails.get(media);
|
||
const doc = await this.managers.appDocsManager.getDoc(details.docId);
|
||
if(doc.type === 'audio' && doc.supportsStreaming && SHOULD_USE_SAFARI_FIX) {
|
||
this.handleSafariStreamable(media);
|
||
}
|
||
|
||
// setTimeout(() => {
|
||
const cacheContext = await this.managers.thumbsStorage.getCacheContext(doc);
|
||
media.src = cacheContext.url;
|
||
|
||
if(this.playingMedia === media) {
|
||
media.playbackRate = this.playbackRate;
|
||
|
||
if(doc.type === 'audio') {
|
||
media.loop = this.loop;
|
||
}
|
||
}
|
||
// }, doc.supportsStreaming ? 500e3 : 0);
|
||
|
||
const set = this.waitingDocumentsForLoad[doc.id];
|
||
if(set) {
|
||
set.delete(media);
|
||
|
||
if(!set.size) {
|
||
delete this.waitingDocumentsForLoad[doc.id];
|
||
}
|
||
}
|
||
};
|
||
|
||
// safari подгрузит последний чанк и песня включится,
|
||
// при этом этот чанк нельзя руками отдать из SW, потому что браузер тогда теряется
|
||
private handleSafariStreamable(media: HTMLMediaElement) {
|
||
media.addEventListener('play', () => {
|
||
/* if(media.readyState === 4) { // https://developer.mozilla.org/ru/docs/Web/API/XMLHttpRequest/readyState
|
||
return;
|
||
} */
|
||
|
||
//media.volume = 0;
|
||
const currentTime = media.currentTime;
|
||
//this.setSafariBuffering(media, true);
|
||
|
||
media.addEventListener('progress', () => {
|
||
media.currentTime = media.duration - 1;
|
||
|
||
media.addEventListener('progress', () => {
|
||
media.currentTime = currentTime;
|
||
//media.volume = 1;
|
||
//this.setSafariBuffering(media, false);
|
||
|
||
if(!media.paused) {
|
||
media.play()/* .catch(() => {}) */;
|
||
}
|
||
}, {once: true});
|
||
}, {once: true});
|
||
}/* , {once: true} */);
|
||
}
|
||
|
||
public resolveWaitingForLoadMedia(peerId: PeerId, mid: number, isScheduled?: boolean) {
|
||
const w = isScheduled ? this.waitingScheduledMediaForLoad : this.waitingMediaForLoad;
|
||
const storage = w.get(peerId);
|
||
if(!storage) {
|
||
return;
|
||
}
|
||
|
||
const promise = storage.get(mid);
|
||
if(promise) {
|
||
promise.resolve();
|
||
storage.delete(mid);
|
||
|
||
if(!storage.size) {
|
||
w.delete(peerId);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Only for audio
|
||
*/
|
||
public isSafariBuffering(media: HTMLMediaElement) {
|
||
/// @ts-ignore
|
||
return !!media.safariBuffering;
|
||
}
|
||
|
||
private setSafariBuffering(media: HTMLMediaElement, value: boolean) {
|
||
// @ts-ignore
|
||
media.safariBuffering = value;
|
||
}
|
||
|
||
private async setNewMediadata(message: Message.message, playingMedia = this.playingMedia) {
|
||
if(document.pictureInPictureElement) {
|
||
return;
|
||
}
|
||
|
||
await onMediaLoad(playingMedia, undefined, false); // have to wait for load, otherwise on macOS won't set
|
||
|
||
const doc = getMediaFromMessage(message) as MyDocument;
|
||
|
||
const artwork: MediaImage[] = [];
|
||
|
||
const isVoice = doc.type === 'voice' || doc.type === 'round';
|
||
let title = '', artist = '';
|
||
|
||
if(doc.thumbs?.length) {
|
||
const size = doc.thumbs[doc.thumbs.length - 1];
|
||
if(!(size as PhotoSize.photoStrippedSize).bytes) {
|
||
const cacheContext = await this.managers.thumbsStorage.getCacheContext(doc, size.type);
|
||
|
||
if(cacheContext.url) {
|
||
artwork.push({
|
||
src: cacheContext.url,
|
||
sizes: `${(size as PhotoSize.photoSize).w}x${(size as PhotoSize.photoSize).h}`,
|
||
type: 'image/jpeg'
|
||
});
|
||
} else {
|
||
const download = appDownloadManager.downloadMediaURL({media: doc, thumb: size});
|
||
download.then(() => {
|
||
if(this.playingMedia !== playingMedia || !cacheContext.url) {
|
||
return;
|
||
}
|
||
|
||
this.setNewMediadata(message);
|
||
});
|
||
}
|
||
}
|
||
} else if(isVoice) {
|
||
const peerId = message.fromId || message.peerId;
|
||
const peerPhoto = await this.managers.appPeersManager.getPeerPhoto(peerId);
|
||
if(peerPhoto) {
|
||
// const result = this.managers.appAvatarsManager.loadAvatar(peerId, peerPhoto, 'photo_small');
|
||
// if(result.cached) {
|
||
// const url = await result.loadPromise;
|
||
// artwork.push({
|
||
// src: url,
|
||
// sizes: '160x160',
|
||
// type: 'image/jpeg'
|
||
// });
|
||
// } else {
|
||
// result.loadPromise.then((url) => {
|
||
// if(this.playingMedia !== playingMedia || !url) {
|
||
// return;
|
||
// }
|
||
|
||
// this.setNewMediadata(message);
|
||
// });
|
||
// }
|
||
}
|
||
|
||
title = await getPeerTitle(peerId, true, false);
|
||
artist = I18n.format(doc.type === 'voice' ? 'AttachAudio' : 'AttachRound', true);
|
||
}
|
||
|
||
if(!isVoice) {
|
||
const attribute = doc.attributes.find((attribute) => attribute._ === 'documentAttributeAudio') as DocumentAttribute.documentAttributeAudio;
|
||
title = attribute?.title ?? doc.file_name;
|
||
artist = attribute?.performer;
|
||
}
|
||
|
||
if(!artwork.length) {
|
||
if(IS_APPLE) {
|
||
if(IS_TOUCH_SUPPORTED) {
|
||
artwork.push({
|
||
src: `assets/img/apple-touch-icon-precomposed.png`,
|
||
sizes: '180x180',
|
||
type: 'image/png'
|
||
});
|
||
} else {
|
||
artwork.push({
|
||
src: `assets/img/apple-touch-icon.png`,
|
||
sizes: '180x180',
|
||
type: 'image/png'
|
||
});
|
||
}
|
||
} else {
|
||
[72, 96, 144, 192, 256, 384, 512].forEach((size) => {
|
||
const sizes = `${size}x${size}`;
|
||
artwork.push({
|
||
src: `assets/img/android-chrome-${sizes}.png`,
|
||
sizes,
|
||
type: 'image/png'
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
const metadata = new MediaMetadata({
|
||
title,
|
||
artist,
|
||
artwork
|
||
});
|
||
|
||
navigator.mediaSession.metadata = metadata;
|
||
}
|
||
|
||
public setCurrentMediadata() {
|
||
const {playingMedia} = this;
|
||
if(!playingMedia) return;
|
||
const message = this.getMessageByMedia(playingMedia);
|
||
this.setNewMediadata(message, playingMedia);
|
||
}
|
||
|
||
private getMessageByMedia(media: HTMLMediaElement): Message.message {
|
||
const details = this.mediaDetails.get(media);
|
||
return details.message;
|
||
// const {peerId, mid} = details;
|
||
// const message = details.isScheduled ?
|
||
// this.managers.appMessagesManager.getScheduledMessageByPeer(peerId, mid) :
|
||
// this.managers.appMessagesManager.getMessageByPeer(peerId, mid);
|
||
// return message;
|
||
}
|
||
|
||
public getPlayingDetails() {
|
||
const {playingMedia} = this;
|
||
if(!playingMedia) {
|
||
return;
|
||
}
|
||
|
||
const message = this.getMessageByMedia(playingMedia);
|
||
return {
|
||
doc: getMediaFromMessage(message) as MyDocument,
|
||
message,
|
||
media: playingMedia,
|
||
playbackParams: this.getPlaybackParams()
|
||
};
|
||
}
|
||
|
||
private onPlay = (e?: Event) => {
|
||
const media = e.target as HTMLMediaElement;
|
||
const details = this.mediaDetails.get(media);
|
||
const {peerId, mid} = details;
|
||
|
||
//console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media);
|
||
|
||
const pip = this.pip;
|
||
if(pip) {
|
||
pip.pause();
|
||
}
|
||
|
||
const message = this.getMessageByMedia(media);
|
||
|
||
const previousMedia = this.playingMedia;
|
||
if(previousMedia !== media) {
|
||
this.stop();
|
||
this.setMedia(media, message);
|
||
|
||
const verify = (element: MediaItem) => element.mid === mid && element.peerId === peerId;
|
||
const listLoader = this.listLoader;
|
||
const current = listLoader.getCurrent();
|
||
if(!current || !verify(current)) {
|
||
const previous = listLoader.getPrevious();
|
||
|
||
let idx = previous.findIndex(verify);
|
||
let jumpLength: number;
|
||
if(idx !== -1) {
|
||
jumpLength = -(previous.length - idx);
|
||
} else {
|
||
idx = listLoader.getNext().findIndex(verify);
|
||
if(idx !== -1) {
|
||
jumpLength = idx + 1;
|
||
}
|
||
}
|
||
|
||
if(idx !== -1) {
|
||
if(jumpLength) {
|
||
this.go(jumpLength, false);
|
||
}
|
||
} else {
|
||
this.setTargets({peerId, mid});
|
||
}
|
||
}
|
||
}
|
||
|
||
// audio_pause не успеет сработать без таймаута
|
||
setTimeout(() => {
|
||
if(this.playingMedia !== media) {
|
||
return;
|
||
}
|
||
|
||
this.dispatchEvent('play', this.getPlayingDetails());
|
||
this.pauseMediaInOtherTabs();
|
||
}, 0);
|
||
};
|
||
|
||
private onPause = (e?: Event) => {
|
||
/* const target = e.target as HTMLMediaElement;
|
||
if(!isInDOM(target)) {
|
||
this.container.append(target);
|
||
target.play();
|
||
return;
|
||
} */
|
||
|
||
// if(this.pip) {
|
||
// this.pip.play();
|
||
// }
|
||
|
||
this.dispatchEvent('pause');
|
||
};
|
||
|
||
private onEnded = (e?: Event) => {
|
||
if(!e.isTrusted) {
|
||
return;
|
||
}
|
||
|
||
this.onPause(e);
|
||
|
||
//console.log('on media end');
|
||
|
||
const listLoader = this.listLoader;
|
||
if(this.lockedSwitchers ||
|
||
(!this.round && listLoader.current && !listLoader.next.length) ||
|
||
!listLoader.getNext().length ||
|
||
!this.next()) {
|
||
this.stop();
|
||
this.dispatchEvent('stop');
|
||
}
|
||
};
|
||
|
||
public pauseMediaInOtherTabs() {
|
||
this.skipMediaPlayEvent = true;
|
||
rootScope.dispatchEvent('media_play');
|
||
}
|
||
|
||
// public get pip() {
|
||
// return document.pictureInPictureElement as HTMLVideoElement;
|
||
// }
|
||
|
||
public toggle(play?: boolean, media = this.playingMedia) {
|
||
if(!media) {
|
||
return false;
|
||
}
|
||
|
||
if(play === undefined) {
|
||
play = media.paused;
|
||
}
|
||
|
||
if(media.paused !== play) {
|
||
return false;
|
||
}
|
||
|
||
if(play) {
|
||
media.play();
|
||
} else {
|
||
media.pause();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public play = () => {
|
||
return this.toggle(true);
|
||
};
|
||
|
||
public pause = () => {
|
||
return this.toggle(false);
|
||
};
|
||
|
||
public stop = (media = this.playingMedia) => {
|
||
if(!media) {
|
||
return false;
|
||
}
|
||
|
||
if(!media.paused) {
|
||
media.pause();
|
||
}
|
||
|
||
media.currentTime = 0;
|
||
simulateEvent(media, 'ended');
|
||
|
||
if(media === this.playingMedia) {
|
||
const details = this.mediaDetails.get(media);
|
||
if(details?.clean) {
|
||
media.src = '';
|
||
const peerId = details.peerId;
|
||
const s = details.isScheduled ? this.scheduled : this.media;
|
||
const storage = s.get(peerId);
|
||
if(storage) {
|
||
storage.delete(details.mid);
|
||
|
||
if(!storage.size) {
|
||
s.delete(peerId);
|
||
}
|
||
}
|
||
|
||
media.remove();
|
||
|
||
this.mediaDetails.delete(media);
|
||
}
|
||
|
||
this.playingMedia = undefined;
|
||
this.playingMediaType = undefined;
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
public playItem = (item: MediaItem) => {
|
||
const {peerId, mid} = item;
|
||
const isScheduled = this.searchContext.isScheduled;
|
||
const media = this.getMedia(peerId, mid, isScheduled);
|
||
|
||
/* if(isSafari) {
|
||
media.autoplay = true;
|
||
} */
|
||
|
||
media.play();
|
||
|
||
setTimeout(() => {
|
||
this.resolveWaitingForLoadMedia(peerId, mid, isScheduled);
|
||
}, 0);
|
||
};
|
||
|
||
public go = (length: number, dispatchJump?: boolean) => {
|
||
const listLoader = this.listLoader;
|
||
if(this.lockedSwitchers || !listLoader) {
|
||
return;
|
||
}
|
||
|
||
if(this.playingMediaType === 'audio') {
|
||
return listLoader.goRound(length, dispatchJump);
|
||
} else {
|
||
return listLoader.go(length, dispatchJump);
|
||
}
|
||
};
|
||
|
||
private bindBrowserCallback(cb: (video: HTMLVideoElement, details: MediaSessionActionDetails) => void) {
|
||
const handler: MediaSessionActionHandler = (details) => {
|
||
cb(this.pip, details);
|
||
};
|
||
|
||
return handler;
|
||
}
|
||
|
||
public browserPlay = this.bindBrowserCallback((video) => this.toggle(true, video));
|
||
public browserPause = this.bindBrowserCallback((video) => this.toggle(false, video));
|
||
public browserStop = this.bindBrowserCallback((video) => this.stop(video));
|
||
public browserSeekBackward = this.bindBrowserCallback((video, details) => this.seekBackward(details, video));
|
||
public browserSeekForward = this.bindBrowserCallback((video, details) => this.seekForward(details, video));
|
||
public browserSeekTo = this.bindBrowserCallback((video, details) => this.seekTo(details, video));
|
||
public browserNext = this.bindBrowserCallback((video) => video || this.next());
|
||
public browserPrevious = this.bindBrowserCallback((video) => video ? this.seekToStart(video) : this.previous());
|
||
|
||
public next = () => {
|
||
return this.go(1);
|
||
};
|
||
|
||
public previous = () => {
|
||
if(this.seekToStart(this.playingMedia)) {
|
||
return;
|
||
}
|
||
|
||
return this.go(-1);
|
||
};
|
||
|
||
public seekToStart(media: HTMLMediaElement) {
|
||
if(media?.currentTime > 5) {
|
||
media.currentTime = 0;
|
||
this.toggle(true, media);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public willBePlayed(media: HTMLMediaElement) {
|
||
this.willBePlayedMedia = media;
|
||
}
|
||
|
||
public setSearchContext(context: MediaSearchContext) {
|
||
if(deepEqual(this.searchContext, context)) {
|
||
return false;
|
||
}
|
||
|
||
this.searchContext = copy(context); // {_: type === 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice'}
|
||
return true;
|
||
}
|
||
|
||
public getSearchContext() {
|
||
return this.searchContext;
|
||
}
|
||
|
||
public setTargets(current: MediaItem, prev?: MediaItem[], next?: MediaItem[]) {
|
||
let listLoader = this.listLoader;
|
||
if(!listLoader) {
|
||
listLoader = this.listLoader = new SearchListLoader({
|
||
loadCount: 10,
|
||
loadWhenLeft: 5,
|
||
processItem: (message: Message.message) => {
|
||
this.addMedia(message, false);
|
||
return {peerId: message.peerId, mid: message.mid};
|
||
},
|
||
onJump: (item, older) => {
|
||
this.playItem(item);
|
||
},
|
||
onEmptied: () => {
|
||
this.dispatchEvent('stop');
|
||
this.stop();
|
||
}
|
||
});
|
||
} else {
|
||
listLoader.reset();
|
||
}
|
||
|
||
const reverse = this.searchContext.folderId !== undefined ? false : true;
|
||
if(prev) {
|
||
listLoader.setTargets(prev, next, reverse);
|
||
} else {
|
||
listLoader.reverse = reverse;
|
||
}
|
||
|
||
listLoader.setSearchContext(this.searchContext);
|
||
listLoader.current = current;
|
||
|
||
listLoader.load(true);
|
||
listLoader.load(false);
|
||
}
|
||
|
||
private getPlaybackMediaTypeFromMessage(message: Message.message) {
|
||
const doc = getMediaFromMessage(message) as MyDocument;
|
||
let mediaType: PlaybackMediaType = 'audio';
|
||
if(doc?.type) {
|
||
if(doc.type === 'voice' || doc.type === 'round') {
|
||
mediaType = 'voice';
|
||
} else if(doc.type === 'video') {
|
||
mediaType = 'video';
|
||
}
|
||
}
|
||
|
||
return mediaType;
|
||
}
|
||
|
||
public setMedia(media: HTMLMediaElement, message: Message.message) {
|
||
const mediaType = this.getPlaybackMediaTypeFromMessage(message);
|
||
|
||
this._playbackRate = this.playbackRates[mediaType];
|
||
|
||
this.playingMedia = media;
|
||
this.playingMediaType = mediaType;
|
||
this.playingMedia.volume = this.volume;
|
||
this.playingMedia.muted = this.muted;
|
||
this.playingMedia.playbackRate = this.playbackRate;
|
||
|
||
if(mediaType === 'audio') {
|
||
this.playingMedia.loop = this.loop;
|
||
}
|
||
|
||
if('mediaSession' in navigator) {
|
||
this.setNewMediadata(message);
|
||
}
|
||
}
|
||
|
||
public setSingleMedia(media?: HTMLMediaElement, message?: Message.message) {
|
||
const playingMedia = this.playingMedia;
|
||
|
||
const wasPlaying = this.pause();
|
||
|
||
let onPlay: () => void;
|
||
if(media) {
|
||
onPlay = () => {
|
||
const pip = this.pip;
|
||
if(pip) {
|
||
pip.pause();
|
||
}
|
||
|
||
this.pauseMediaInOtherTabs();
|
||
};
|
||
|
||
if(!media.paused) {
|
||
onPlay();
|
||
}
|
||
|
||
media.addEventListener('play', onPlay);
|
||
} else { // maybe it's voice recording
|
||
this.pauseMediaInOtherTabs();
|
||
}
|
||
|
||
this.willBePlayed(undefined);
|
||
if(media) this.setMedia(media, message);
|
||
else this.playingMedia = undefined;
|
||
this.toggleSwitchers(false);
|
||
|
||
return (playPaused = wasPlaying) => {
|
||
this.toggleSwitchers(true);
|
||
|
||
if(playingMedia) {
|
||
if(this.mediaDetails.get(playingMedia)) {
|
||
this.setMedia(playingMedia, this.getMessageByMedia(playingMedia));
|
||
} else {
|
||
this.next() || this.previous();
|
||
}
|
||
}
|
||
|
||
// If it's still not cleaned
|
||
if(this.playingMedia === media) {
|
||
this.playingMedia = undefined;
|
||
this.playingMediaType = undefined;
|
||
}
|
||
|
||
if(media) {
|
||
media.removeEventListener('play', onPlay);
|
||
}
|
||
|
||
// I don't remember what it was for
|
||
// if(media && this.playingMedia === media) {
|
||
// this.stop();
|
||
// }
|
||
|
||
if(playPaused) {
|
||
this.play();
|
||
}
|
||
};
|
||
}
|
||
|
||
public toggleSwitchers(enabled: boolean) {
|
||
this.lockedSwitchers = !enabled;
|
||
}
|
||
|
||
public setPictureInPicture(video: HTMLVideoElement) {
|
||
this.pip = video;
|
||
|
||
// let wasPlaying = this.pause();
|
||
|
||
const listenerSetter = new ListenerSetter();
|
||
listenerSetter.add(video)('leavepictureinpicture', () => {
|
||
if(this.pip !== video) {
|
||
return;
|
||
}
|
||
|
||
this.pip = undefined;
|
||
// if(wasPlaying) {
|
||
// this.play();
|
||
// }
|
||
|
||
listenerSetter.removeAll();
|
||
}, {once: true});
|
||
|
||
listenerSetter.add(video)('play', (e) => {
|
||
if(this.playingMedia !== video) {
|
||
this.pause();
|
||
}
|
||
|
||
this.pauseMediaInOtherTabs();
|
||
// if(this.pause()) {
|
||
// listenerSetter.add(video)('pause', () => {
|
||
// this.play();
|
||
// }, {once: true});
|
||
// }
|
||
});
|
||
}
|
||
}
|
||
|
||
const appMediaPlaybackController = new AppMediaPlaybackController();
|
||
MOUNT_CLASS_TO.appMediaPlaybackController = appMediaPlaybackController;
|
||
export default appMediaPlaybackController;
|