Single playback controller for audio & round video

This commit is contained in:
morethanwords 2020-08-29 14:45:37 +03:00
parent 994bbccde9
commit fb1ebeb9a1
8 changed files with 268 additions and 178 deletions

View File

@ -1,149 +0,0 @@
import { MTDocument } from "../types";
import { $rootScope } from "../lib/utils";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appDocsManager from "../lib/appManagers/appDocsManager";
import opusDecodeController from "../lib/opusDecodeController";
// TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда
class AppAudio {
private container: HTMLElement;
private audios: {[mid: string]: HTMLAudioElement} = {};
private playingAudio: HTMLAudioElement;
public willBePlayedAudio: HTMLAudioElement;
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 addAudio(doc: MTDocument, mid: number) {
if(this.audios[mid]) return this.audios[mid];
const audio = document.createElement('audio');
const source = document.createElement('source');
source.type = doc.type == 'voice' && !opusDecodeController.isPlaySupported() ? 'audio/wav' : doc.mime_type;
audio.autoplay = false;
audio.volume = 1;
audio.append(source);
audio.addEventListener('playing', (e) => {
if(this.playingAudio != audio) {
if(this.playingAudio && !this.playingAudio.paused) {
this.playingAudio.pause();
}
this.playingAudio = audio;
this.loadSiblingsAudio(doc.type as 'voice' | 'audio', mid);
}
// audio_pause не успеет сработать без таймаута
setTimeout(() => {
$rootScope.$broadcast('audio_play', {doc, mid});
}, 0);
});
audio.addEventListener('pause', this.onPause);
audio.addEventListener('ended', this.onEnded);
const onError = (e: Event) => {
if(this.nextMid == mid) {
this.loadSiblingsAudio(doc.type as 'voice' | 'audio', mid).then(() => {
if(this.nextMid && this.audios[this.nextMid]) {
this.audios[this.nextMid].play();
}
})
}
};
audio.addEventListener('error', onError);
const downloadPromise: Promise<any> = !doc.supportsStreaming ? appDocsManager.downloadDocNew(doc.id) : Promise.resolve();
downloadPromise.then(() => {
this.container.append(audio);
source.src = doc.url;
}, onError);
return this.audios[mid] = audio;
}
onPause = (e: Event) => {
$rootScope.$broadcast('audio_pause');
};
onEnded = (e: Event) => {
this.onPause(e);
if(this.nextMid) {
this.audios[this.nextMid].play();
}
};
private loadSiblingsAudio(type: 'voice' | 'audio', mid: number) {
const audio = this.playingAudio;
const message = appMessagesManager.getMessage(mid);
this.prevMid = this.nextMid = 0;
return appMessagesManager.getSearch(message.peerID, '', {
_: type == 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterVoice'
}, mid, 3, 0, 2).then(value => {
if(this.playingAudio != audio) {
return;
}
for(let 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.getMessage(mid);
this.addAudio(message.media.document, mid);
});
//console.log('loadSiblingsAudio', audio, type, mid, value, this.prevMid, this.nextMid);
});
}
public toggle() {
if(!this.playingAudio) return;
if(this.playingAudio.paused) {
this.playingAudio.play();
} else {
this.playingAudio.pause();
}
}
public pause() {
if(!this.playingAudio || this.playingAudio.paused) return;
this.playingAudio.pause();
}
public willBePlayed(audio: HTMLAudioElement) {
this.willBePlayedAudio = audio;
}
public audioExists(mid: number) {
return !!this.audios[mid];
}
}
const appAudio = new AppAudio();
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).appAudio = appAudio;
}
export default appAudio;

View File

@ -0,0 +1,157 @@
import { MTDocument } from "../types";
import { $rootScope } from "../lib/utils";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appDocsManager from "../lib/appManagers/appDocsManager";
// TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда
type HTMLMediaElement = HTMLAudioElement | HTMLVideoElement;
type MediaType = 'voice' | 'audio' | 'round';
class AppMediaPlaybackController {
private container: HTMLElement;
private media: {[mid: string]: HTMLMediaElement} = {};
private playingMedia: HTMLMediaElement;
public willBePlayedMedia: HTMLMediaElement;
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(doc: MTDocument, mid: number): HTMLMediaElement {
if(this.media[mid]) return this.media[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;
media.autoplay = false;
media.volume = 1;
//media.append(source);
media.addEventListener('playing', () => {
if(this.playingMedia != media) {
if(this.playingMedia && !this.playingMedia.paused) {
this.playingMedia.pause();
}
this.playingMedia = media;
this.loadSiblingsMedia(doc.type as MediaType, mid);
}
// audio_pause не успеет сработать без таймаута
setTimeout(() => {
$rootScope.$broadcast('audio_play', {doc, mid});
}, 0);
});
media.addEventListener('pause', this.onPause);
media.addEventListener('ended', this.onEnded);
const onError = (e: Event) => {
if(this.nextMid == mid) {
this.loadSiblingsMedia(doc.type as MediaType, mid).then(() => {
if(this.nextMid && this.media[this.nextMid]) {
this.media[this.nextMid].play();
}
});
}
};
media.addEventListener('error', onError);
const downloadPromise: Promise<any> = !doc.supportsStreaming ? appDocsManager.downloadDocNew(doc.id) : Promise.resolve();
downloadPromise.then(() => {
//if(doc.type != 'round') {
this.container.append(media);
//}
//source.src = doc.url;
media.src = doc.url;
}, onError);
return this.media[mid] = media;
}
onPause = (e: Event) => {
$rootScope.$broadcast('audio_pause');
};
onEnded = (e: Event) => {
this.onPause(e);
if(this.nextMid) {
this.media[this.nextMid].play();
}
};
private loadSiblingsMedia(type: MediaType, mid: number) {
const media = this.playingMedia;
const message = appMessagesManager.getMessage(mid);
this.prevMid = this.nextMid = 0;
return appMessagesManager.getSearch(message.peerID, '', {
//_: type == 'audio' ? 'inputMessagesFilterMusic' : (type == 'round' ? 'inputMessagesFilterRoundVideo' : 'inputMessagesFilterVoice')
_: type == 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice'
}, mid, 3, 0, 2).then(value => {
if(this.playingMedia != media) {
return;
}
for(let 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.getMessage(mid);
this.addMedia(message.media.document, mid);
});
//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;
}
public mediaExists(mid: number) {
return !!this.media[mid];
}
}
const appMediaPlaybackController = new AppMediaPlaybackController();
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).appMediaPlaybackController = appMediaPlaybackController;
}
export default appMediaPlaybackController;

View File

