morethanwords
4 years ago
62 changed files with 4050 additions and 814 deletions
@ -0,0 +1,526 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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