552 lines
17 KiB
TypeScript
552 lines
17 KiB
TypeScript
/*
|
|
* https://github.com/morethanwords/tweb
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
*/
|
|
|
|
import { IS_SAFARI } from "../../environment/userAgent";
|
|
import { animateSingle } from "../../helpers/animation";
|
|
import { ChatAutoDownloadSettings } from "../../helpers/autoDownload";
|
|
import deferredPromise from "../../helpers/cancellablePromise";
|
|
import cancelEvent from "../../helpers/dom/cancelEvent";
|
|
import { attachClickEvent } from "../../helpers/dom/clickEvent";
|
|
import createVideo from "../../helpers/dom/createVideo";
|
|
import isInDOM from "../../helpers/dom/isInDOM";
|
|
import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl";
|
|
import mediaSizes, { ScreenSize } from "../../helpers/mediaSizes";
|
|
import onMediaLoad from "../../helpers/onMediaLoad";
|
|
import throttleWithRaf from "../../helpers/schedulers/throttleWithRaf";
|
|
import sequentialDom from "../../helpers/sequentialDom";
|
|
import toHHMMSS from "../../helpers/string/toHHMMSS";
|
|
import { Message, PhotoSize } from "../../layer";
|
|
import { MyDocument } from "../../lib/appManagers/appDocsManager";
|
|
import appDownloadManager from "../../lib/appManagers/appDownloadManager";
|
|
import appImManager from "../../lib/appManagers/appImManager";
|
|
import { AppManagers } from "../../lib/appManagers/managers";
|
|
import { NULL_PEER_ID } from "../../lib/mtproto/mtproto_config";
|
|
import rootScope from "../../lib/rootScope";
|
|
import { ThumbCache } from "../../lib/storages/thumbs";
|
|
import animationIntersector from "../animationIntersector";
|
|
import appMediaPlaybackController, { MediaSearchContext } from "../appMediaPlaybackController";
|
|
import { findMediaTargets } from "../audio";
|
|
import LazyLoadQueue from "../lazyLoadQueue";
|
|
import ProgressivePreloader from "../preloader";
|
|
import wrapPhoto from './photo';
|
|
|
|
const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
|
|
let roundVideoCircumference = 0;
|
|
mediaSizes.addEventListener('changeScreen', (from, to) => {
|
|
if(to === ScreenSize.mobile || from === ScreenSize.mobile) {
|
|
const elements = Array.from(document.querySelectorAll('.media-round .progress-ring')) as SVGSVGElement[];
|
|
const width = mediaSizes.active.round.width;
|
|
const halfSize = width / 2;
|
|
const radius = halfSize - 7;
|
|
roundVideoCircumference = 2 * Math.PI * radius;
|
|
elements.forEach((element) => {
|
|
element.setAttributeNS(null, 'width', '' + width);
|
|
element.setAttributeNS(null, 'height', '' + width);
|
|
|
|
const circle = element.firstElementChild as SVGCircleElement;
|
|
circle.setAttributeNS(null, 'cx', '' + halfSize);
|
|
circle.setAttributeNS(null, 'cy', '' + halfSize);
|
|
circle.setAttributeNS(null, 'r', '' + radius);
|
|
|
|
circle.style.strokeDasharray = roundVideoCircumference + ' ' + roundVideoCircumference;
|
|
circle.style.strokeDashoffset = '' + roundVideoCircumference;
|
|
});
|
|
}
|
|
});
|
|
|
|
export default async function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises, noPlayButton, size, searchContext, autoDownload, managers = rootScope.managers}: {
|
|
doc: MyDocument,
|
|
container?: HTMLElement,
|
|
message?: Message.message,
|
|
boxWidth?: number,
|
|
boxHeight?: number,
|
|
withTail?: boolean,
|
|
isOut?: boolean,
|
|
middleware?: () => boolean,
|
|
lazyLoadQueue?: LazyLoadQueue,
|
|
noInfo?: boolean,
|
|
noPlayButton?: boolean,
|
|
group?: string,
|
|
onlyPreview?: boolean,
|
|
withoutPreloader?: boolean,
|
|
loadPromises?: Promise<any>[],
|
|
autoDownload?: ChatAutoDownloadSettings,
|
|
size?: PhotoSize,
|
|
searchContext?: MediaSearchContext,
|
|
managers?: AppManagers
|
|
}) {
|
|
const autoDownloadSize = autoDownload?.video;
|
|
let noAutoDownload = autoDownloadSize === 0;
|
|
const isAlbumItem = !(boxWidth && boxHeight);
|
|
const canAutoplay = /* doc.sticker || */(
|
|
(
|
|
doc.type !== 'video' || (
|
|
doc.size <= MAX_VIDEO_AUTOPLAY_SIZE &&
|
|
!isAlbumItem
|
|
)
|
|
) && (doc.type === 'gif' ? rootScope.settings.autoPlay.gifs : rootScope.settings.autoPlay.videos)
|
|
);
|
|
let spanTime: HTMLElement, spanPlay: HTMLElement;
|
|
|
|
if(!noInfo) {
|
|
spanTime = document.createElement('span');
|
|
spanTime.classList.add('video-time');
|
|
container.append(spanTime);
|
|
|
|
let needPlayButton = false;
|
|
if(doc.type !== 'gif') {
|
|
spanTime.innerText = toHHMMSS(doc.duration, false);
|
|
|
|
if(!noPlayButton && doc.type !== 'round') {
|
|
if(canAutoplay && !noAutoDownload) {
|
|
spanTime.classList.add('tgico', 'can-autoplay');
|
|
} else {
|
|
needPlayButton = true;
|
|
}
|
|
}
|
|
} else {
|
|
spanTime.innerText = 'GIF';
|
|
|
|
if(!canAutoplay && !noPlayButton) {
|
|
needPlayButton = true;
|
|
noAutoDownload = undefined;
|
|
}
|
|
}
|
|
|
|
if(needPlayButton) {
|
|
spanPlay = document.createElement('span');
|
|
spanPlay.classList.add('video-play', 'tgico-largeplay', 'btn-circle', 'position-center');
|
|
container.append(spanPlay);
|
|
}
|
|
}
|
|
|
|
let res: {
|
|
thumb?: typeof photoRes,
|
|
loadPromise: Promise<any>
|
|
} = {} as any;
|
|
|
|
if(doc.mime_type === 'image/gif') {
|
|
const photoRes = await wrapPhoto({
|
|
photo: doc,
|
|
message,
|
|
container,
|
|
boxWidth,
|
|
boxHeight,
|
|
withTail,
|
|
isOut,
|
|
lazyLoadQueue,
|
|
middleware,
|
|
withoutPreloader,
|
|
loadPromises,
|
|
autoDownloadSize,
|
|
size,
|
|
managers
|
|
});
|
|
|
|
res.thumb = photoRes;
|
|
res.loadPromise = photoRes.loadPromises.full;
|
|
return res;
|
|
}
|
|
|
|
/* const video = doc.type === 'round' ? appMediaPlaybackController.addMedia(doc, message.mid) as HTMLVideoElement : document.createElement('video');
|
|
if(video.parentElement) {
|
|
video.remove();
|
|
} */
|
|
|
|
let preloader: ProgressivePreloader; // it must be here, otherwise will get error before initialization in round onPlay
|
|
|
|
const video = createVideo();
|
|
video.classList.add('media-video');
|
|
video.muted = true;
|
|
if(doc.type === 'round') {
|
|
const divRound = document.createElement('div');
|
|
divRound.classList.add('media-round', 'z-depth-1');
|
|
divRound.dataset.mid = '' + message.mid;
|
|
divRound.dataset.peerId = '' + message.peerId;
|
|
(divRound as any).message = message;
|
|
|
|
const size = mediaSizes.active.round;
|
|
const halfSize = size.width / 2;
|
|
const strokeWidth = 3.5;
|
|
const radius = halfSize - (strokeWidth * 2);
|
|
divRound.innerHTML = `<svg class="progress-ring" width="${size.width}" height="${size.width}" style="transform: rotate(-90deg);">
|
|
<circle class="progress-ring__circle" stroke="white" stroke-opacity="0.3" stroke-width="${strokeWidth}" cx="${halfSize}" cy="${halfSize}" r="${radius}" fill="transparent"/>
|
|
</svg>`;
|
|
|
|
const circle = divRound.firstElementChild.firstElementChild as SVGCircleElement;
|
|
if(!roundVideoCircumference) {
|
|
roundVideoCircumference = 2 * Math.PI * radius;
|
|
}
|
|
circle.style.strokeDasharray = roundVideoCircumference + ' ' + roundVideoCircumference;
|
|
circle.style.strokeDashoffset = '' + roundVideoCircumference;
|
|
|
|
spanTime.classList.add('tgico');
|
|
|
|
const isUnread = message.pFlags.media_unread;
|
|
if(isUnread) {
|
|
divRound.classList.add('is-unread');
|
|
}
|
|
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = canvas.height = doc.w/* * window.devicePixelRatio */;
|
|
|
|
divRound.prepend(canvas, spanTime);
|
|
divRound.append(video);
|
|
container.append(divRound);
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
/* ctx.beginPath();
|
|
ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2);
|
|
ctx.clip(); */
|
|
|
|
const onLoad = () => {
|
|
const message: Message.message = (divRound as any).message;
|
|
const globalVideo = appMediaPlaybackController.addMedia(message, !noAutoDownload) as HTMLVideoElement;
|
|
const clear = () => {
|
|
(appImManager.chat.setPeerPromise || Promise.resolve()).finally(() => {
|
|
if(isInDOM(globalVideo)) {
|
|
return;
|
|
}
|
|
|
|
globalVideo.removeEventListener('play', onPlay);
|
|
globalVideo.removeEventListener('timeupdate', throttledTimeUpdate);
|
|
globalVideo.removeEventListener('pause', onPaused);
|
|
globalVideo.removeEventListener('ended', onEnded);
|
|
});
|
|
};
|
|
|
|
const onFrame = () => {
|
|
ctx.drawImage(globalVideo, 0, 0);
|
|
|
|
const offset = roundVideoCircumference - globalVideo.currentTime / globalVideo.duration * roundVideoCircumference;
|
|
circle.style.strokeDashoffset = '' + offset;
|
|
|
|
return !globalVideo.paused;
|
|
};
|
|
|
|
const onTimeUpdate = () => {
|
|
if(!globalVideo.duration) {
|
|
return;
|
|
}
|
|
|
|
if(!isInDOM(globalVideo)) {
|
|
clear();
|
|
return;
|
|
}
|
|
|
|
if(globalVideo.paused) {
|
|
onFrame();
|
|
}
|
|
|
|
spanTime.innerText = toHHMMSS(globalVideo.duration - globalVideo.currentTime, false);
|
|
};
|
|
|
|
const throttledTimeUpdate = throttleWithRaf(onTimeUpdate);
|
|
|
|
const onPlay = () => {
|
|
video.classList.add('hide');
|
|
divRound.classList.remove('is-paused');
|
|
animateSingle(onFrame, canvas);
|
|
|
|
if(preloader && preloader.preloader && preloader.preloader.classList.contains('manual')) {
|
|
preloader.onClick();
|
|
}
|
|
};
|
|
|
|
const onPaused = () => {
|
|
if(!isInDOM(globalVideo)) {
|
|
clear();
|
|
return;
|
|
}
|
|
|
|
divRound.classList.add('is-paused');
|
|
};
|
|
|
|
const onEnded = () => {
|
|
video.classList.remove('hide');
|
|
divRound.classList.add('is-paused');
|
|
|
|
video.currentTime = 0;
|
|
spanTime.innerText = toHHMMSS(globalVideo.duration, false);
|
|
|
|
if(globalVideo.currentTime) {
|
|
globalVideo.currentTime = 0;
|
|
}
|
|
};
|
|
|
|
globalVideo.addEventListener('play', onPlay);
|
|
globalVideo.addEventListener('timeupdate', throttledTimeUpdate);
|
|
globalVideo.addEventListener('pause', onPaused);
|
|
globalVideo.addEventListener('ended', onEnded);
|
|
|
|
attachClickEvent(canvas, (e) => {
|
|
cancelEvent(e);
|
|
|
|
// ! костыль
|
|
if(preloader && !preloader.detached) {
|
|
preloader.onClick();
|
|
}
|
|
|
|
// ! can't use it here. on Safari iOS video won't start.
|
|
/* if(globalVideo.readyState < 2) {
|
|
return;
|
|
} */
|
|
|
|
if(globalVideo.paused) {
|
|
const hadSearchContext = !!searchContext;
|
|
if(appMediaPlaybackController.setSearchContext(searchContext || {
|
|
peerId: NULL_PEER_ID,
|
|
inputFilter: {_: 'inputMessagesFilterEmpty'},
|
|
useSearch: false
|
|
})) {
|
|
const [prev, next] = !hadSearchContext ? [] : findMediaTargets(divRound, message.mid/* , searchContext.useSearch */);
|
|
appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid}, prev, next);
|
|
}
|
|
|
|
globalVideo.play();
|
|
} else {
|
|
globalVideo.pause();
|
|
}
|
|
});
|
|
|
|
if(globalVideo.paused) {
|
|
if(globalVideo.duration && globalVideo.currentTime !== globalVideo.duration && globalVideo.currentTime > 0) {
|
|
onFrame();
|
|
onTimeUpdate();
|
|
video.classList.add('hide');
|
|
} else {
|
|
onPaused();
|
|
}
|
|
} else {
|
|
onPlay();
|
|
}
|
|
};
|
|
|
|
if(message.pFlags.is_outgoing) {
|
|
(divRound as any).onLoad = onLoad;
|
|
divRound.dataset.isOutgoing = '1';
|
|
} else {
|
|
onLoad();
|
|
}
|
|
} else {
|
|
video.autoplay = true; // для safari
|
|
}
|
|
|
|
let photoRes: Awaited<ReturnType<typeof wrapPhoto>>;
|
|
if(message) {
|
|
photoRes = await wrapPhoto({
|
|
photo: doc,
|
|
message,
|
|
container,
|
|
boxWidth,
|
|
boxHeight,
|
|
withTail,
|
|
isOut,
|
|
lazyLoadQueue,
|
|
middleware,
|
|
withoutPreloader: true,
|
|
loadPromises,
|
|
autoDownloadSize: autoDownload?.photo,
|
|
size,
|
|
managers
|
|
});
|
|
|
|
res.thumb = photoRes;
|
|
|
|
if((!canAutoplay && doc.type !== 'gif') || onlyPreview) {
|
|
res.loadPromise = photoRes.loadPromises.full;
|
|
return res;
|
|
}
|
|
|
|
if(withTail) {
|
|
const foreignObject = (photoRes.images.thumb || photoRes.images.full).parentElement;
|
|
video.width = +foreignObject.getAttributeNS(null, 'width');
|
|
video.height = +foreignObject.getAttributeNS(null, 'height');
|
|
foreignObject.append(video);
|
|
}
|
|
} else { // * gifs masonry
|
|
// const gotThumb = managers.appDocsManager.getThumb(doc, false);
|
|
// if(gotThumb) {
|
|
// gotThumb.promise.then(() => {
|
|
// video.poster = gotThumb.cacheContext.url;
|
|
// });
|
|
// }
|
|
}
|
|
|
|
if(!video.parentElement && container) {
|
|
(photoRes?.aspecter || container).append(video);
|
|
}
|
|
|
|
let cacheContext: ThumbCache;
|
|
const getCacheContext = async() => {
|
|
return cacheContext = await managers.thumbsStorage.getCacheContext(doc);
|
|
};
|
|
|
|
await getCacheContext();
|
|
|
|
const uploadFileName = message?.uploadingFileName;
|
|
if(uploadFileName) { // means upload
|
|
preloader = new ProgressivePreloader({
|
|
attachMethod: 'prepend',
|
|
isUpload: true
|
|
});
|
|
preloader.attachPromise(appDownloadManager.getUpload(uploadFileName));
|
|
preloader.attach(container, false);
|
|
noAutoDownload = undefined;
|
|
} else if(!cacheContext.downloaded && !doc.supportsStreaming && !withoutPreloader) {
|
|
preloader = new ProgressivePreloader({
|
|
attachMethod: 'prepend'
|
|
});
|
|
} else if(doc.supportsStreaming) {
|
|
preloader = new ProgressivePreloader({
|
|
cancelable: false,
|
|
attachMethod: 'prepend'
|
|
});
|
|
}
|
|
|
|
const renderDeferred = deferredPromise<void>();
|
|
video.addEventListener('error', (e) => {
|
|
if(video.error.code !== 4) {
|
|
console.error("Error " + video.error.code + "; details: " + video.error.message);
|
|
}
|
|
|
|
if(preloader && !uploadFileName) {
|
|
preloader.detach();
|
|
}
|
|
|
|
if(!renderDeferred.isFulfilled) {
|
|
renderDeferred.resolve();
|
|
}
|
|
}, {once: true});
|
|
|
|
onMediaLoad(video).then(() => {
|
|
if(group) {
|
|
animationIntersector.addAnimation(video, group);
|
|
}
|
|
|
|
if(preloader && !uploadFileName) {
|
|
preloader.detach();
|
|
}
|
|
|
|
renderDeferred.resolve();
|
|
});
|
|
|
|
if(doc.type === 'video') {
|
|
const onTimeUpdate = () => {
|
|
if(!video.videoWidth) {
|
|
return;
|
|
}
|
|
|
|
spanTime.innerText = toHHMMSS(video.duration - video.currentTime, false);
|
|
};
|
|
|
|
const throttledTimeUpdate = throttleWithRaf(onTimeUpdate);
|
|
|
|
video.addEventListener('timeupdate', throttledTimeUpdate);
|
|
|
|
if(spanPlay) {
|
|
video.addEventListener('timeupdate', () => {
|
|
sequentialDom.mutateElement(spanPlay, () => {
|
|
spanPlay.remove();
|
|
});
|
|
}, {once: true});
|
|
}
|
|
}
|
|
|
|
video.muted = true;
|
|
video.loop = true;
|
|
//video.play();
|
|
video.autoplay = true;
|
|
|
|
let loadPhotoThumbFunc = noAutoDownload && photoRes?.preloader?.loadFunc;
|
|
const load = async() => {
|
|
if(preloader && noAutoDownload && !withoutPreloader) {
|
|
preloader.construct();
|
|
preloader.setManual();
|
|
}
|
|
|
|
await getCacheContext();
|
|
let loadPromise: Promise<any> = Promise.resolve();
|
|
if((preloader && !uploadFileName) || withoutPreloader) {
|
|
if(!cacheContext.downloaded && !doc.supportsStreaming) {
|
|
const promise = loadPromise = managers.apiFileManager.downloadMediaURL({media: doc, queueId: lazyLoadQueue?.queueId, onlyCache: noAutoDownload});
|
|
if(preloader) {
|
|
preloader.attach(container, false, promise);
|
|
}
|
|
} else if(doc.supportsStreaming) {
|
|
if(noAutoDownload) {
|
|
loadPromise = Promise.reject();
|
|
} else if(!cacheContext.downloaded && preloader) { // * check for uploading video
|
|
preloader.attach(container, false, null);
|
|
video.addEventListener(IS_SAFARI ? 'timeupdate' : 'canplay', () => {
|
|
preloader.detach();
|
|
}, {once: true});
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!noAutoDownload && loadPhotoThumbFunc) {
|
|
loadPhotoThumbFunc();
|
|
loadPhotoThumbFunc = null;
|
|
}
|
|
|
|
noAutoDownload = undefined;
|
|
|
|
loadPromise.then(async() => {
|
|
if(middleware && !middleware()) {
|
|
renderDeferred.resolve();
|
|
return;
|
|
}
|
|
|
|
if(doc.type === 'round') {
|
|
appMediaPlaybackController.resolveWaitingForLoadMedia(message.peerId, message.mid, message.pFlags.is_scheduled);
|
|
}
|
|
|
|
await getCacheContext();
|
|
renderImageFromUrl(video, cacheContext.url);
|
|
}, () => {});
|
|
|
|
return {download: loadPromise, render: renderDeferred};
|
|
};
|
|
|
|
if(preloader && !uploadFileName) {
|
|
preloader.setDownloadFunction(load);
|
|
}
|
|
|
|
/* if(doc.size >= 20e6 && !doc.downloaded) {
|
|
let downloadDiv = document.createElement('div');
|
|
downloadDiv.classList.add('download');
|
|
|
|
let span = document.createElement('span');
|
|
span.classList.add('btn-circle', 'tgico-download');
|
|
downloadDiv.append(span);
|
|
|
|
downloadDiv.addEventListener('click', () => {
|
|
downloadDiv.remove();
|
|
loadVideo();
|
|
});
|
|
|
|
container.prepend(downloadDiv);
|
|
|
|
return;
|
|
} */
|
|
|
|
if(doc.type === 'gif' && !canAutoplay) {
|
|
attachClickEvent(container, (e) => {
|
|
cancelEvent(e);
|
|
spanPlay.remove();
|
|
load();
|
|
}, {capture: true, once: true});
|
|
} else {
|
|
res.loadPromise = !lazyLoadQueue ?
|
|
(await load()).render :
|
|
(lazyLoadQueue.push({div: container, load: () => load().then(({render}) => render)}), Promise.resolve());
|
|
}
|
|
|
|
return res;
|
|
}
|