@ -3,7 +3,7 @@ import { RichTextProcessor } from "../lib/richtextprocessor";
import { formatDate } from "./wrappers";
import ProgressivePreloader from "./preloader";
import { MediaProgressLine } from "../lib/mediaPlayer";
import appAudio from "./appAudio";
import appMediaPlaybackController from "./appMediaPlaybackController";
import { MTDocument } from "../types";
import { mediaSizes } from "../lib/config";
import { Download } from "../lib/appManagers/appDownloadManager";
@ -313,7 +313,7 @@ export default class AudioElement extends HTMLElement {
audioTimeDiv.innerHTML = durationStr;
const onLoad = () => {
const audio = this.audio = appAudio.addAudio(doc, mid);
const audio = this.audio = appMediaPlaybackController.addMedia(doc, mid);
this.onTypeDisconnect = onTypeLoad();
@ -390,13 +390,13 @@ export default class AudioElement extends HTMLElement {
this.addEventListener('click', onClick);
this.click();
} else {
if(appAudio.audioExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано
if(appMediaPlaybackController.mediaExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано
onLoad();
} else {
const r = () => {
onLoad();
appAudio.willBePlayed(this.audio); // prepare for loading audio
appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio
if(!preloader) {
preloader = new ProgressivePreloader(null, false);
@ -413,9 +413,9 @@ export default class AudioElement extends HTMLElement {
//setTimeout(() => {
// release loaded audio
if(appAudio.willBePlayedAudio == this.audio) {
if(appMediaPlaybackController.willBePlayedMedia == this.audio) {
this.audio.play();
appAudio.willBePlayedAudio = null;
appMediaPlaybackController.willBePlayedMedia = null;
}
//}, 10e3);
});

View File

@ -1,7 +1,7 @@
import appPhotosManager, { MTPhoto } from '../lib/appManagers/appPhotosManager';
import LottieLoader from '../lib/lottieLoader';
import appDocsManager from "../lib/appManagers/appDocsManager";
import { formatBytes, getEmojiToneIndex } from "../lib/utils";
import { formatBytes, getEmojiToneIndex, isInDOM } from "../lib/utils";
import ProgressivePreloader from './preloader';
import LazyLoadQueue from './lazyLoadQueue';
import VideoPlayer from '../lib/mediaPlayer';
@ -17,6 +17,7 @@ import AudioElement from './audio';
import { DownloadBlob } from '../lib/appManagers/appDownloadManager';
import webpWorkerController from '../lib/webp/webpWorkerController';
import { readBlobAsText } from '../helpers/blob';
import appMediaPlaybackController from './appMediaPlaybackController';
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group}: {
doc: MTDocument,
@ -55,7 +56,72 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
return wrapPhoto(doc, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware);
}
/* const video = doc.type == 'round' ? appMediaPlaybackController.addMedia(doc, message.mid) as HTMLVideoElement : document.createElement('video');
if(video.parentElement) {
video.remove();
} */
const video = document.createElement('video');
if(doc.type == 'round') {
video.muted = true;
const globalVideo = appMediaPlaybackController.addMedia(doc, message.mid);
video.addEventListener('canplay', () => {
if(globalVideo.currentTime > 0) {
video.currentTime = globalVideo.currentTime;
}
if(!globalVideo.paused) {
// с закоментированными настройками - хром выключал видео при скролле, для этого нужно было включить видео - выйти из диалога, зайти заново и проскроллить вверх
/* video.autoplay = true;
video.loop = false; */
video.play();
}
}, {once: true});
const clear = () => {
//console.log('clearing video');
globalVideo.removeEventListener('timeupdate', onTimeUpdate);
globalVideo.removeEventListener('play', onGlobalPlay);
globalVideo.removeEventListener('pause', onGlobalPause);
video.removeEventListener('play', onVideoPlay);
video.removeEventListener('pause', onVideoPause);
};
const onTimeUpdate = () => {
if(!isInDOM(video)) {
clear();
}
};
const onGlobalPlay = () => {
video.play();
};
const onGlobalPause = () => {
video.pause();
};
const onVideoPlay = () => {
globalVideo.play();
};
const onVideoPause = () => {
//console.log('video pause event');
if(isInDOM(video)) {
globalVideo.pause();
} else {
clear();
}
};
globalVideo.addEventListener('timeupdate', onTimeUpdate);
globalVideo.addEventListener('play', onGlobalPlay);
globalVideo.addEventListener('pause', onGlobalPause);
video.addEventListener('play', onVideoPlay);
video.addEventListener('pause', onVideoPause);
}
let img: HTMLImageElement;
if(message) {
@ -154,7 +220,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
}, {once: true});
//}
renderImageFromUrl(video, doc.url);
//if(doc.type != 'round') {
renderImageFromUrl(video, doc.url);
//}
video.setAttribute('playsinline', '');
/* if(!container.parentElement) {

View File

@ -35,7 +35,7 @@ import PopupStickers from '../../components/popupStickers';
import SearchInput from '../../components/searchInput';
import AppSearch, { SearchGroup } from '../../components/appSearch';
import PopupDatePicker from '../../components/popupDatepicker';
import appAudio from '../../components/appAudio';
import appMediaPlaybackController from '../../components/appMediaPlaybackController';
import appPollsManager from './appPollsManager';
import { ripple } from '../../components/ripple';
import { horizontalMenu } from '../../components/horizontalMenu';
@ -420,20 +420,20 @@ class ChatAudio {
this.container.style.display = 'none';
this.container.parentElement.classList.remove('is-audio-shown');
if(this.toggle.classList.contains('flip-icon')) {
appAudio.toggle();
appMediaPlaybackController.toggle();
}
});
this.toggle.addEventListener('click', (e) => {
cancelEvent(e);
appAudio.toggle();
appMediaPlaybackController.toggle();
});
$rootScope.$on('audio_play', (e: CustomEvent) => {
const {doc, mid} = e.detail;
let title: string, subtitle: string;
if(doc.type == 'voice') {
if(doc.type == 'voice' || doc.type == 'round') {
const message = appMessagesManager.getMessage(mid);
title = appPeersManager.getPeerTitle(message.fromID, false, true);
//subtitle = 'Voice message';

View File

@ -12,10 +12,9 @@ import AvatarElement from "../../components/avatar";
import LazyLoadQueue from "../../components/lazyLoadQueue";
import appForward from "../../components/appForward";
import { isSafari, mediaSizes, touchSupport } from "../config";
import appAudio from "../../components/appAudio";
import { deferredPromise } from "../polyfill";
import { MTDocument } from "../../types";
import idbFileStorage from "../idb";
import appMediaPlaybackController from "../../components/appMediaPlaybackController";
// TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию
// TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода)
@ -954,8 +953,8 @@ export class AppMediaViewer {
video.dataset.overlay = '1';
// fix for simultaneous play
appAudio.pause();
appAudio.willBePlayedAudio = null;
appMediaPlaybackController.pause();
appMediaPlaybackController.willBePlayedMedia = null;
Promise.all([canPlayThrough, onAnimationEnd]).then(() => {
const player = new VideoPlayer(video, true, media.supportsStreaming);

View File

@ -2896,7 +2896,7 @@ export class AppMessagesManager {
var neededContents: {
[type: string]: boolean
} = {},
neededDocType: string | boolean;
neededDocTypes: string[] = [];
var neededLimit = limit || 20;
var message;
@ -2908,32 +2908,36 @@ export class AppMessagesManager {
case 'inputMessagesFilterPhotoVideo':
neededContents['messageMediaPhoto'] = true;
neededContents['messageMediaDocument'] = true;
neededDocType = 'video';
neededDocTypes.push('video');
break;
case 'inputMessagesFilterVideo':
neededContents['messageMediaDocument'] = true;
neededDocType = 'video';
neededDocTypes.push('video');
break;
case 'inputMessagesFilterDocument':
neededContents['messageMediaDocument'] = true;
neededDocType = false;
break;
case 'inputMessagesFilterVoice':
neededContents['messageMediaDocument'] = true;
neededDocType = 'voice';
neededDocTypes.push('voice');
break;
case 'inputMessagesFilterRoundVoice':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('round', 'voice');
break;
case 'inputMessagesFilterRoundVideo':
neededContents['messageMediaDocument'] = true;
neededDocType = 'round';
neededDocTypes.push('round');
break;
case 'inputMessagesFilterMusic':
neededContents['messageMediaDocument'] = true;
neededDocType = 'audio';
neededDocTypes.push('audio');
break;
case 'inputMessagesFilterUrl':
@ -2955,9 +2959,9 @@ export class AppMessagesManager {
for(let i = 0; i < historyStorage.history.length; i++) {
message = this.messagesStorage[historyStorage.history[i]];
if(message.media && neededContents[message.media._]) {
if(neededDocType !== undefined &&
if(neededDocTypes.length &&
message.media._ == 'messageMediaDocument' &&
message.media.document.type != neededDocType) {
!neededDocTypes.includes(message.media.document.type)) {
continue;
}

View File

@ -1,4 +1,4 @@
import { cancelEvent, whichChild } from "./utils";
import { cancelEvent } from "./utils";
import { touchSupport } from "./config";
export class ProgressLine {
@ -7,7 +7,7 @@ export class ProgressLine {
protected seek: HTMLInputElement;
protected duration = 1;
protected mousedown = false;
public mousedown = false;
private events: Partial<{
//onMouseMove: ProgressLine['onMouseMove'],
@ -343,7 +343,9 @@ export default class VideoPlayer {
volumeSvg.innerHTML = `<path d="${d}"></path>`;
} catch(err) {}
volumeProgress.setProgress(video.muted ? 0 : volume);
if(!volumeProgress.mousedown) {
volumeProgress.setProgress(video.muted ? 0 : volume);
}
};
// не вызовется повторно если на 1 установить 1
@ -439,6 +441,14 @@ export default class VideoPlayer {
iconVolume.style.display = '';
});
}
video.addEventListener('play', () => {
this.wrapper.classList.add('is-playing');
});
video.addEventListener('pause', () => {
this.wrapper.classList.remove('is-playing');
});
if(video.duration > 0) {
timeDuration.innerHTML = String(Math.round(video.duration)).toHHMMSS();
@ -469,7 +479,7 @@ export default class VideoPlayer {
}
this.video[this.video.paused ? 'play' : 'pause']();
this.video.paused ? this.wrapper.classList.remove('is-playing') : this.wrapper.classList.add('is-playing');
//this.wrapper.classList.toggle('is-playing', !this.video.paused);
}
private handleProgress(timeDuration: HTMLElement, circumference: number, circle: SVGCircleElement, updateInterval: number) {