Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
966 lines
28 KiB
966 lines
28 KiB
/* |
|
* 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; |
|
|
|
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); |
|
} |
|
} |
|
}); |
|
|
|
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()); |
|
}, 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 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(); |
|
} |
|
}; |
|
|
|
if(!media.paused) { |
|
onPlay(); |
|
} |
|
|
|
media.addEventListener('play', onPlay); |
|
} |
|
|
|
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(); |
|
} |
|
// if(this.pause()) { |
|
// listenerSetter.add(video)('pause', () => { |
|
// this.play(); |
|
// }, {once: true}); |
|
// } |
|
}); |
|
} |
|
} |
|
|
|
const appMediaPlaybackController = new AppMediaPlaybackController(); |
|
MOUNT_CLASS_TO.appMediaPlaybackController = appMediaPlaybackController; |
|
export default appMediaPlaybackController;
|
|
|