morethanwords
4 years ago
62 changed files with 4050 additions and 814 deletions
@ -0,0 +1,526 @@ |
|||||||
|
import { AppImManager } from "../lib/appManagers/appImManager"; |
||||||
|
import { AppMessagesManager } from "../lib/appManagers/appMessagesManager"; |
||||||
|
import { horizontalMenu, renderImageFromUrl } from "./misc"; |
||||||
|
import lottieLoader from "../lib/lottieLoader"; |
||||||
|
//import Scrollable from "./scrollable";
|
||||||
|
import Scrollable from "./scrollable_new"; |
||||||
|
import { findUpTag, whichChild, calcImageInBox } from "../lib/utils"; |
||||||
|
import { RichTextProcessor } from "../lib/richtextprocessor"; |
||||||
|
import appStickersManager, { MTStickerSet } from "../lib/appManagers/appStickersManager"; |
||||||
|
//import apiManager from '../lib/mtproto/apiManager';
|
||||||
|
import apiManager from '../lib/mtproto/mtprotoworker'; |
||||||
|
//import CryptoWorker from '../lib/crypto/cryptoworker';
|
||||||
|
import LazyLoadQueue from "./lazyLoadQueue"; |
||||||
|
import { wrapSticker } from "./wrappers"; |
||||||
|
import appDocsManager from "../lib/appManagers/appDocsManager"; |
||||||
|
import ProgressivePreloader from "./preloader"; |
||||||
|
import Config from "../lib/config"; |
||||||
|
import { MTDocument } from "../types"; |
||||||
|
import animationIntersector from "./animationIntersector"; |
||||||
|
import appSidebarRight from "../lib/appManagers/appSidebarRight"; |
||||||
|
|
||||||
|
export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown'; |
||||||
|
|
||||||
|
const initEmoticonsDropdown = (pageEl: HTMLDivElement, |
||||||
|
appImManager: AppImManager, appMessagesManager: AppMessagesManager, |
||||||
|
messageInput: HTMLDivElement, toggleEl: HTMLButtonElement, btnSend: HTMLButtonElement) => { |
||||||
|
let dropdown = pageEl.querySelector('.emoji-dropdown') as HTMLDivElement; |
||||||
|
|
||||||
|
dropdown.style.display = ''; |
||||||
|
void dropdown.offsetLeft; // reflow
|
||||||
|
dropdown.classList.add('active'); // need
|
||||||
|
|
||||||
|
let lazyLoadQueue = new LazyLoadQueue(); |
||||||
|
|
||||||
|
const searchButton = dropdown.querySelector('.emoji-tabs-search'); |
||||||
|
searchButton.addEventListener('click', () => { |
||||||
|
appSidebarRight.stickersTab.init(); |
||||||
|
}); |
||||||
|
|
||||||
|
let container = pageEl.querySelector('.emoji-container .tabs-container') as HTMLDivElement; |
||||||
|
let tabs = pageEl.querySelector('.emoji-dropdown .emoji-tabs') as HTMLUListElement; |
||||||
|
let tabID = -1; |
||||||
|
horizontalMenu(tabs, container, (id) => { |
||||||
|
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP); |
||||||
|
|
||||||
|
tabID = id; |
||||||
|
}, () => { |
||||||
|
if(tabID == 1 && stickersInit) { |
||||||
|
stickersInit(); |
||||||
|
} else if(tabID == 2 && gifsInit) { |
||||||
|
gifsInit(); |
||||||
|
} |
||||||
|
|
||||||
|
animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP); |
||||||
|
}); |
||||||
|
|
||||||
|
(tabs.firstElementChild.children[1] as HTMLLIElement).click(); // set emoji tab
|
||||||
|
|
||||||
|
let emoticonsMenuOnClick = (menu: HTMLUListElement, heights: number[], scroll: Scrollable, menuScroll?: Scrollable) => { |
||||||
|
menu.addEventListener('click', function(e) { |
||||||
|
let target = e.target as HTMLLIElement; |
||||||
|
target = findUpTag(target, 'LI'); |
||||||
|
|
||||||
|
let index = whichChild(target); |
||||||
|
let y = heights[index - 1/* 2 */] || 0; // 10 == padding .scrollable
|
||||||
|
|
||||||
|
/* if(menuScroll) { |
||||||
|
menuScroll.container.scrollLeft = target.scrollWidth * index; |
||||||
|
} |
||||||
|
console.log('emoticonsMenuOnClick', menu.getBoundingClientRect(), target.getBoundingClientRect()); |
||||||
|
*/ |
||||||
|
/* scroll.onAddedBottom = () => { // привет, костыль, давно не виделись! |
||||||
|
scroll.container.scrollTop = y; |
||||||
|
scroll.onAddedBottom = () => {}; |
||||||
|
}; */ |
||||||
|
scroll.container.scrollTop = y; |
||||||
|
|
||||||
|
setTimeout(() => { |
||||||
|
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP); |
||||||
|
}, 100); |
||||||
|
|
||||||
|
/* window.requestAnimationFrame(() => { |
||||||
|
window.requestAnimationFrame(() => { |
||||||
|
lottieLoader.checkAnimations(true, EMOTICONSSTICKERGROUP); |
||||||
|
}); |
||||||
|
}); */ |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
let emoticonsContentOnScroll = (menu: HTMLUListElement, heights: number[], prevCategoryIndex: number, scroll: HTMLDivElement, menuScroll?: Scrollable) => { |
||||||
|
let y = scroll.scrollTop; |
||||||
|
|
||||||
|
//console.log(heights, y);
|
||||||
|
|
||||||
|
for(let i = 0; i < heights.length; ++i) { |
||||||
|
let height = heights[i]; |
||||||
|
if(y < height) { |
||||||
|
menu.children[prevCategoryIndex].classList.remove('active'); |
||||||
|
prevCategoryIndex = i/* + 1 */; |
||||||
|
menu.children[prevCategoryIndex].classList.add('active'); |
||||||
|
|
||||||
|
if(menuScroll) { |
||||||
|
if(i < heights.length - 4) { |
||||||
|
menuScroll.container.scrollLeft = (i - 3) * 47; |
||||||
|
} else { |
||||||
|
menuScroll.container.scrollLeft = i * 47; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return prevCategoryIndex; |
||||||
|
}; |
||||||
|
|
||||||
|
{ |
||||||
|
const categories = ["Smileys & Emotion", "Animals & Nature", "Food & Drink", "Travel & Places", "Activities", "Objects", /* "Symbols", */"Flags", "Skin Tones"]; |
||||||
|
let divs: { |
||||||
|
[category: string]: HTMLDivElement |
||||||
|
} = {}; |
||||||
|
|
||||||
|
let sorted: { |
||||||
|
[category: string]: string[] |
||||||
|
} = {}; |
||||||
|
|
||||||
|
for(let emoji in Config.Emoji) { |
||||||
|
let details = Config.Emoji[emoji]; |
||||||
|
let i = '' + details; |
||||||
|
let category = categories[+i[0] - 1]; |
||||||
|
if(!category) continue; // maybe it's skin tones
|
||||||
|
|
||||||
|
if(!sorted[category]) sorted[category] = []; |
||||||
|
sorted[category][+i.slice(1) || 0] = emoji; |
||||||
|
} |
||||||
|
|
||||||
|
console.log('emoticons sorted:', sorted); |
||||||
|
|
||||||
|
//Object.keys(sorted).forEach(c => sorted[c].sort((a, b) => a - b));
|
||||||
|
|
||||||
|
categories.pop(); |
||||||
|
delete sorted["Skin Tones"]; |
||||||
|
|
||||||
|
//console.time('emojiParse');
|
||||||
|
for(let category in sorted) { |
||||||
|
let div = document.createElement('div'); |
||||||
|
div.classList.add('emoji-category'); |
||||||
|
|
||||||
|
let titleDiv = document.createElement('div'); |
||||||
|
titleDiv.classList.add('category-title'); |
||||||
|
titleDiv.innerText = category; |
||||||
|
|
||||||
|
let itemsDiv = document.createElement('div'); |
||||||
|
itemsDiv.classList.add('category-items'); |
||||||
|
|
||||||
|
div.append(titleDiv, itemsDiv); |
||||||
|
|
||||||
|
let emojis = sorted[category]; |
||||||
|
emojis.forEach(emoji => { |
||||||
|
//let emoji = details.unified;
|
||||||
|
//let emoji = (details.unified as string).split('-')
|
||||||
|
//.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
|
||||||
|
|
||||||
|
let spanEmoji = document.createElement('span'); |
||||||
|
let kek = RichTextProcessor.wrapRichText(emoji); |
||||||
|
|
||||||
|
if(!kek.includes('emoji')) { |
||||||
|
console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji)); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
//console.log(kek);
|
||||||
|
|
||||||
|
spanEmoji.innerHTML = kek; |
||||||
|
|
||||||
|
//spanEmoji = spanEmoji.firstElementChild as HTMLSpanElement;
|
||||||
|
//spanEmoji.setAttribute('emoji', emoji);
|
||||||
|
itemsDiv.appendChild(spanEmoji); |
||||||
|
}); |
||||||
|
|
||||||
|
divs[category] = div; |
||||||
|
} |
||||||
|
//console.timeEnd('emojiParse');
|
||||||
|
|
||||||
|
let contentEmojiDiv = document.getElementById('content-emoji') as HTMLDivElement; |
||||||
|
let heights: number[] = [0]; |
||||||
|
|
||||||
|
let prevCategoryIndex = 1; |
||||||
|
let menu = contentEmojiDiv.previousElementSibling.firstElementChild as HTMLUListElement; |
||||||
|
let emojiScroll = new Scrollable(contentEmojiDiv, 'y', 'EMOJI', null); |
||||||
|
emojiScroll.container.addEventListener('scroll', (e) => { |
||||||
|
prevCategoryIndex = emoticonsContentOnScroll(menu, heights, prevCategoryIndex, emojiScroll.container); |
||||||
|
}); |
||||||
|
//emojiScroll.setVirtualContainer(emojiScroll.container);
|
||||||
|
|
||||||
|
categories.map(category => { |
||||||
|
let div = divs[category]; |
||||||
|
|
||||||
|
if(!div) { |
||||||
|
console.error('no div by category:', category); |
||||||
|
} |
||||||
|
|
||||||
|
emojiScroll.append(div); |
||||||
|
return div; |
||||||
|
}).forEach(div => { |
||||||
|
//console.log('emoji heights push: ', (heights[heights.length - 1] || 0) + div.scrollHeight, div, div.scrollHeight);
|
||||||
|
heights.push((heights[heights.length - 1] || 0) + div.scrollHeight); |
||||||
|
}); |
||||||
|
|
||||||
|
contentEmojiDiv.addEventListener('click', function(e) { |
||||||
|
let target = e.target as any; |
||||||
|
//if(target.tagName != 'SPAN') return;
|
||||||
|
|
||||||
|
if(target.tagName == 'SPAN' && !target.classList.contains('emoji')) { |
||||||
|
target = target.firstElementChild; |
||||||
|
} else if(target.tagName == 'DIV') return; |
||||||
|
|
||||||
|
//console.log('contentEmoji div', target);
|
||||||
|
|
||||||
|
/* if(!target.classList.contains('emoji')) { |
||||||
|
target = target.parentElement as HTMLSpanElement; |
||||||
|
|
||||||
|
if(!target.classList.contains('emoji')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
} */ |
||||||
|
|
||||||
|
//messageInput.innerHTML += target.innerHTML;
|
||||||
|
messageInput.innerHTML += target.outerHTML; |
||||||
|
|
||||||
|
btnSend.classList.add('tgico-send'); |
||||||
|
btnSend.classList.remove('tgico-microphone2'); |
||||||
|
}); |
||||||
|
|
||||||
|
emoticonsMenuOnClick(menu, heights, emojiScroll); |
||||||
|
} |
||||||
|
|
||||||
|
let onMediaClick = (e: MouseEvent) => { |
||||||
|
let target = e.target as HTMLDivElement; |
||||||
|
target = findUpTag(target, 'DIV'); |
||||||
|
|
||||||
|
let fileID = target.dataset.docID; |
||||||
|
if(appImManager.chatInputC.sendMessageWithDocument(fileID)) { |
||||||
|
dropdown.classList.remove('active'); |
||||||
|
toggleEl.classList.remove('active'); |
||||||
|
} else { |
||||||
|
console.warn('got no doc by id:', fileID); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
let stickersInit = () => { |
||||||
|
let contentStickersDiv = document.getElementById('content-stickers') as HTMLDivElement; |
||||||
|
//let stickersDiv = contentStickersDiv.querySelector('.os-content') as HTMLDivElement;
|
||||||
|
|
||||||
|
let menuWrapper = contentStickersDiv.previousElementSibling as HTMLDivElement; |
||||||
|
let menu = menuWrapper.firstElementChild.firstElementChild as HTMLUListElement; |
||||||
|
|
||||||
|
let menuScroll = new Scrollable(menuWrapper, 'x'); |
||||||
|
|
||||||
|
let stickersDiv = document.createElement('div'); |
||||||
|
stickersDiv.classList.add('stickers-categories'); |
||||||
|
contentStickersDiv.append(stickersDiv); |
||||||
|
|
||||||
|
/* stickersDiv.addEventListener('mouseover', (e) => { |
||||||
|
let target = e.target as HTMLElement; |
||||||
|
|
||||||
|
if(target.tagName == 'CANVAS') { // turn on sticker
|
||||||
|
let animation = lottieLoader.getAnimation(target.parentElement, EMOTICONSSTICKERGROUP); |
||||||
|
|
||||||
|
if(animation) { |
||||||
|
// @ts-ignore
|
||||||
|
if(animation.currentFrame == animation.totalFrames - 1) { |
||||||
|
animation.goToAndPlay(0, true); |
||||||
|
} else { |
||||||
|
animation.play(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}); */ |
||||||
|
|
||||||
|
stickersDiv.addEventListener('click', onMediaClick); |
||||||
|
|
||||||
|
let heights: number[] = []; |
||||||
|
|
||||||
|
let heightRAF = 0; |
||||||
|
let categoryPush = (categoryDiv: HTMLDivElement, categoryTitle: string, docs: MTDocument[], prepend?: boolean) => { |
||||||
|
//if((docs.length % 5) != 0) categoryDiv.classList.add('not-full');
|
||||||
|
|
||||||
|
let itemsDiv = document.createElement('div'); |
||||||
|
itemsDiv.classList.add('category-items'); |
||||||
|
|
||||||
|
let titleDiv = document.createElement('div'); |
||||||
|
titleDiv.classList.add('category-title'); |
||||||
|
titleDiv.innerText = categoryTitle; |
||||||
|
|
||||||
|
categoryDiv.append(titleDiv, itemsDiv); |
||||||
|
|
||||||
|
docs.forEach(doc => { |
||||||
|
let div = document.createElement('div'); |
||||||
|
wrapSticker({ |
||||||
|
doc, |
||||||
|
div, |
||||||
|
lazyLoadQueue, |
||||||
|
group: EMOTICONSSTICKERGROUP, |
||||||
|
onlyThumb: true |
||||||
|
}); |
||||||
|
|
||||||
|
itemsDiv.append(div); |
||||||
|
}); |
||||||
|
|
||||||
|
if(prepend) stickersScroll.prepend(categoryDiv); |
||||||
|
else stickersScroll.append(categoryDiv); |
||||||
|
|
||||||
|
/* let scrollHeight = categoryDiv.scrollHeight; |
||||||
|
let prevHeight = heights[heights.length - 1] || 0; |
||||||
|
//console.log('scrollHeight', scrollHeight, categoryDiv, stickersDiv.childElementCount);
|
||||||
|
if(prepend && heights.length) {// all stickers loaded faster than recent
|
||||||
|
heights.forEach((h, i) => heights[i] += scrollHeight); |
||||||
|
|
||||||
|
return heights.unshift(scrollHeight) - 1; |
||||||
|
} */ |
||||||
|
|
||||||
|
if(heightRAF) window.cancelAnimationFrame(heightRAF); |
||||||
|
heightRAF = window.requestAnimationFrame(() => { |
||||||
|
heightRAF = 0; |
||||||
|
|
||||||
|
let paddingTop = parseInt(window.getComputedStyle(stickersScroll.container).getPropertyValue('padding-top')) || 0; |
||||||
|
|
||||||
|
heights.length = 0; |
||||||
|
/* let concated = stickersScroll.hiddenElements.up.concat(stickersScroll.visibleElements, stickersScroll.hiddenElements.down); |
||||||
|
concated.forEach((el, i) => { |
||||||
|
heights[i] = (heights[i - 1] || 0) + el.height + (i == 0 ? paddingTop : 0); |
||||||
|
}); */ |
||||||
|
let concated = Array.from(stickersScroll.splitUp.children) as HTMLElement[]; |
||||||
|
concated.forEach((el, i) => { |
||||||
|
heights[i] = (heights[i - 1] || 0) + el.scrollHeight + (i == 0 ? paddingTop : 0); |
||||||
|
}); |
||||||
|
|
||||||
|
//console.log('stickers concated', concated, heights);
|
||||||
|
}); |
||||||
|
|
||||||
|
/* Array.from(stickersDiv.children).forEach((div, i) => { |
||||||
|
heights[i] = (heights[i - 1] || 0) + div.scrollHeight; |
||||||
|
}); */ |
||||||
|
|
||||||
|
//stickersScroll.onScroll();
|
||||||
|
|
||||||
|
//return heights.push(prevHeight + scrollHeight) - 1;
|
||||||
|
}; |
||||||
|
|
||||||
|
let prevCategoryIndex = 0; |
||||||
|
let stickersScroll = new Scrollable(contentStickersDiv, 'y', 'STICKERS', undefined, undefined, 2); |
||||||
|
stickersScroll.container.addEventListener('scroll', (e) => { |
||||||
|
animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP); |
||||||
|
|
||||||
|
prevCategoryIndex = emoticonsContentOnScroll(menu, heights, prevCategoryIndex, stickersScroll.container, menuScroll); |
||||||
|
}); |
||||||
|
stickersScroll.setVirtualContainer(stickersDiv); |
||||||
|
|
||||||
|
emoticonsMenuOnClick(menu, heights, stickersScroll, menuScroll); |
||||||
|
|
||||||
|
stickersInit = null; |
||||||
|
|
||||||
|
Promise.all([ |
||||||
|
appStickersManager.getRecentStickers().then(stickers => { |
||||||
|
let categoryDiv = document.createElement('div'); |
||||||
|
categoryDiv.classList.add('sticker-category'); |
||||||
|
|
||||||
|
//stickersScroll.prepend(categoryDiv);
|
||||||
|
|
||||||
|
categoryPush(categoryDiv, 'Recent', stickers.stickers, true); |
||||||
|
}), |
||||||
|
|
||||||
|
apiManager.invokeApi('messages.getAllStickers', {hash: 0}).then(async(res) => { |
||||||
|
let stickers: { |
||||||
|
_: 'messages.allStickers', |
||||||
|
hash: number, |
||||||
|
sets: Array<MTStickerSet> |
||||||
|
} = res as any; |
||||||
|
|
||||||
|
for(let set of stickers.sets) { |
||||||
|
let categoryDiv = document.createElement('div'); |
||||||
|
categoryDiv.classList.add('sticker-category'); |
||||||
|
|
||||||
|
let li = document.createElement('li'); |
||||||
|
li.classList.add('btn-icon'); |
||||||
|
|
||||||
|
menu.append(li); |
||||||
|
|
||||||
|
//stickersScroll.append(categoryDiv);
|
||||||
|
|
||||||
|
let stickerSet = await appStickersManager.getStickerSet(set); |
||||||
|
|
||||||
|
//console.log('got stickerSet', stickerSet, li);
|
||||||
|
|
||||||
|
if(stickerSet.set.thumb) { |
||||||
|
appStickersManager.getStickerSetThumb(stickerSet.set).then((blob) => { |
||||||
|
//console.log('setting thumb', stickerSet, blob);
|
||||||
|
if(stickerSet.set.pFlags.animated) { // means animated
|
||||||
|
const reader = new FileReader(); |
||||||
|
|
||||||
|
reader.addEventListener('loadend', async(e) => { |
||||||
|
// @ts-ignore
|
||||||
|
const text = e.srcElement.result; |
||||||
|
let json = await apiManager.gzipUncompress<string>(text, true); |
||||||
|
|
||||||
|
let animation = await lottieLoader.loadAnimationWorker({ |
||||||
|
container: li, |
||||||
|
loop: true, |
||||||
|
autoplay: false, |
||||||
|
animationData: JSON.parse(json), |
||||||
|
width: 40, |
||||||
|
height: 40 |
||||||
|
}, EMOTICONSSTICKERGROUP); |
||||||
|
}); |
||||||
|
|
||||||
|
reader.readAsArrayBuffer(blob); |
||||||
|
} else { |
||||||
|
let image = new Image(); |
||||||
|
renderImageFromUrl(image, URL.createObjectURL(blob)); |
||||||
|
|
||||||
|
li.append(image); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else { // as thumb will be used first sticker
|
||||||
|
wrapSticker({ |
||||||
|
doc: stickerSet.documents[0], |
||||||
|
div: li as any, |
||||||
|
group: EMOTICONSSTICKERGROUP |
||||||
|
}); // kostil
|
||||||
|
} |
||||||
|
|
||||||
|
categoryPush(categoryDiv, stickerSet.set.title, stickerSet.documents, false); |
||||||
|
} |
||||||
|
}) |
||||||
|
]); |
||||||
|
}; |
||||||
|
|
||||||
|
let gifsInit = () => { |
||||||
|
let contentDiv = document.getElementById('content-gifs') as HTMLDivElement; |
||||||
|
let masonry = contentDiv.firstElementChild as HTMLDivElement; |
||||||
|
|
||||||
|
masonry.addEventListener('click', onMediaClick); |
||||||
|
|
||||||
|
let scroll = new Scrollable(contentDiv, 'y', 'GIFS', null); |
||||||
|
|
||||||
|
let width = 400; |
||||||
|
let maxSingleWidth = width - 100; |
||||||
|
let height = 100; |
||||||
|
|
||||||
|
apiManager.invokeApi('messages.getSavedGifs', {hash: 0}).then((_res) => { |
||||||
|
let res = _res as { |
||||||
|
_: 'messages.savedGifs', |
||||||
|
gifs: MTDocument[], |
||||||
|
hash: number |
||||||
|
}; |
||||||
|
console.log('getSavedGifs res:', res); |
||||||
|
|
||||||
|
let line: MTDocument[] = []; |
||||||
|
|
||||||
|
let wastedWidth = 0; |
||||||
|
|
||||||
|
res.gifs.forEach((gif, idx) => { |
||||||
|
res.gifs[idx] = appDocsManager.saveDoc(gif); |
||||||
|
}); |
||||||
|
|
||||||
|
for(let i = 0, length = res.gifs.length; i < length;) { |
||||||
|
let gif = res.gifs[i]; |
||||||
|
|
||||||
|
let gifWidth = gif.w; |
||||||
|
let gifHeight = gif.h; |
||||||
|
if(gifHeight < height) { |
||||||
|
gifWidth = height / gifHeight * gifWidth; |
||||||
|
gifHeight = height; |
||||||
|
} |
||||||
|
|
||||||
|
let willUseWidth = Math.min(maxSingleWidth, width - wastedWidth, gifWidth); |
||||||
|
let {w, h} = calcImageInBox(gifWidth, gifHeight, willUseWidth, height); |
||||||
|
|
||||||
|
/* wastedWidth += w; |
||||||
|
|
||||||
|
if(wastedWidth == width || h < height) { |
||||||
|
wastedWidth = 0; |
||||||
|
console.log('completed line', i, line); |
||||||
|
line = []; |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
line.push(gif); */ |
||||||
|
++i; |
||||||
|
|
||||||
|
console.log('gif:', gif, w, h); |
||||||
|
|
||||||
|
let div = document.createElement('div'); |
||||||
|
div.style.width = w + 'px'; |
||||||
|
//div.style.height = h + 'px';
|
||||||
|
div.dataset.docID = gif.id; |
||||||
|
|
||||||
|
masonry.append(div); |
||||||
|
|
||||||
|
let preloader = new ProgressivePreloader(div); |
||||||
|
lazyLoadQueue.push({ |
||||||
|
div, |
||||||
|
load: () => { |
||||||
|
let promise = appDocsManager.downloadDoc(gif); |
||||||
|
preloader.attach(div, true, promise); |
||||||
|
|
||||||
|
promise.then(blob => { |
||||||
|
preloader.detach(); |
||||||
|
|
||||||
|
div.innerHTML = `<video autoplay="true" muted="true" loop="true" src="${gif.url}" type="video/mp4"></video>`; |
||||||
|
}); |
||||||
|
|
||||||
|
return promise; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
gifsInit = undefined; |
||||||
|
}; |
||||||
|
|
||||||
|
return {dropdown, lazyLoadQueue}; |
||||||
|
}; |
||||||
|
|
||||||
|
export default initEmoticonsDropdown; |
@ -0,0 +1,134 @@ |
|||||||
|
import { PopupElement } from "./popup"; |
||||||
|
import Scrollable from "./scrollable_new"; |
||||||
|
import appMessagesManager from "../lib/appManagers/appMessagesManager"; |
||||||
|
import { $rootScope } from "../lib/utils"; |
||||||
|
import { Poll } from "../lib/appManagers/appPollsManager"; |
||||||
|
import { nextRandomInt, bigint } from "../lib/bin_utils"; |
||||||
|
import { toast } from "./misc"; |
||||||
|
|
||||||
|
const InputField = (placeholder: string, label: string, name: string) => { |
||||||
|
const div = document.createElement('div'); |
||||||
|
div.classList.add('input-field'); |
||||||
|
|
||||||
|
div.innerHTML = ` |
||||||
|
<input type="text" name="${name}" id="input-${name}" placeholder="${placeholder}" autocomplete="off" required=""> |
||||||
|
<label for="input-${name}">${label}</label> |
||||||
|
`;
|
||||||
|
|
||||||
|
return div; |
||||||
|
}; |
||||||
|
|
||||||
|
export default class PopupCreatePoll extends PopupElement { |
||||||
|
private questionInput: HTMLInputElement; |
||||||
|
private questions: HTMLElement; |
||||||
|
private scrollable: Scrollable; |
||||||
|
private tempID = 0; |
||||||
|
|
||||||
|
constructor() { |
||||||
|
super('popup-create-poll popup-new-media', null, {closable: true, withConfirm: 'CREATE', body: true}); |
||||||
|
|
||||||
|
this.title.innerText = 'New Poll'; |
||||||
|
|
||||||
|
const questionField = InputField('Ask a question', 'Ask a question', 'question'); |
||||||
|
this.questionInput = questionField.firstElementChild as HTMLInputElement; |
||||||
|
|
||||||
|
this.header.append(questionField); |
||||||
|
|
||||||
|
const d = document.createElement('div'); |
||||||
|
d.innerText = 'Options'; |
||||||
|
|
||||||
|
this.questions = document.createElement('div'); |
||||||
|
this.questions.classList.add('poll-create-questions'); |
||||||
|
|
||||||
|
this.body.append(d, this.questions); |
||||||
|
|
||||||
|
this.confirmBtn.addEventListener('click', this.onSubmitClick); |
||||||
|
|
||||||
|
this.scrollable = new Scrollable(this.body, 'y', undefined); |
||||||
|
this.appendMoreField(); |
||||||
|
} |
||||||
|
|
||||||
|
onSubmitClick = (e: MouseEvent) => { |
||||||
|
const question = this.questionInput.value; |
||||||
|
|
||||||
|
if(!question.trim()) { |
||||||
|
toast('Please enter a question'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const answers = Array.from(this.questions.children).map((el, idx) => { |
||||||
|
const input = (el.firstElementChild as HTMLInputElement); |
||||||
|
return input.value; |
||||||
|
}).filter(v => !!v.trim()); |
||||||
|
|
||||||
|
if(answers.length < 2) { |
||||||
|
toast('Please enter at least two options'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.closeBtn.click(); |
||||||
|
this.confirmBtn.removeEventListener('click', this.onSubmitClick); |
||||||
|
|
||||||
|
//const randomID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)];
|
||||||
|
//const randomIDS = bigint(randomID[0]).shiftLeft(32).add(bigint(randomID[1])).toString();
|
||||||
|
|
||||||
|
const poll: Partial<Poll> = {}; |
||||||
|
poll._ = 'poll'; |
||||||
|
//poll.id = randomIDS;
|
||||||
|
poll.flags = 0; |
||||||
|
poll.question = question; |
||||||
|
|
||||||
|
poll.answers = answers.map((value, idx) => { |
||||||
|
return { |
||||||
|
_: 'pollAnswer', |
||||||
|
text: value, |
||||||
|
option: new Uint8Array([idx]) |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
appMessagesManager.sendOther($rootScope.selectedPeerID, { |
||||||
|
_: 'inputMediaPoll', |
||||||
|
flags: 0, |
||||||
|
poll |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
onInput = (e: Event) => { |
||||||
|
const target = e.target as HTMLInputElement; |
||||||
|
|
||||||
|
if(target.value.length) { |
||||||
|
target.parentElement.classList.add('is-filled'); |
||||||
|
} |
||||||
|
|
||||||
|
const isLast = !target.parentElement.nextElementSibling; |
||||||
|
if(isLast && target.value.length && this.questions.childElementCount < 10) { |
||||||
|
this.appendMoreField(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
onDeleteClick = (e: MouseEvent) => { |
||||||
|
const target = e.target as HTMLSpanElement; |
||||||
|
target.parentElement.remove(); |
||||||
|
|
||||||
|
Array.from(this.questions.children).forEach((el, idx) => { |
||||||
|
const label = el.firstElementChild.nextElementSibling as HTMLLabelElement; |
||||||
|
label.innerText = 'Option ' + (idx + 1); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
private appendMoreField() { |
||||||
|
const idx = this.questions.childElementCount + 1; |
||||||
|
const questionField = InputField('Add an Option', 'Option ' + idx, 'question-' + this.tempID++); |
||||||
|
(questionField.firstElementChild as HTMLInputElement).addEventListener('input', this.onInput); |
||||||
|
|
||||||
|
const deleteBtn = document.createElement('span'); |
||||||
|
deleteBtn.classList.add('btn-icon', 'tgico-close'); |
||||||
|
questionField.append(deleteBtn); |
||||||
|
|
||||||
|
deleteBtn.addEventListener('click', this.onDeleteClick, {once: true}); |
||||||
|
|
||||||
|
this.questions.append(questionField); |
||||||
|
|
||||||
|
this.scrollable.scrollTo(this.scrollable.scrollHeight, true, true); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,272 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2018-present, Evgeny Nadymov |
||||||
|
* |
||||||
|
* This source code is licensed under the GPL v.3.0 license found in the |
||||||
|
* LICENSE file in the root directory of this source tree. |
||||||
|
*/ |
||||||
|
|
||||||
|
import MP4Box from 'mp4box/dist/mp4box.all.min'; |
||||||
|
//import { LOG, logSourceBufferRanges } from '../Utils/Common';
|
||||||
|
|
||||||
|
let LOG = () => console.log(...arguments); |
||||||
|
|
||||||
|
export default class MP4Source { |
||||||
|
constructor(video, getBufferAsync) { |
||||||
|
this.mp4file = null; |
||||||
|
this.nextBufferStart = 0; |
||||||
|
this.mediaSource = null; |
||||||
|
this.ready = false; |
||||||
|
this.bufferedTime = 40; |
||||||
|
|
||||||
|
this.beforeMoovBufferSize = 32 * 1024; |
||||||
|
this.moovBufferSize = 512 * 1024; |
||||||
|
this.bufferSize = 1024 * 1024; |
||||||
|
this.seekBufferSize = 1024 * 1024; |
||||||
|
|
||||||
|
this.currentBufferSize = this.beforeMoovBufferSize; |
||||||
|
this.nbSamples = 10; |
||||||
|
this.video = video; |
||||||
|
this.getBufferAsync = getBufferAsync; |
||||||
|
this.expectedSize = this.video.video.expected_size; |
||||||
|
|
||||||
|
this.seeking = false; |
||||||
|
this.loading = false; |
||||||
|
this.url = null; |
||||||
|
|
||||||
|
this.init(video.duration); |
||||||
|
} |
||||||
|
|
||||||
|
init(videoDuration) { |
||||||
|
const mediaSource = new MediaSource(); |
||||||
|
mediaSource.addEventListener('sourceopen', async () => { |
||||||
|
LOG('[MediaSource] sourceopen start', this.mediaSource, this); |
||||||
|
|
||||||
|
if (this.mediaSource.sourceBuffers.length > 0) return; |
||||||
|
|
||||||
|
const mp4File = MP4Box.createFile(); |
||||||
|
mp4File.onMoovStart = () => { |
||||||
|
LOG('[MP4Box] onMoovStart'); |
||||||
|
this.currentBufferSize = this.moovBufferSize; |
||||||
|
}; |
||||||
|
mp4File.onError = error => { |
||||||
|
LOG('[MP4Box] onError', error); |
||||||
|
}; |
||||||
|
mp4File.onReady = info => { |
||||||
|
LOG('[MP4Box] onReady', info); |
||||||
|
this.ready = true; |
||||||
|
this.currentBufferSize = this.bufferSize; |
||||||
|
const { isFragmented, timescale, fragment_duration, duration } = info; |
||||||
|
|
||||||
|
if (!fragment_duration && !duration) { |
||||||
|
this.mediaSource.duration = videoDuration; |
||||||
|
this.bufferedTime = videoDuration; |
||||||
|
} else { |
||||||
|
this.mediaSource.duration = isFragmented |
||||||
|
? fragment_duration / timescale |
||||||
|
: duration / timescale; |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i < info.tracks.length; i++) { |
||||||
|
this.addSourceBuffer(mp4File, this.mediaSource, info.tracks[i]); |
||||||
|
} |
||||||
|
|
||||||
|
const initSegs = mp4File.initializeSegmentation(); |
||||||
|
LOG('[MP4Box] initializeSegmentation', initSegs); |
||||||
|
|
||||||
|
for (let i = 0; i < initSegs.length; i++) { |
||||||
|
const { user: sourceBuffer } = initSegs[i]; |
||||||
|
sourceBuffer.onupdateend = () => { |
||||||
|
sourceBuffer.initSegs = true; |
||||||
|
sourceBuffer.onupdateend = this.handleSourceBufferUpdateEnd; |
||||||
|
}; |
||||||
|
sourceBuffer.appendBuffer(initSegs[i].buffer); |
||||||
|
} |
||||||
|
|
||||||
|
LOG('[MP4Box] start fragmentation'); |
||||||
|
mp4File.start(); |
||||||
|
}; |
||||||
|
mp4File.onSegment = (id, sourceBuffer, buffer, sampleNum, is_last) => { |
||||||
|
const isLast = (sampleNum + this.nbSamples) > sourceBuffer.nb_samples; |
||||||
|
|
||||||
|
LOG('[MP4Box] onSegment', id, buffer, `${sampleNum}/${sourceBuffer.nb_samples}`, isLast, sourceBuffer.timestampOffset); |
||||||
|
|
||||||
|
if (mediaSource.readyState !== 'open') { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
sourceBuffer.pendingUpdates.push({ id, buffer, sampleNum, isLast }); |
||||||
|
if (sourceBuffer.initSegs && !sourceBuffer.updating) { |
||||||
|
this.handleSourceBufferUpdateEnd({ target: sourceBuffer, mediaSource: this.mediaSource }); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
this.nextBufferStart = 0; |
||||||
|
this.mp4file = mp4File; |
||||||
|
LOG('[MediaSource] sourceopen end', this, this.mp4file); |
||||||
|
|
||||||
|
this.loadNextBuffer(); |
||||||
|
}); |
||||||
|
mediaSource.addEventListener('sourceended', () => { |
||||||
|
LOG('[MediaSource] sourceended', mediaSource.readyState); |
||||||
|
}); |
||||||
|
mediaSource.addEventListener('sourceclose', () => { |
||||||
|
LOG('[MediaSource] sourceclose', mediaSource.readyState); |
||||||
|
}); |
||||||
|
|
||||||
|
this.mediaSource = mediaSource; |
||||||
|
} |
||||||
|
|
||||||
|
addSourceBuffer(file, source, track) { |
||||||
|
if (!track) return null; |
||||||
|
|
||||||
|
const { id, codec, type: trackType, nb_samples } = track; |
||||||
|
const type = `video/mp4; codecs="${codec}"`; |
||||||
|
if (!MediaSource.isTypeSupported(type)) { |
||||||
|
LOG('[addSourceBuffer] not supported', type); |
||||||
|
return null; |
||||||
|
} |
||||||
|
// if (trackType !== 'video') {
|
||||||
|
// LOG('[addSourceBuffer] skip', trackType);
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const sourceBuffer = source.addSourceBuffer(type); |
||||||
|
sourceBuffer.id = id; |
||||||
|
sourceBuffer.pendingUpdates = []; |
||||||
|
sourceBuffer.nb_samples = nb_samples; |
||||||
|
file.setSegmentOptions(id, sourceBuffer, { nbSamples: this.nbSamples }); |
||||||
|
LOG('[addSourceBuffer] add', id, codec, trackType); |
||||||
|
|
||||||
|
return sourceBuffer; |
||||||
|
} |
||||||
|
|
||||||
|
handleSourceBufferUpdateEnd = event => { |
||||||
|
const { target: sourceBuffer } = event; |
||||||
|
const { mediaSource, mp4file } = this; |
||||||
|
|
||||||
|
if (!sourceBuffer) return; |
||||||
|
if (sourceBuffer.updating) return; |
||||||
|
|
||||||
|
//logSourceBufferRanges(sourceBuffer, 0, 0);
|
||||||
|
|
||||||
|
const { pendingUpdates } = sourceBuffer; |
||||||
|
if (!pendingUpdates) return; |
||||||
|
if (!pendingUpdates.length) { |
||||||
|
if (sourceBuffer.isLast && mediaSource.readyState === 'open') { |
||||||
|
LOG('[SourceBuffer] updateend endOfStream start', sourceBuffer.id); |
||||||
|
if (Array.from(mediaSource.sourceBuffers).every(x => !x.pendingUpdates.length && !x.updating)) { |
||||||
|
mediaSource.endOfStream(); |
||||||
|
LOG('[SourceBuffer] updateend endOfStream stop', sourceBuffer.id); |
||||||
|
} |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const update = pendingUpdates.shift(); |
||||||
|
if (!update) return; |
||||||
|
|
||||||
|
const { id, buffer, sampleNum, isLast } = update; |
||||||
|
|
||||||
|
if (sampleNum) { |
||||||
|
LOG('[SourceBuffer] updateend releaseUsedSamples', id, sampleNum); |
||||||
|
mp4file.releaseUsedSamples(id, sampleNum); |
||||||
|
} |
||||||
|
|
||||||
|
LOG('[SourceBuffer] updateend end', sourceBuffer.id, sourceBuffer.pendingUpdates.length); |
||||||
|
sourceBuffer.isLast = isLast; |
||||||
|
sourceBuffer.appendBuffer(buffer); |
||||||
|
}; |
||||||
|
|
||||||
|
getURL() { |
||||||
|
this.url = this.url || URL.createObjectURL(this.mediaSource); |
||||||
|
|
||||||
|
return this.url; |
||||||
|
} |
||||||
|
|
||||||
|
seek(currentTime, buffered) { |
||||||
|
const seekInfo = this.mp4file.seek(currentTime, true); |
||||||
|
this.nextBufferStart = seekInfo.offset; |
||||||
|
|
||||||
|
let loadNextBuffer = buffered.length === 0; |
||||||
|
for (let i = 0; i < buffered.length; i++) { |
||||||
|
const start = buffered.start(i); |
||||||
|
const end = buffered.end(i); |
||||||
|
|
||||||
|
if (start <= currentTime && currentTime + this.bufferedTime > end) { |
||||||
|
loadNextBuffer = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
LOG('[player] onSeeked', loadNextBuffer, currentTime, seekInfo, this.nextBufferStart); |
||||||
|
if (loadNextBuffer) { |
||||||
|
this.loadNextBuffer(true); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
timeUpdate(currentTime, duration, buffered) { |
||||||
|
const ranges = []; |
||||||
|
for (let i = 0; i < buffered.length; i++) { |
||||||
|
ranges.push({ start: buffered.start(i), end: buffered.end(i)}) |
||||||
|
} |
||||||
|
|
||||||
|
let loadNextBuffer = buffered.length === 0; |
||||||
|
let hasRange = false; |
||||||
|
for (let i = 0; i < buffered.length; i++) { |
||||||
|
const start = buffered.start(i); |
||||||
|
const end = buffered.end(i); |
||||||
|
|
||||||
|
if (start <= currentTime && currentTime <= end) { |
||||||
|
hasRange = true; |
||||||
|
if (end < duration && currentTime + this.bufferedTime > end) { |
||||||
|
loadNextBuffer = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!hasRange) { |
||||||
|
loadNextBuffer = true; |
||||||
|
} |
||||||
|
|
||||||
|
LOG('[player] timeUpdate', loadNextBuffer, currentTime, duration, JSON.stringify(ranges)); |
||||||
|
if (loadNextBuffer) { |
||||||
|
this.loadNextBuffer(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async loadNextBuffer(seek = false) { |
||||||
|
const { nextBufferStart, loading, currentBufferSize, mp4file } = this; |
||||||
|
LOG('[player] loadNextBuffer', nextBufferStart === undefined, loading, !mp4file); |
||||||
|
if (!mp4file) return; |
||||||
|
if (nextBufferStart === undefined) return; |
||||||
|
if (loading) return; |
||||||
|
|
||||||
|
this.loading = true; |
||||||
|
let bufferSize = seek ? this.seekBufferSize : this.bufferSize; |
||||||
|
if (nextBufferStart + bufferSize > this.expectedSize) { |
||||||
|
bufferSize = this.expectedSize - nextBufferStart; |
||||||
|
} |
||||||
|
const nextBuffer = await this.getBufferAsync(nextBufferStart, nextBufferStart + bufferSize); |
||||||
|
nextBuffer.fileStart = nextBufferStart; |
||||||
|
|
||||||
|
LOG('[player] loadNextBuffer start', nextBuffer.byteLength, nextBufferStart); |
||||||
|
if (nextBuffer.byteLength) { |
||||||
|
this.nextBufferStart = mp4file.appendBuffer(nextBuffer); |
||||||
|
} else { |
||||||
|
this.nextBufferStart = undefined; |
||||||
|
} |
||||||
|
LOG('[player] loadNextBuffer stop', nextBuffer.byteLength, nextBufferStart, this.nextBufferStart); |
||||||
|
|
||||||
|
if (nextBuffer.byteLength < currentBufferSize) { |
||||||
|
LOG('[player] loadNextBuffer flush'); |
||||||
|
this.mp4file.flush(); |
||||||
|
} |
||||||
|
|
||||||
|
this.loading = false; |
||||||
|
if (!this.ready) { |
||||||
|
LOG('[player] loadNextBuffer next'); |
||||||
|
this.loadNextBuffer(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,323 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2018-present, Evgeny Nadymov |
||||||
|
* |
||||||
|
* This source code is licensed under the GPL v.3.0 license found in the |
||||||
|
* LICENSE file in the root directory of this source tree. |
||||||
|
*/ |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import MP4Box from 'mp4box/dist/mp4box.all.min'; |
||||||
|
import { logger, LogLevels } from './polyfill'; |
||||||
|
|
||||||
|
export default class MP4Source { |
||||||
|
private mp4file: any; |
||||||
|
private nextBufferStart = 0; |
||||||
|
private mediaSource: MediaSource = null; |
||||||
|
private ready = false; |
||||||
|
private bufferedTime = 40; |
||||||
|
|
||||||
|
private beforeMoovBufferSize = 32 * 1024; |
||||||
|
private moovBufferSize = 512 * 1024; |
||||||
|
private bufferSize = 512 * 1024; |
||||||
|
private seekBufferSize = 256 * 1024; |
||||||
|
|
||||||
|
private currentBufferSize = this.beforeMoovBufferSize; |
||||||
|
private nbSamples = 10; |
||||||
|
private expectedSize: number; |
||||||
|
|
||||||
|
private seeking = false; |
||||||
|
private loading = false; |
||||||
|
private url: string; |
||||||
|
|
||||||
|
private log = logger('MP4', LogLevels.error); |
||||||
|
|
||||||
|
//public onLoadBuffer: (offset: number)
|
||||||
|
|
||||||
|
constructor(private video: {duration: number, video: {expected_size: number}}, private getBufferAsync: (start: number, end: number) => Promise<ArrayBuffer>) { |
||||||
|
this.expectedSize = this.video.video.expected_size; |
||||||
|
|
||||||
|
this.init(video.duration); |
||||||
|
} |
||||||
|
|
||||||
|
init(videoDuration: number) { |
||||||
|
const mediaSource = new MediaSource(); |
||||||
|
mediaSource.addEventListener('sourceopen', () => { |
||||||
|
this.log('[MediaSource] sourceopen start', this.mediaSource, this); |
||||||
|
|
||||||
|
if(this.mediaSource.sourceBuffers.length > 0) return; |
||||||
|
|
||||||
|
const mp4File = MP4Box.createFile(); |
||||||
|
mp4File.onMoovStart = () => { |
||||||
|
this.log('[MP4Box] onMoovStart'); |
||||||
|
this.currentBufferSize = this.moovBufferSize; |
||||||
|
}; |
||||||
|
|
||||||
|
mp4File.onError = (error: Error) => { |
||||||
|
this.log('[MP4Box] onError', error); |
||||||
|
}; |
||||||
|
|
||||||
|
mp4File.onReady = (info: any) => { |
||||||
|
this.log('[MP4Box] onReady', info); |
||||||
|
this.ready = true; |
||||||
|
this.currentBufferSize = this.bufferSize; |
||||||
|
const { isFragmented, timescale, fragment_duration, duration } = info; |
||||||
|
|
||||||
|
if(!fragment_duration && !duration) { |
||||||
|
this.mediaSource.duration = videoDuration; |
||||||
|
this.bufferedTime = videoDuration; |
||||||
|
} else { |
||||||
|
this.mediaSource.duration = isFragmented |
||||||
|
? fragment_duration / timescale |
||||||
|
: duration / timescale; |
||||||
|
} |
||||||
|
|
||||||
|
this.initializeAllSourceBuffers(info); |
||||||
|
}; |
||||||
|
|
||||||
|
mp4File.onSegment = (id: number, sb: any, buffer: ArrayBuffer, sampleNum: number, is_last: boolean) => { |
||||||
|
const isLast = (sampleNum + this.nbSamples) > sb.nb_samples; |
||||||
|
|
||||||
|
this.log('[MP4Box] onSegment', id, buffer, `${sampleNum}/${sb.nb_samples}`, isLast, sb.timestampOffset, mediaSource, is_last); |
||||||
|
|
||||||
|
sb.segmentIndex++; |
||||||
|
sb.pendingAppends.push({ id, buffer, sampleNum, is_last: isLast }); |
||||||
|
|
||||||
|
this.onUpdateEnd(sb, true, false); |
||||||
|
}; |
||||||
|
|
||||||
|
this.mp4file = mp4File; |
||||||
|
this.log('[MediaSource] sourceopen end', this, this.mp4file); |
||||||
|
|
||||||
|
this.loadNextBuffer(); |
||||||
|
}); |
||||||
|
|
||||||
|
mediaSource.addEventListener('sourceended', () => { |
||||||
|
this.log('[MediaSource] sourceended', mediaSource.readyState); |
||||||
|
//this.getBufferAsync = null;
|
||||||
|
}); |
||||||
|
|
||||||
|
mediaSource.addEventListener('sourceclose', () => { |
||||||
|
this.log('[MediaSource] sourceclose', mediaSource.readyState); |
||||||
|
//this.getBufferAsync = null;
|
||||||
|
}); |
||||||
|
|
||||||
|
this.mediaSource = mediaSource; |
||||||
|
} |
||||||
|
|
||||||
|
private onInitAppended(sb: any) { |
||||||
|
sb.sampleNum = 0; |
||||||
|
sb.addEventListener('updateend', () => this.onUpdateEnd(sb, true, true)); |
||||||
|
/* In case there are already pending buffers we call onUpdateEnd to start appending them*/ |
||||||
|
this.onUpdateEnd(sb, false, true); |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
this.mediaSource.pendingInits--; |
||||||
|
// @ts-ignore
|
||||||
|
if(this.mediaSource.pendingInits === 0) { |
||||||
|
this.log('onInitAppended start!'); |
||||||
|
this.mp4file.start(); |
||||||
|
|
||||||
|
if(this.expectedSize > this.bufferSize) { |
||||||
|
this.nextBufferStart = this.bufferSize; |
||||||
|
} else { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
/* setInterval(() => { |
||||||
|
this.loadNextBuffer(); |
||||||
|
}, 1e3); */ |
||||||
|
this.loadNextBuffer(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
private onUpdateEnd(sb: any, isNotInit: boolean, isEndOfAppend: boolean) { |
||||||
|
//console.this.log('onUpdateEnd', sb, isNotInit, isEndOfAppend, sb.sampleNum, sb.is_last);
|
||||||
|
if(isEndOfAppend === true) { |
||||||
|
if(sb.sampleNum) { |
||||||
|
this.mp4file.releaseUsedSamples(sb.id, sb.sampleNum); |
||||||
|
delete sb.sampleNum; |
||||||
|
} |
||||||
|
|
||||||
|
if(sb.is_last) { |
||||||
|
this.log('onUpdateEnd', sb, isNotInit, isEndOfAppend, sb.sampleNum, sb.is_last); |
||||||
|
this.mediaSource.endOfStream(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(this.mediaSource.readyState === "open" && sb.updating === false && sb.pendingAppends.length > 0) { |
||||||
|
const obj = sb.pendingAppends.shift(); |
||||||
|
this.log("MSE - SourceBuffer #"+sb.id, "Appending new buffer, pending: "+sb.pendingAppends.length); |
||||||
|
sb.sampleNum = obj.sampleNum; |
||||||
|
sb.is_last = obj.is_last; |
||||||
|
sb.appendBuffer(obj.buffer); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private initializeAllSourceBuffers(info: any) { |
||||||
|
for(let i = 0; i < info.tracks.length; i++) { |
||||||
|
this.addSourceBuffer(info.tracks[i]); |
||||||
|
} |
||||||
|
|
||||||
|
this.initializeSourceBuffers(); |
||||||
|
} |
||||||
|
|
||||||
|
private initializeSourceBuffers() { |
||||||
|
const initSegs = this.mp4file.initializeSegmentation(); |
||||||
|
this.log('[MP4Box] initializeSegmentation', initSegs); |
||||||
|
|
||||||
|
for(let i = 0; i < initSegs.length; i++) { |
||||||
|
const sb: any = initSegs[i].user; |
||||||
|
if(i === 0) { |
||||||
|
// @ts-ignore
|
||||||
|
this.mediaSource.pendingInits = 0; |
||||||
|
} |
||||||
|
|
||||||
|
let onInitAppended = () => { |
||||||
|
if(this.mediaSource.readyState === "open") { |
||||||
|
sb.removeEventListener('updateend', onInitAppended); |
||||||
|
this.onInitAppended(sb); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
sb.addEventListener('updateend', onInitAppended); |
||||||
|
sb.appendBuffer(initSegs[i].buffer); |
||||||
|
sb.segmentIndex = 0; |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
this.mediaSource.pendingInits++; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private addSourceBuffer(track: {id: number, codec: string, type: 'video', nb_samples: number}) { |
||||||
|
const file = this.mp4file; |
||||||
|
const ms = this.mediaSource; |
||||||
|
if(!track) return; |
||||||
|
|
||||||
|
const { id, codec, type: trackType, nb_samples } = track; |
||||||
|
const mime = `video/mp4; codecs="${codec}"`; |
||||||
|
this.log('mimetype:', mime); |
||||||
|
if(!MediaSource.isTypeSupported(mime)) { |
||||||
|
this.log('[addSourceBuffer] not supported', mime); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const sb: any = ms.addSourceBuffer(mime); |
||||||
|
sb.id = id; |
||||||
|
sb.pendingAppends = []; |
||||||
|
sb.nb_samples = nb_samples; |
||||||
|
file.setSegmentOptions(id, sb, { nbSamples: this.nbSamples }); |
||||||
|
|
||||||
|
this.log('[addSourceBuffer] add', id, codec, trackType, sb); |
||||||
|
sb.addEventListener("error", (e: Event) => { |
||||||
|
this.log("MSE SourceBuffer #" + id, e); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
stop() { |
||||||
|
this.mp4file.stop(); |
||||||
|
this.mp4file = null; |
||||||
|
this.getBufferAsync = null; |
||||||
|
} |
||||||
|
|
||||||
|
getURL() { |
||||||
|
return this.url ?? (this.url = URL.createObjectURL(this.mediaSource)); |
||||||
|
} |
||||||
|
|
||||||
|
seek(currentTime: number/* , buffered: any */) { |
||||||
|
const seekInfo: {offset: number, time: number} = this.mp4file.seek(currentTime, true); |
||||||
|
this.nextBufferStart = seekInfo.offset; |
||||||
|
|
||||||
|
const loadNextBuffer = true; |
||||||
|
/* let loadNextBuffer = buffered.length === 0; |
||||||
|
for(let i = 0; i < buffered.length; i++) { |
||||||
|
const start = buffered.start(i); |
||||||
|
const end = buffered.end(i); |
||||||
|
|
||||||
|
if(start <= currentTime && currentTime + this.bufferedTime > end) { |
||||||
|
loadNextBuffer = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} */ |
||||||
|
|
||||||
|
this.log('[player] onSeeked', loadNextBuffer, currentTime, seekInfo, this.nextBufferStart); |
||||||
|
if(loadNextBuffer) { |
||||||
|
this.loadNextBuffer(true); |
||||||
|
} |
||||||
|
|
||||||
|
return seekInfo.offset; |
||||||
|
} |
||||||
|
|
||||||
|
timeUpdate(currentTime: number, duration: number, buffered: any) { |
||||||
|
//return;
|
||||||
|
|
||||||
|
const ranges = []; |
||||||
|
for(let i = 0; i < buffered.length; i++) { |
||||||
|
ranges.push({ start: buffered.start(i), end: buffered.end(i)}) |
||||||
|
} |
||||||
|
|
||||||
|
let loadNextBuffer = buffered.length === 0; |
||||||
|
let hasRange = false; |
||||||
|
for(let i = 0; i < buffered.length; i++) { |
||||||
|
const start = buffered.start(i); |
||||||
|
const end = buffered.end(i); |
||||||
|
|
||||||
|
if (start <= currentTime && currentTime <= end) { |
||||||
|
hasRange = true; |
||||||
|
if (end < duration && currentTime + this.bufferedTime > end) { |
||||||
|
loadNextBuffer = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(!hasRange) { |
||||||
|
loadNextBuffer = true; |
||||||
|
} |
||||||
|
|
||||||
|
this.log('[player] timeUpdate', loadNextBuffer, currentTime, duration, JSON.stringify(ranges)); |
||||||
|
if(loadNextBuffer) { |
||||||
|
this.loadNextBuffer(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async loadNextBuffer(seek = false) { |
||||||
|
const { nextBufferStart, loading, currentBufferSize, mp4file } = this; |
||||||
|
this.log('[player] loadNextBuffer', nextBufferStart === undefined, loading, !mp4file); |
||||||
|
if(!mp4file) return; |
||||||
|
if(nextBufferStart === undefined) return; |
||||||
|
if(loading) return; |
||||||
|
|
||||||
|
//return;
|
||||||
|
|
||||||
|
this.loading = true; |
||||||
|
let bufferSize = seek ? this.seekBufferSize : this.bufferSize; |
||||||
|
if(nextBufferStart + bufferSize > this.expectedSize) { |
||||||
|
bufferSize = this.expectedSize - nextBufferStart; |
||||||
|
} |
||||||
|
const nextBuffer = await this.getBufferAsync(nextBufferStart, nextBufferStart + bufferSize); |
||||||
|
// @ts-ignore
|
||||||
|
nextBuffer.fileStart = nextBufferStart; |
||||||
|
|
||||||
|
const end = (nextBuffer.byteLength !== bufferSize)/* || (nextBuffer.byteLength === this.expectedSize) */; |
||||||
|
|
||||||
|
this.log('[player] loadNextBuffer start', nextBuffer.byteLength, nextBufferStart, end); |
||||||
|
if(nextBuffer.byteLength) { |
||||||
|
this.nextBufferStart = mp4file.appendBuffer(nextBuffer/* , end */); |
||||||
|
} else { |
||||||
|
this.nextBufferStart = undefined; |
||||||
|
} |
||||||
|
|
||||||
|
if(end) { |
||||||
|
this.log('[player] loadNextBuffer flush'); |
||||||
|
this.mp4file.flush(); |
||||||
|
} |
||||||
|
|
||||||
|
this.log('[player] loadNextBuffer stop', nextBuffer.byteLength, nextBufferStart, this.nextBufferStart); |
||||||
|
|
||||||
|
this.loading = false; |
||||||
|
if(!this.ready || !end) { |
||||||
|
this.log('[player] loadNextBuffer next'); |
||||||
|
this.loadNextBuffer(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,279 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2018-present, Evgeny Nadymov |
||||||
|
* |
||||||
|
* This source code is licensed under the GPL v.3.0 license found in the |
||||||
|
* LICENSE file in the root directory of this source tree. |
||||||
|
*/ |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import MP4Box from 'mp4box/dist/mp4box.all.min'; |
||||||
|
|
||||||
|
let LOG = (...args: any[]) => { |
||||||
|
console.log(...args); |
||||||
|
}; |
||||||
|
|
||||||
|
export default class MP4Source { |
||||||
|
private mp4file: any; |
||||||
|
private nextBufferStart = 0; |
||||||
|
private mediaSource: MediaSource = null; |
||||||
|
private ready = false; |
||||||
|
private bufferedTime = 40; |
||||||
|
|
||||||
|
private beforeMoovBufferSize = 32 * 1024; |
||||||
|
private moovBufferSize = 512 * 1024; |
||||||
|
private bufferSize = 512 * 1024; |
||||||
|
private seekBufferSize = 512 * 1024; |
||||||
|
|
||||||
|
private currentBufferSize = this.beforeMoovBufferSize; |
||||||
|
private nbSamples = 12; |
||||||
|
private expectedSize: number; |
||||||
|
|
||||||
|
private seeking = false; |
||||||
|
private loading = false; |
||||||
|
private url: string = null; |
||||||
|
|
||||||
|
constructor(private video: {duration: number, video: {expected_size: number}}, private getBufferAsync: (start: number, end: number) => Promise<ArrayBuffer>) { |
||||||
|
this.expectedSize = this.video.video.expected_size; |
||||||
|
|
||||||
|
this.init(video.duration); |
||||||
|
} |
||||||
|
|
||||||
|
init(videoDuration: any) { |
||||||
|
const mediaSource = new MediaSource(); |
||||||
|
mediaSource.addEventListener('sourceopen', async () => { |
||||||
|
LOG('[MediaSource] sourceopen start', this.mediaSource, this); |
||||||
|
|
||||||
|
if (this.mediaSource.sourceBuffers.length > 0) return; |
||||||
|
|
||||||
|
const mp4File = MP4Box.createFile(); |
||||||
|
mp4File.onMoovStart = () => { |
||||||
|
LOG('[MP4Box] onMoovStart'); |
||||||
|
this.currentBufferSize = this.moovBufferSize; |
||||||
|
}; |
||||||
|
mp4File.onError = (error: any) => { |
||||||
|
LOG('[MP4Box] onError', error); |
||||||
|
}; |
||||||
|
mp4File.onReady = (info: any) => { |
||||||
|
LOG('[MP4Box] onReady', info); |
||||||
|
this.ready = true; |
||||||
|
this.currentBufferSize = this.bufferSize; |
||||||
|
const { isFragmented, timescale, fragment_duration, duration } = info; |
||||||
|
|
||||||
|
if (!fragment_duration && !duration) { |
||||||
|
this.mediaSource.duration = videoDuration; |
||||||
|
this.bufferedTime = videoDuration; |
||||||
|
} else { |
||||||
|
this.mediaSource.duration = isFragmented |
||||||
|
? fragment_duration / timescale |
||||||
|
: duration / timescale; |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i < info.tracks.length; i++) { |
||||||
|
this.addSourceBuffer(mp4File, this.mediaSource, info.tracks[i]); |
||||||
|
} |
||||||
|
|
||||||
|
const initSegs = mp4File.initializeSegmentation(); |
||||||
|
LOG('[MP4Box] initializeSegmentation', initSegs); |
||||||
|
|
||||||
|
for (let i = 0; i < initSegs.length; i++) { |
||||||
|
const { user: sourceBuffer } = initSegs[i]; |
||||||
|
sourceBuffer.onupdateend = () => { |
||||||
|
sourceBuffer.initSegs = true; |
||||||
|
sourceBuffer.onupdateend = this.handleSourceBufferUpdateEnd; |
||||||
|
}; |
||||||
|
sourceBuffer.appendBuffer(initSegs[i].buffer); |
||||||
|
} |
||||||
|
|
||||||
|
LOG('[MP4Box] start fragmentation'); |
||||||
|
mp4File.start(); |
||||||
|
|
||||||
|
setInterval(() => { |
||||||
|
this.loadNextBuffer(); |
||||||
|
}, 1e3); |
||||||
|
}; |
||||||
|
mp4File.onSegment = (id: any, sourceBuffer: any, buffer: any, sampleNum: any, is_last: boolean) => { |
||||||
|
const isLast = (sampleNum + this.nbSamples) > sourceBuffer.nb_samples; |
||||||
|
|
||||||
|
LOG('[MP4Box] onSegment', id, buffer, `${sampleNum}/${sourceBuffer.nb_samples}`, isLast, sourceBuffer.timestampOffset); |
||||||
|
|
||||||
|
if (mediaSource.readyState !== 'open') { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
sourceBuffer.pendingUpdates.push({ id, buffer, sampleNum, isLast }); |
||||||
|
if (sourceBuffer.initSegs && !sourceBuffer.updating) { |
||||||
|
this.handleSourceBufferUpdateEnd({ target: sourceBuffer, mediaSource: this.mediaSource }); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
this.nextBufferStart = 0; |
||||||
|
this.mp4file = mp4File; |
||||||
|
LOG('[MediaSource] sourceopen end', this, this.mp4file); |
||||||
|
|
||||||
|
this.loadNextBuffer(); |
||||||
|
}); |
||||||
|
mediaSource.addEventListener('sourceended', () => { |
||||||
|
LOG('[MediaSource] sourceended', mediaSource.readyState); |
||||||
|
}); |
||||||
|
mediaSource.addEventListener('sourceclose', () => { |
||||||
|
LOG('[MediaSource] sourceclose', mediaSource.readyState); |
||||||
|
}); |
||||||
|
|
||||||
|
this.mediaSource = mediaSource; |
||||||
|
} |
||||||
|
|
||||||
|
addSourceBuffer(file: any, source: any, track: any) { |
||||||
|
if (!track) return null; |
||||||
|
|
||||||
|
const { id, codec, type: trackType, nb_samples } = track; |
||||||
|
const type = `video/mp4; codecs="${codec}"`; |
||||||
|
if (!MediaSource.isTypeSupported(type)) { |
||||||
|
LOG('[addSourceBuffer] not supported', type); |
||||||
|
return null; |
||||||
|
} |
||||||
|
// if (trackType !== 'video') {
|
||||||
|
// LOG('[addSourceBuffer] skip', trackType);
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const sourceBuffer = source.addSourceBuffer(type); |
||||||
|
sourceBuffer.id = id; |
||||||
|
sourceBuffer.pendingUpdates = []; |
||||||
|
sourceBuffer.nb_samples = nb_samples; |
||||||
|
file.setSegmentOptions(id, sourceBuffer, { nbSamples: this.nbSamples }); |
||||||
|
LOG('[addSourceBuffer] add', id, codec, trackType); |
||||||
|
|
||||||
|
return sourceBuffer; |
||||||
|
} |
||||||
|
|
||||||
|
handleSourceBufferUpdateEnd = (event: any) => { |
||||||
|
const { target: sourceBuffer } = event; |
||||||
|
const { mediaSource, mp4file } = this; |
||||||
|
|
||||||
|
if (!sourceBuffer) return; |
||||||
|
if (sourceBuffer.updating) return; |
||||||
|
|
||||||
|
//logSourceBufferRanges(sourceBuffer, 0, 0);
|
||||||
|
|
||||||
|
const { pendingUpdates } = sourceBuffer; |
||||||
|
if (!pendingUpdates) return; |
||||||
|
if (!pendingUpdates.length) { |
||||||
|
if (sourceBuffer.isLast && mediaSource.readyState === 'open') { |
||||||
|
LOG('[SourceBuffer] updateend endOfStream start', sourceBuffer.id); |
||||||
|
if (Array.from(mediaSource.sourceBuffers).every((x: any) => !x.pendingUpdates.length && !x.updating)) { |
||||||
|
mediaSource.endOfStream(); |
||||||
|
LOG('[SourceBuffer] updateend endOfStream stop', sourceBuffer.id); |
||||||
|
} |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const update = pendingUpdates.shift(); |
||||||
|
if (!update) return; |
||||||
|
|
||||||
|
const { id, buffer, sampleNum, isLast } = update; |
||||||
|
|
||||||
|
if (sampleNum) { |
||||||
|
LOG('[SourceBuffer] updateend releaseUsedSamples', id, sampleNum); |
||||||
|
mp4file.releaseUsedSamples(id, sampleNum); |
||||||
|
} |
||||||
|
|
||||||
|
LOG('[SourceBuffer] updateend end', sourceBuffer.id, sourceBuffer.pendingUpdates.length); |
||||||
|
sourceBuffer.isLast = isLast; |
||||||
|
sourceBuffer.appendBuffer(buffer); |
||||||
|
}; |
||||||
|
|
||||||
|
getURL() { |
||||||
|
this.url = this.url || URL.createObjectURL(this.mediaSource); |
||||||
|
|
||||||
|
return this.url; |
||||||
|
} |
||||||
|
|
||||||
|
seek(currentTime: number, buffered: any) { |
||||||
|
const seekInfo = this.mp4file.seek(currentTime, true); |
||||||
|
this.nextBufferStart = seekInfo.offset; |
||||||
|
|
||||||
|
let loadNextBuffer = buffered.length === 0; |
||||||
|
for (let i = 0; i < buffered.length; i++) { |
||||||
|
const start = buffered.start(i); |
||||||
|
const end = buffered.end(i); |
||||||
|
|
||||||
|
if (start <= currentTime && currentTime + this.bufferedTime > end) { |
||||||
|
loadNextBuffer = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
LOG('[player] onSeeked', loadNextBuffer, currentTime, seekInfo, this.nextBufferStart); |
||||||
|
if (loadNextBuffer) { |
||||||
|
this.loadNextBuffer(true); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
timeUpdate(currentTime: number, duration: number, buffered: any) { |
||||||
|
const ranges = []; |
||||||
|
for (let i = 0; i < buffered.length; i++) { |
||||||
|
ranges.push({ start: buffered.start(i), end: buffered.end(i)}) |
||||||
|
} |
||||||
|
|
||||||
|
let loadNextBuffer = buffered.length === 0; |
||||||
|
let hasRange = false; |
||||||
|
for (let i = 0; i < buffered.length; i++) { |
||||||
|
const start = buffered.start(i); |
||||||
|
const end = buffered.end(i); |
||||||
|
|
||||||
|
if (start <= currentTime && currentTime <= end) { |
||||||
|
hasRange = true; |
||||||
|
if (end < duration && currentTime + this.bufferedTime > end) { |
||||||
|
loadNextBuffer = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!hasRange) { |
||||||
|
loadNextBuffer = true; |
||||||
|
} |
||||||
|
|
||||||
|
LOG('[player] timeUpdate', loadNextBuffer, currentTime, duration, JSON.stringify(ranges)); |
||||||
|
if (loadNextBuffer) { |
||||||
|
this.loadNextBuffer(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async loadNextBuffer(seek = false) { |
||||||
|
const { nextBufferStart, loading, currentBufferSize, mp4file } = this; |
||||||
|
LOG('[player] loadNextBuffer', nextBufferStart === undefined, loading, !mp4file); |
||||||
|
if (!mp4file) return; |
||||||
|
if (nextBufferStart === undefined) return; |
||||||
|
if (loading) return; |
||||||
|
|
||||||
|
this.loading = true; |
||||||
|
let bufferSize = seek ? this.seekBufferSize : this.bufferSize; |
||||||
|
if (nextBufferStart + bufferSize > this.expectedSize) { |
||||||
|
bufferSize = this.expectedSize - nextBufferStart; |
||||||
|
} |
||||||
|
const nextBuffer = await this.getBufferAsync(nextBufferStart, nextBufferStart + bufferSize); |
||||||
|
// @ts-ignore
|
||||||
|
nextBuffer.fileStart = nextBufferStart; |
||||||
|
|
||||||
|
LOG('[player] loadNextBuffer start', nextBuffer.byteLength, nextBufferStart); |
||||||
|
if (nextBuffer.byteLength) { |
||||||
|
this.nextBufferStart = mp4file.appendBuffer(nextBuffer); |
||||||
|
} else { |
||||||
|
this.nextBufferStart = undefined; |
||||||
|
} |
||||||
|
LOG('[player] loadNextBuffer stop', nextBuffer.byteLength, nextBufferStart, this.nextBufferStart); |
||||||
|
|
||||||
|
if (nextBuffer.byteLength < currentBufferSize) { |
||||||
|
LOG('[player] loadNextBuffer flush'); |
||||||
|
this.mp4file.flush(); |
||||||
|
} |
||||||
|
|
||||||
|
this.loading = false; |
||||||
|
if (!this.ready) { |
||||||
|
LOG('[player] loadNextBuffer next'); |
||||||
|
this.loadNextBuffer(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
class ReferenceDatabase { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
const referenceDatabase = new ReferenceDatabase(); |
||||||
|
// @ts-ignore
|
||||||
|
if(process.env.NODE_ENV != 'production') { |
||||||
|
(window as any).referenceDatabase = referenceDatabase; |
||||||
|
} |
||||||
|
export default referenceDatabase; |
@ -0,0 +1,33 @@ |
|||||||
|
.popup-create-poll { |
||||||
|
$parent: ".popup"; |
||||||
|
|
||||||
|
#{$parent} { |
||||||
|
&-container { |
||||||
|
max-height: 468px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.input-field { |
||||||
|
.btn-icon { |
||||||
|
position: absolute; |
||||||
|
right: .5rem; |
||||||
|
top: 50%; |
||||||
|
z-index: 1; |
||||||
|
transform: translateY(-50%); |
||||||
|
opacity: 1; |
||||||
|
transition: opacity .2s ease; |
||||||
|
} |
||||||
|
|
||||||
|
&:not(.is-filled), &:first-child:last-child { |
||||||
|
.btn-icon { |
||||||
|
pointer-events: none; |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
/* &:last-child:not(:nth-child(10)) { |
||||||
|
.btn-icon { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
} */ |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue