276 lines
8.5 KiB
TypeScript
276 lines
8.5 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 appMessagesManager from "../lib/appManagers/appMessagesManager";
|
||
import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager";
|
||
import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise";
|
||
import { isSafari } from "../helpers/userAgent";
|
||
import { MOUNT_CLASS_TO } from "../config/debug";
|
||
|
||
// TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда
|
||
|
||
// TODO: Safari: проверить стрим, включить его и сразу попробовать включить видео или другую песню
|
||
// TODO: Safari: попробовать замаскировать подгрузку последнего чанка
|
||
// TODO: Safari: пофиксить момент, когда заканчивается песня и пытаешься включить её заново - прогресс сразу в конце
|
||
|
||
type HTMLMediaElement = HTMLAudioElement | HTMLVideoElement;
|
||
|
||
type MediaType = 'voice' | 'audio' | 'round';
|
||
|
||
class AppMediaPlaybackController {
|
||
private container: HTMLElement;
|
||
private media: {
|
||
[peerId: string]: {
|
||
[mid: string]: HTMLMediaElement
|
||
}
|
||
} = {};
|
||
private playingMedia: HTMLMediaElement;
|
||
|
||
private waitingMediaForLoad: {
|
||
[peerId: string]: {
|
||
[mid: string]: CancellablePromise<void>
|
||
}
|
||
} = {};
|
||
|
||
public willBePlayedMedia: HTMLMediaElement;
|
||
|
||
private currentPeerId: number;
|
||
private prevMid: number;
|
||
private nextMid: number;
|
||
|
||
constructor() {
|
||
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);
|
||
}
|
||
|
||
public addMedia(peerId: number, doc: MyDocument, mid: number, autoload = true): HTMLMediaElement {
|
||
const storage = this.media[peerId] ?? (this.media[peerId] = {});
|
||
if(storage[mid]) return storage[mid];
|
||
|
||
const media = document.createElement(doc.type === 'round' ? '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;
|
||
}
|
||
|
||
media.dataset.mid = '' + mid;
|
||
media.dataset.type = doc.type;
|
||
|
||
//media.autoplay = true;
|
||
media.volume = 1;
|
||
//media.append(source);
|
||
|
||
this.container.append(media);
|
||
|
||
media.addEventListener('playing', () => {
|
||
this.currentPeerId = peerId;
|
||
|
||
//console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media);
|
||
|
||
if(this.playingMedia !== media) {
|
||
if(this.playingMedia && !this.playingMedia.paused) {
|
||
this.playingMedia.pause();
|
||
}
|
||
|
||
this.playingMedia = media;
|
||
this.loadSiblingsMedia(peerId, doc.type as MediaType, mid);
|
||
}
|
||
|
||
// audio_pause не успеет сработать без таймаута
|
||
setTimeout(() => {
|
||
rootScope.broadcast('audio_play', {peerId, doc, mid});
|
||
}, 0);
|
||
});
|
||
|
||
media.addEventListener('pause', this.onPause);
|
||
media.addEventListener('ended', this.onEnded);
|
||
|
||
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 waitingStorage = this.waitingMediaForLoad[peerId] ?? (this.waitingMediaForLoad[peerId] = {});
|
||
waitingStorage[mid] = deferred;
|
||
}
|
||
|
||
deferred.then(() => {
|
||
//media.autoplay = true;
|
||
//console.log('will set media url:', media, doc, doc.type, doc.url);
|
||
|
||
((!doc.supportsStreaming ? appDocsManager.downloadDoc(doc) : Promise.resolve()) as Promise<any>).then(() => {
|
||
if(doc.type === 'audio' && doc.supportsStreaming && isSafari) {
|
||
this.handleSafariStreamable(media);
|
||
}
|
||
|
||
media.src = doc.url;
|
||
});
|
||
}, onError);
|
||
|
||
return storage[mid] = media;
|
||
}
|
||
|
||
// 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: number, mid: number) {
|
||
const storage = this.waitingMediaForLoad[peerId] ?? (this.waitingMediaForLoad[peerId] = {});
|
||
const promise = storage[mid];
|
||
if(promise) {
|
||
promise.resolve();
|
||
delete storage[mid];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Only for audio
|
||
*/
|
||
public isSafariBuffering(media: HTMLMediaElement) {
|
||
/// @ts-ignore
|
||
return !!media.safariBuffering;
|
||
}
|
||
|
||
private setSafariBuffering(media: HTMLMediaElement, value: boolean) {
|
||
// @ts-ignore
|
||
media.safariBuffering = value;
|
||
}
|
||
|
||
onPause = (e?: Event) => {
|
||
/* const target = e.target as HTMLMediaElement;
|
||
if(!isInDOM(target)) {
|
||
this.container.append(target);
|
||
target.play();
|
||
return;
|
||
} */
|
||
|
||
rootScope.broadcast('audio_pause');
|
||
};
|
||
|
||
onEnded = (e?: Event) => {
|
||
this.onPause(e);
|
||
|
||
//console.log('on media end');
|
||
|
||
if(this.nextMid) {
|
||
const media = this.media[this.currentPeerId][this.nextMid];
|
||
|
||
/* if(isSafari) {
|
||
media.autoplay = true;
|
||
} */
|
||
|
||
this.resolveWaitingForLoadMedia(this.currentPeerId, this.nextMid);
|
||
|
||
setTimeout(() => {
|
||
media.play()//.catch(() => {});
|
||
}, 0);
|
||
}
|
||
};
|
||
|
||
private loadSiblingsMedia(peerId: number, type: MediaType, mid: number) {
|
||
const media = this.playingMedia;
|
||
this.prevMid = this.nextMid = 0;
|
||
|
||
return appMessagesManager.getSearch({
|
||
peerId,
|
||
query: '',
|
||
inputFilter: {
|
||
//_: type === 'audio' ? 'inputMessagesFilterMusic' : (type === 'round' ? 'inputMessagesFilterRoundVideo' : 'inputMessagesFilterVoice')
|
||
_: type === 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice'
|
||
},
|
||
maxId: mid,
|
||
limit: 3,
|
||
backLimit: 2
|
||
}).then(value => {
|
||
if(this.playingMedia !== media) {
|
||
return;
|
||
}
|
||
|
||
for(const {mid: m} of value.history) {
|
||
if(m > mid) {
|
||
this.nextMid = m;
|
||
} else if(m < mid) {
|
||
this.prevMid = m;
|
||
break;
|
||
}
|
||
}
|
||
|
||
[this.prevMid, this.nextMid].filter(Boolean).forEach(mid => {
|
||
const message = appMessagesManager.getMessageByPeer(peerId, mid);
|
||
this.addMedia(peerId, message.media.document, mid, false);
|
||
});
|
||
|
||
//console.log('loadSiblingsAudio', audio, type, mid, value, this.prevMid, this.nextMid);
|
||
});
|
||
}
|
||
|
||
public toggle() {
|
||
if(!this.playingMedia) return;
|
||
|
||
if(this.playingMedia.paused) {
|
||
this.playingMedia.play();
|
||
} else {
|
||
this.playingMedia.pause();
|
||
}
|
||
}
|
||
|
||
public pause() {
|
||
if(!this.playingMedia || this.playingMedia.paused) return;
|
||
this.playingMedia.pause();
|
||
}
|
||
|
||
public willBePlayed(media: HTMLMediaElement) {
|
||
this.willBePlayedMedia = media;
|
||
}
|
||
}
|
||
|
||
const appMediaPlaybackController = new AppMediaPlaybackController();
|
||
MOUNT_CLASS_TO.appMediaPlaybackController = appMediaPlaybackController;
|
||
export default appMediaPlaybackController;
|