Completed task #3:
Recording voice messages: 1) Record and play on Safari 2) Ripple effect 3) Encoding and decoding new waveform
This commit is contained in:
parent
7bd087ec0e
commit
ca4946f32e
@ -11,7 +11,9 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager";
|
||||
import initEmoticonsDropdown, { EMOTICONSSTICKERGROUP } from "./emoticonsDropdown";
|
||||
import lottieLoader from "../lib/lottieLoader";
|
||||
import { Layouter, RectPart } from "./groupedLayout";
|
||||
import Recorder from '../opus-recorder/dist/recorder.min';
|
||||
import Recorder from '../../public/recorder.min';
|
||||
//import Recorder from '../opus-recorder/dist/recorder.min';
|
||||
import opusDecodeController from "../lib/opusDecodeController";
|
||||
|
||||
export class ChatInput {
|
||||
public pageEl = document.getElementById('page-chats') as HTMLDivElement;
|
||||
@ -20,7 +22,7 @@ export class ChatInput {
|
||||
public inputMessageContainer = document.getElementsByClassName('input-message-container')[0] as HTMLDivElement;
|
||||
public inputScroll = new Scrollable(this.inputMessageContainer);
|
||||
public btnSend = document.getElementById('btn-send') as HTMLButtonElement;
|
||||
public btnCancelRecord = this.btnSend.previousElementSibling as HTMLButtonElement;
|
||||
public btnCancelRecord = this.btnSend.parentElement.previousElementSibling as HTMLButtonElement;
|
||||
public emoticonsDropdown: HTMLDivElement = null;
|
||||
public emoticonsTimeout: number = 0;
|
||||
public toggleEmoticons: HTMLButtonElement;
|
||||
@ -28,7 +30,7 @@ export class ChatInput {
|
||||
public lastUrl = '';
|
||||
public lastTimeType = 0;
|
||||
|
||||
private inputContainer = this.btnSend.parentElement as HTMLDivElement;
|
||||
private inputContainer = this.btnSend.parentElement.parentElement as HTMLDivElement;
|
||||
|
||||
public attachMenu: {
|
||||
container?: HTMLButtonElement,
|
||||
@ -68,6 +70,8 @@ export class ChatInput {
|
||||
private recording = false;
|
||||
private recordCanceled = false;
|
||||
private recordTimeEl = this.inputContainer.querySelector('.record-time') as HTMLDivElement;
|
||||
private recordRippleEl = this.inputContainer.querySelector('.record-ripple') as HTMLDivElement;
|
||||
private recordStartTime = 0;
|
||||
|
||||
constructor() {
|
||||
this.toggleEmoticons = this.pageEl.querySelector('.toggle-emoticons') as HTMLButtonElement;
|
||||
@ -470,12 +474,37 @@ export class ChatInput {
|
||||
this.btnSend.classList.add('tgico-send');
|
||||
this.inputContainer.classList.add('is-recording');
|
||||
this.recording = true;
|
||||
opusDecodeController.setKeepAlive(true);
|
||||
|
||||
let startTime = Date.now();
|
||||
this.recordStartTime = Date.now();
|
||||
|
||||
const sourceNode: MediaStreamAudioSourceNode = this.recorder.sourceNode;
|
||||
const context = sourceNode.context;
|
||||
|
||||
const analyser = context.createAnalyser();
|
||||
sourceNode.connect(analyser);
|
||||
//analyser.connect(context.destination);
|
||||
analyser.fftSize = 32;
|
||||
|
||||
const frequencyData = new Uint8Array(analyser.frequencyBinCount);
|
||||
const max = frequencyData.length * 255;
|
||||
const min = 54 / 150;
|
||||
let r = () => {
|
||||
if(!this.recording) return;
|
||||
|
||||
let diff = Date.now() - startTime;
|
||||
analyser.getByteFrequencyData(frequencyData);
|
||||
|
||||
let sum = 0;
|
||||
frequencyData.forEach(value => {
|
||||
sum += value;
|
||||
});
|
||||
|
||||
let percents = Math.min(1, (sum / max) + min);
|
||||
console.log('frequencyData', frequencyData, percents);
|
||||
|
||||
this.recordRippleEl.style.transform = `scale(${percents})`;
|
||||
|
||||
let diff = Date.now() - this.recordStartTime;
|
||||
let ms = diff % 1000;
|
||||
|
||||
let formatted = ('' + (diff / 1000)).toHHMMSS() + ',' + ('00' + Math.round(ms / 10)).slice(-2);
|
||||
@ -495,20 +524,23 @@ export class ChatInput {
|
||||
this.btnCancelRecord.addEventListener('click', () => {
|
||||
this.recordCanceled = true;
|
||||
this.recorder.stop();
|
||||
opusDecodeController.setKeepAlive(false);
|
||||
});
|
||||
|
||||
this.recorder.onstop = () => {
|
||||
this.recording = false;
|
||||
this.inputContainer.classList.remove('is-recording');
|
||||
this.btnSend.classList.remove('tgico-send');
|
||||
this.recordRippleEl.style.transform = '';
|
||||
};
|
||||
|
||||
this.recorder.ondataavailable = (typedArray: Uint8Array) => {
|
||||
if(this.recordCanceled) return;
|
||||
|
||||
const duration = (Date.now() - this.recordStartTime) / 1000 | 0;
|
||||
const dataBlob = new Blob([typedArray], {type: 'audio/ogg'});
|
||||
const fileName = new Date().toISOString() + ".opus";
|
||||
console.log('Recorder data received', typedArray, dataBlob);
|
||||
/* const fileName = new Date().toISOString() + ".opus";
|
||||
console.log('Recorder data received', typedArray, dataBlob); */
|
||||
|
||||
/* var url = URL.createObjectURL( dataBlob );
|
||||
|
||||
@ -529,11 +561,21 @@ export class ChatInput {
|
||||
|
||||
return; */
|
||||
|
||||
let peerID = appImManager.peerID;
|
||||
appMessagesManager.sendFile(peerID, dataBlob, {
|
||||
isVoiceMessage: true,
|
||||
duration: 0,
|
||||
isMedia: true
|
||||
let perf = performance.now();
|
||||
opusDecodeController.decode(typedArray, true).then(result => {
|
||||
console.log('WAVEFORM!:', /* waveform, */performance.now() - perf);
|
||||
|
||||
opusDecodeController.setKeepAlive(false);
|
||||
|
||||
let peerID = appImManager.peerID;
|
||||
// тут objectURL ставится уже с audio/wav
|
||||
appMessagesManager.sendFile(peerID, dataBlob, {
|
||||
isVoiceMessage: true,
|
||||
isMedia: true,
|
||||
duration,
|
||||
waveform: result.waveform,
|
||||
objectURL: result.url
|
||||
});
|
||||
});
|
||||
|
||||
/* const url = URL.createObjectURL(dataBlob);
|
||||
|
@ -192,7 +192,7 @@ let formatDate = (timestamp: number) => {
|
||||
|
||||
export function wrapDocument(doc: MTDocument, withTime = false, uploading = false): HTMLDivElement {
|
||||
if(doc.type == 'voice') {
|
||||
return wrapVoiceMessage(doc, withTime);
|
||||
return wrapVoiceMessage(doc, uploading);
|
||||
} else if(doc.type == 'audio') {
|
||||
return wrapAudio(doc, withTime);
|
||||
}
|
||||
@ -384,8 +384,42 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement {
|
||||
return div;
|
||||
}
|
||||
|
||||
// https://github.com/LonamiWebs/Telethon/blob/4393ec0b83d511b6a20d8a20334138730f084375/telethon/utils.py#L1285
|
||||
function decodeWaveform(waveform: Uint8Array | number[]) {
|
||||
if(!(waveform instanceof Uint8Array)) {
|
||||
waveform = new Uint8Array(waveform);
|
||||
}
|
||||
|
||||
var bitCount = waveform.length * 8;
|
||||
var valueCount = bitCount / 5 | 0;
|
||||
if(!valueCount) {
|
||||
return new Uint8Array([]);
|
||||
}
|
||||
|
||||
var dataView = new DataView(waveform.buffer);
|
||||
var result = new Uint8Array(valueCount);
|
||||
for(var i = 0; i < valueCount; i++) {
|
||||
var byteIndex = i * 5 / 8 | 0;
|
||||
var bitShift = i * 5 % 8;
|
||||
var value = dataView.getUint16(byteIndex, true);
|
||||
result[i] = (value >> bitShift) & 0b00011111;
|
||||
}
|
||||
|
||||
/* var byteIndex = (valueCount - 1) / 8 | 0;
|
||||
var bitShift = (valueCount - 1) % 8;
|
||||
if(byteIndex == waveform.length - 1) {
|
||||
var value = waveform[byteIndex];
|
||||
} else {
|
||||
var value = dataView.getUint16(byteIndex, true);
|
||||
}
|
||||
|
||||
console.log('decoded waveform, setting last value:', value, byteIndex, bitShift);
|
||||
result[valueCount - 1] = (value >> bitShift) & 0b00011111; */
|
||||
return result;
|
||||
}
|
||||
|
||||
let lastAudioToggle: HTMLDivElement = null;
|
||||
export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElement {
|
||||
export function wrapVoiceMessage(doc: MTDocument, uploading = false): HTMLDivElement {
|
||||
let div = document.createElement('div');
|
||||
div.classList.add('audio', 'is-voice');
|
||||
|
||||
@ -395,7 +429,7 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="audio-toggle audio-ico tgico-largeplay"></div>
|
||||
<div class="audio-download"><div class="tgico-download"></div></div>
|
||||
<div class="audio-download">${uploading ? '' : '<div class="tgico-download"></div>'}</div>
|
||||
<div class="audio-time">${durationStr}</div>
|
||||
`;
|
||||
|
||||
@ -414,9 +448,66 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem
|
||||
svg.setAttributeNS(null, 'viewBox', '0 0 190 23');
|
||||
|
||||
div.insertBefore(svg, div.lastElementChild);
|
||||
let wave = doc.attributes[0].waveform as Uint8Array;
|
||||
|
||||
const barWidth = 2;
|
||||
const barMargin = 1;
|
||||
const barHeightMin = 2;
|
||||
const barHeightMax = 23;
|
||||
|
||||
let waveform = doc.attributes[0].waveform || [];
|
||||
waveform = decodeWaveform(waveform.slice());
|
||||
|
||||
//console.log('decoded waveform:', waveform);
|
||||
|
||||
const normValue = Math.max(...waveform);
|
||||
const wfSize = waveform.length ? waveform.length : 100;
|
||||
const availW = 190;
|
||||
const barCount = Math.min((availW / (barWidth + barMargin)) | 0, wfSize);
|
||||
|
||||
let maxValue = 0;
|
||||
let maxDelta = barHeightMax - barHeightMin;
|
||||
|
||||
let html = '';
|
||||
for(let i = 0, barX = 0, sumI = 0; i < wfSize; ++i) {
|
||||
const value = waveform[i] || 0;
|
||||
if((sumI + barCount) >= wfSize) { // draw bar
|
||||
sumI = sumI + barCount - wfSize;
|
||||
if(sumI < (barCount + 1) / 2) {
|
||||
if(maxValue < value) maxValue = value;
|
||||
}
|
||||
|
||||
const bar_value = Math.max(((maxValue * maxDelta) + ((normValue + 1) / 2)) / (normValue + 1), barHeightMin);
|
||||
|
||||
let h = `
|
||||
<rect x="${barX}" y="${barHeightMax - bar_value}" width="2" height="${bar_value}" rx="1" ry="1"></rect>
|
||||
`;
|
||||
html += h;
|
||||
|
||||
/* if(barX >= activeW) {
|
||||
p.fillRect(nameleft + barX, bottom - bar_value, barWidth, barHeightMin + bar_value, inactive);
|
||||
} else if (barX + barWidth <= activeW) {
|
||||
p.fillRect(nameleft + barX, bottom - bar_value, barWidth, barHeightMin + bar_value, active);
|
||||
} else {
|
||||
p.fillRect(nameleft + barX, bottom - bar_value, activeW - barX, barHeightMin + bar_value, active);
|
||||
p.fillRect(nameleft + activeW, bottom - bar_value, barWidth - (activeW - barX), barHeightMin + bar_value, inactive);
|
||||
} */
|
||||
barX += barWidth + barMargin;
|
||||
|
||||
if(sumI < (barCount + 1) / 2) {
|
||||
maxValue = 0;
|
||||
} else {
|
||||
maxValue = value;
|
||||
}
|
||||
} else {
|
||||
if(maxValue < value) maxValue = value;
|
||||
|
||||
sumI += barCount;
|
||||
}
|
||||
}
|
||||
|
||||
svg.insertAdjacentHTML('beforeend', html);
|
||||
|
||||
let index = 0;
|
||||
/* let index = 0;
|
||||
let skipped = 0;
|
||||
let h = '';
|
||||
for(let uint8 of wave) {
|
||||
@ -425,10 +516,11 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
let percents = uint8 / 255;
|
||||
//let percents = uint8 / 255;
|
||||
let percents = uint8 / 31;
|
||||
|
||||
let height = 23 * percents;
|
||||
if(/* !height || */height < 2) {
|
||||
if(height < 2) {
|
||||
height = 2;
|
||||
}
|
||||
|
||||
@ -438,141 +530,149 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem
|
||||
|
||||
++index;
|
||||
}
|
||||
svg.insertAdjacentHTML('beforeend', h);
|
||||
svg.insertAdjacentHTML('beforeend', h); */
|
||||
|
||||
let progress = div.querySelector('.audio-waveform') as HTMLDivElement;
|
||||
|
||||
let onClick = () => {
|
||||
if(!promise) {
|
||||
if(!preloader) {
|
||||
preloader = new ProgressivePreloader(null, true);
|
||||
}
|
||||
|
||||
promise = appDocsManager.downloadDoc(doc.id);
|
||||
preloader.attach(downloadDiv, true, promise);
|
||||
|
||||
promise.then(blob => {
|
||||
downloadDiv.classList.remove('downloading');
|
||||
downloadDiv.remove();
|
||||
|
||||
let audio = document.createElement('audio');
|
||||
let source = document.createElement('source');
|
||||
source.src = doc.url;
|
||||
source.type = doc.mime_type;
|
||||
|
||||
audio.volume = 1;
|
||||
|
||||
div.removeEventListener('click', onClick);
|
||||
let toggle = div.querySelector('.audio-toggle') as HTMLDivElement;
|
||||
|
||||
let interval = 0;
|
||||
let lastIndex = 0;
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
if(audio.paused) {
|
||||
if(lastAudioToggle && lastAudioToggle.classList.contains('tgico-largepause')) {
|
||||
lastAudioToggle.click();
|
||||
}
|
||||
|
||||
audio.currentTime = 0;
|
||||
audio.play();
|
||||
|
||||
lastAudioToggle = toggle;
|
||||
|
||||
toggle.classList.remove('tgico-largeplay');
|
||||
toggle.classList.add('tgico-largepause');
|
||||
|
||||
(Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active'));
|
||||
|
||||
interval = setInterval(() => {
|
||||
if(lastIndex > svg.childElementCount || isNaN(audio.duration)) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
timeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true);
|
||||
|
||||
lastIndex = Math.round(audio.currentTime / audio.duration * 47);
|
||||
|
||||
//svg.children[lastIndex].setAttributeNS(null, 'fill', '#000');
|
||||
//svg.children[lastIndex].classList.add('active'); #Иногда пропускает полоски..
|
||||
(Array.from(svg.children) as HTMLElement[]).slice(0,lastIndex+1).forEach(node => node.classList.add('active'));
|
||||
//++lastIndex;
|
||||
//console.log('lastIndex:', lastIndex, audio.currentTime);
|
||||
//}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */);
|
||||
}, 20);
|
||||
} else {
|
||||
audio.pause();
|
||||
toggle.classList.add('tgico-largeplay');
|
||||
toggle.classList.remove('tgico-largepause');
|
||||
|
||||
clearInterval(interval);
|
||||
}
|
||||
});
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
toggle.classList.add('tgico-largeplay');
|
||||
toggle.classList.remove('tgico-largepause');
|
||||
clearInterval(interval);
|
||||
(Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active'));
|
||||
|
||||
timeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true);
|
||||
});
|
||||
|
||||
let mousedown = false, mousemove = false;
|
||||
progress.addEventListener('mouseleave', (e) => {
|
||||
if(mousedown) {
|
||||
audio.play();
|
||||
mousedown = false;
|
||||
}
|
||||
mousemove = false;
|
||||
})
|
||||
progress.addEventListener('mousemove', (e) => {
|
||||
mousemove = true;
|
||||
if(mousedown) scrub(e, audio, progress);
|
||||
});
|
||||
progress.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
if(!audio.paused) {
|
||||
audio.pause();
|
||||
scrub(e, audio, progress);
|
||||
mousedown = true;
|
||||
}
|
||||
});
|
||||
progress.addEventListener('mouseup', (e) => {
|
||||
if (mousemove && mousedown) {
|
||||
audio.play();
|
||||
mousedown = false;
|
||||
}
|
||||
});
|
||||
progress.addEventListener('click', (e) => {
|
||||
if(!audio.paused) scrub(e, audio, progress);
|
||||
});
|
||||
|
||||
function scrub(e: MouseEvent, audio: HTMLAudioElement, progress: HTMLDivElement) {
|
||||
let scrubTime = e.offsetX / 190 /* width */ * audio.duration;
|
||||
(Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active'));
|
||||
lastIndex = Math.round(scrubTime / audio.duration * 47);
|
||||
|
||||
(Array.from(svg.children) as HTMLElement[]).slice(0,lastIndex+1).forEach(node => node.classList.add('active'));
|
||||
audio.currentTime = scrubTime;
|
||||
let onLoad = () => {
|
||||
let audio = document.createElement('audio');
|
||||
let source = document.createElement('source');
|
||||
source.src = doc.url;
|
||||
//source.type = doc.mime_type;
|
||||
source.type = 'audio/wav';
|
||||
|
||||
audio.volume = 1;
|
||||
|
||||
let toggle = div.querySelector('.audio-toggle') as HTMLDivElement;
|
||||
|
||||
let interval = 0;
|
||||
let lastIndex = 0;
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
if(audio.paused) {
|
||||
if(lastAudioToggle && lastAudioToggle.classList.contains('tgico-largepause')) {
|
||||
lastAudioToggle.click();
|
||||
}
|
||||
|
||||
audio.style.display = 'none';
|
||||
audio.append(source);
|
||||
div.append(audio);
|
||||
});
|
||||
audio.currentTime = 0;
|
||||
audio.play();
|
||||
|
||||
lastAudioToggle = toggle;
|
||||
|
||||
toggle.classList.remove('tgico-largeplay');
|
||||
toggle.classList.add('tgico-largepause');
|
||||
|
||||
(Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active'));
|
||||
|
||||
interval = setInterval(() => {
|
||||
if(lastIndex > svg.childElementCount || isNaN(audio.duration)) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
timeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true);
|
||||
|
||||
lastIndex = Math.round(audio.currentTime / audio.duration * 47);
|
||||
|
||||
//svg.children[lastIndex].setAttributeNS(null, 'fill', '#000');
|
||||
//svg.children[lastIndex].classList.add('active'); #Иногда пропускает полоски..
|
||||
(Array.from(svg.children) as HTMLElement[]).slice(0,lastIndex+1).forEach(node => node.classList.add('active'));
|
||||
//++lastIndex;
|
||||
//console.log('lastIndex:', lastIndex, audio.currentTime);
|
||||
//}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */);
|
||||
}, 20);
|
||||
} else {
|
||||
audio.pause();
|
||||
toggle.classList.add('tgico-largeplay');
|
||||
toggle.classList.remove('tgico-largepause');
|
||||
|
||||
clearInterval(interval);
|
||||
}
|
||||
});
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
toggle.classList.add('tgico-largeplay');
|
||||
toggle.classList.remove('tgico-largepause');
|
||||
clearInterval(interval);
|
||||
(Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active'));
|
||||
|
||||
downloadDiv.classList.add('downloading');
|
||||
} else {
|
||||
downloadDiv.classList.remove('downloading');
|
||||
promise.cancel();
|
||||
promise = null;
|
||||
timeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true);
|
||||
});
|
||||
|
||||
let mousedown = false, mousemove = false;
|
||||
progress.addEventListener('mouseleave', (e) => {
|
||||
if(mousedown) {
|
||||
audio.play();
|
||||
mousedown = false;
|
||||
}
|
||||
mousemove = false;
|
||||
})
|
||||
progress.addEventListener('mousemove', (e) => {
|
||||
mousemove = true;
|
||||
if(mousedown) scrub(e, audio, progress);
|
||||
});
|
||||
progress.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
if(!audio.paused) {
|
||||
audio.pause();
|
||||
scrub(e, audio, progress);
|
||||
mousedown = true;
|
||||
}
|
||||
});
|
||||
progress.addEventListener('mouseup', (e) => {
|
||||
if (mousemove && mousedown) {
|
||||
audio.play();
|
||||
mousedown = false;
|
||||
}
|
||||
});
|
||||
progress.addEventListener('click', (e) => {
|
||||
if(!audio.paused) scrub(e, audio, progress);
|
||||
});
|
||||
|
||||
function scrub(e: MouseEvent, audio: HTMLAudioElement, progress: HTMLDivElement) {
|
||||
let scrubTime = e.offsetX / 190 /* width */ * audio.duration;
|
||||
(Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active'));
|
||||
lastIndex = Math.round(scrubTime / audio.duration * 47);
|
||||
|
||||
(Array.from(svg.children) as HTMLElement[]).slice(0,lastIndex+1).forEach(node => node.classList.add('active'));
|
||||
audio.currentTime = scrubTime;
|
||||
}
|
||||
|
||||
audio.style.display = 'none';
|
||||
audio.append(source);
|
||||
div.append(audio);
|
||||
};
|
||||
|
||||
div.addEventListener('click', onClick);
|
||||
div.click();
|
||||
|
||||
if(!uploading) {
|
||||
let onClick = () => {
|
||||
if(!promise) {
|
||||
if(!preloader) {
|
||||
preloader = new ProgressivePreloader(null, true);
|
||||
}
|
||||
|
||||
promise = appDocsManager.downloadDoc(doc.id);
|
||||
preloader.attach(downloadDiv, true, promise);
|
||||
|
||||
promise.then(() => {
|
||||
downloadDiv.classList.remove('downloading');
|
||||
downloadDiv.remove();
|
||||
div.removeEventListener('click', onClick);
|
||||
onLoad();
|
||||
});
|
||||
|
||||
downloadDiv.classList.add('downloading');
|
||||
} else {
|
||||
downloadDiv.classList.remove('downloading');
|
||||
promise.cancel();
|
||||
promise = null;
|
||||
}
|
||||
};
|
||||
|
||||
div.addEventListener('click', onClick);
|
||||
div.click();
|
||||
} else {
|
||||
onLoad();
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import apiFileManager from '../mtproto/apiFileManager';
|
||||
import FileManager from '../filemanager';
|
||||
import {RichTextProcessor} from '../richtextprocessor';
|
||||
import { CancellablePromise } from '../polyfill';
|
||||
import { CancellablePromise, deferredPromise } from '../polyfill';
|
||||
import { MTDocument } from '../../components/wrappers';
|
||||
import { isObject } from '../utils';
|
||||
import opusDecodeController from '../opusDecodeController';
|
||||
|
||||
class AppDocsManager {
|
||||
private docs: {[docID: string]: MTDocument} = {};
|
||||
@ -208,34 +209,49 @@ class AppDocsManager {
|
||||
|
||||
//historyDoc.progress = {enabled: !historyDoc.downloaded, percent: 1, total: doc.size};
|
||||
|
||||
let deferred = deferredPromise<Blob>();
|
||||
|
||||
deferred.cancel = () => {
|
||||
downloadPromise.cancel();
|
||||
};
|
||||
|
||||
// нет смысла делать объект с выполняющимися промисами, нижняя строка и так вернёт загружающийся
|
||||
let downloadPromise: CancellablePromise<Blob> = apiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, {
|
||||
let downloadPromise = apiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, {
|
||||
mimeType: doc.mime_type || 'application/octet-stream',
|
||||
toFileEntry: toFileEntry,
|
||||
stickerType: doc.sticker
|
||||
});
|
||||
|
||||
|
||||
downloadPromise.then((blob) => {
|
||||
if(blob) {
|
||||
doc.downloaded = true;
|
||||
|
||||
if(doc.type && doc.sticker != 2) {
|
||||
if(doc.type == 'voice'/* && false */) {
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onloadend = (e) => {
|
||||
let uint8 = new Uint8Array(e.target.result as ArrayBuffer);
|
||||
//console.log('sending uint8 to decoder:', uint8);
|
||||
opusDecodeController.decode(uint8).then(result => {
|
||||
doc.url = result.url;
|
||||
deferred.resolve(blob);
|
||||
}, deferred.reject);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(blob);
|
||||
|
||||
return;
|
||||
} else if(doc.type && doc.sticker != 2) {
|
||||
doc.url = URL.createObjectURL(blob);
|
||||
}
|
||||
}
|
||||
|
||||
/* doc.progress.percent = 100;
|
||||
setTimeout(() => {
|
||||
delete doc.progress;
|
||||
}, 0); */
|
||||
// console.log('file save done')
|
||||
|
||||
return blob;
|
||||
|
||||
deferred.resolve(blob);
|
||||
}, (e) => {
|
||||
console.log('document download failed', e);
|
||||
//historyDoc.progress.enabled = false;
|
||||
});
|
||||
|
||||
|
||||
/* downloadPromise.notify = (progress) => {
|
||||
console.log('dl progress', progress);
|
||||
historyDoc.progress.enabled = true;
|
||||
@ -248,7 +264,7 @@ class AppDocsManager {
|
||||
|
||||
//console.log('return downloadPromise:', downloadPromise);
|
||||
|
||||
return downloadPromise;
|
||||
return deferred;
|
||||
}
|
||||
|
||||
public downloadDocThumb(docID: any, thumbSize: string) {
|
||||
|
@ -1737,11 +1737,18 @@ export class AppImManager {
|
||||
}
|
||||
|
||||
case 'audio':
|
||||
case 'voice':
|
||||
case 'document': {
|
||||
let docDiv = wrapDocument(pending, false, true);
|
||||
let doc = appDocsManager.getDoc(message.id);
|
||||
this.log('will wrap pending doc:', doc);
|
||||
let docDiv = wrapDocument(doc, false, true);
|
||||
|
||||
let icoDiv = docDiv.querySelector('.document-ico');
|
||||
let icoDiv = docDiv.querySelector('.audio-download, .document-ico');
|
||||
preloader.attach(icoDiv, false);
|
||||
|
||||
if(pending.type == 'voice') {
|
||||
bubble.classList.add('bubble-audio');
|
||||
}
|
||||
|
||||
bubble.classList.remove('is-message-empty');
|
||||
messageDiv.classList.add((pending.type || 'document') + '-message');
|
||||
|
@ -597,9 +597,11 @@ export class AppMessagesManager {
|
||||
height: number,
|
||||
objectURL: string,
|
||||
isRoundMessage: boolean,
|
||||
isVoiceMessage: boolean,
|
||||
duration: number,
|
||||
background: boolean
|
||||
background: boolean,
|
||||
|
||||
isVoiceMessage: boolean,
|
||||
waveform: Uint8Array
|
||||
}> = {}) {
|
||||
peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID;
|
||||
var messageID = this.tempID--;
|
||||
@ -621,6 +623,8 @@ export class AppMessagesManager {
|
||||
|
||||
let date = tsNow(true) + ServerTimeManager.serverTimeOffset;
|
||||
|
||||
console.log('sendFile', file, fileType);
|
||||
|
||||
if(caption) {
|
||||
let entities = options.entities || [];
|
||||
caption = RichTextProcessor.parseMarkdown(caption, entities);
|
||||
@ -667,6 +671,7 @@ export class AppMessagesManager {
|
||||
if(options.isVoiceMessage) {
|
||||
flags |= 1 << 10;
|
||||
flags |= 1 << 2;
|
||||
attachType = 'voice';
|
||||
}
|
||||
|
||||
let attribute = {
|
||||
@ -674,10 +679,10 @@ export class AppMessagesManager {
|
||||
flags: flags,
|
||||
pFlags: { // that's only for client, not going to telegram
|
||||
voice: options.isVoiceMessage
|
||||
},
|
||||
waveform: new Uint8Array([0, 0, 0, 0, 0, 0, 128, 35, 8, 25, 34, 132, 16, 66, 8, 0, 0, 0, 0, 0, 0, 0, 96, 60, 254, 255, 255, 79, 223, 255, 63, 183, 226, 107, 255, 255, 255, 255, 191, 188, 255, 255, 246, 255, 255, 255, 255, 63, 155, 117, 135, 24, 249, 191, 167, 51, 149, 0, 0, 0, 0, 0, 0]),
|
||||
},
|
||||
waveform: options.waveform,
|
||||
voice: options.isVoiceMessage,
|
||||
duration: options.duration || 0,
|
||||
duration: options.duration || 0
|
||||
};
|
||||
|
||||
attributes.push(attribute);
|
||||
@ -703,7 +708,15 @@ export class AppMessagesManager {
|
||||
};
|
||||
|
||||
attributes.push(videoAttribute);
|
||||
} else {
|
||||
attachType = 'document';
|
||||
apiFileName = 'document.' + fileType.split('/')[1];
|
||||
actionName = 'sendMessageUploadDocumentAction';
|
||||
}
|
||||
|
||||
attributes.push({_: 'documentAttributeFilename', file_name: fileName || apiFileName});
|
||||
|
||||
if(['document', 'video', 'audio', 'voice'].indexOf(attachType) !== -1) {
|
||||
let doc: any = {
|
||||
_: 'document',
|
||||
id: '' + messageID,
|
||||
@ -719,10 +732,6 @@ export class AppMessagesManager {
|
||||
};
|
||||
|
||||
appDocsManager.saveDoc(doc);
|
||||
} else {
|
||||
attachType = 'document';
|
||||
apiFileName = 'document.' + fileType.split('/')[1];
|
||||
actionName = 'sendMessageUploadDocumentAction';
|
||||
}
|
||||
|
||||
console.log('AMM: sendFile', attachType, apiFileName, file.type, options);
|
||||
@ -769,8 +778,6 @@ export class AppMessagesManager {
|
||||
}
|
||||
};
|
||||
|
||||
attributes.push({_: 'documentAttributeFilename', file_name: media.file_name});
|
||||
|
||||
preloader.preloader.onclick = () => {
|
||||
console.log('cancelling upload', media);
|
||||
appImManager.setTyping('sendMessageCancelAction');
|
||||
@ -786,12 +793,12 @@ export class AppMessagesManager {
|
||||
pFlags: pFlags,
|
||||
date: date,
|
||||
message: caption,
|
||||
media: isDocument ? {
|
||||
media: /* isDocument ? {
|
||||
_: 'messageMediaDocument',
|
||||
pFlags: {},
|
||||
flags: 1,
|
||||
document: file
|
||||
} : media,
|
||||
} : */media,
|
||||
random_id: randomIDS,
|
||||
reply_to_msg_id: replyToMsgID,
|
||||
views: asChannel && 1,
|
||||
|
@ -413,8 +413,8 @@ export class ApiFileManager {
|
||||
limit: limit
|
||||
}, {
|
||||
dcID: dcID,
|
||||
fileDownload: true,
|
||||
singleInRequest: 'safari' in window
|
||||
fileDownload: true/* ,
|
||||
singleInRequest: 'safari' in window */
|
||||
});
|
||||
}, dcID).then((result: any) => {
|
||||
writeFilePromise.then(() => {
|
||||
|
134
src/lib/opusDecodeController.ts
Normal file
134
src/lib/opusDecodeController.ts
Normal file
@ -0,0 +1,134 @@
|
||||
type Result = {
|
||||
bytes: Uint8Array,
|
||||
waveform?: Uint8Array
|
||||
};
|
||||
|
||||
type Task = {
|
||||
pages: Uint8Array,
|
||||
withWaveform: boolean,
|
||||
waveform?: Uint8Array,
|
||||
callback: {resolve: (result: Result) => void, reject: (err: Error) => void}
|
||||
};
|
||||
|
||||
export class OpusDecodeController {
|
||||
private worker: Worker;
|
||||
private wavWorker : Worker;
|
||||
private sampleRate = 48000;
|
||||
private tasks: Array<Task> = [];
|
||||
private keepAlive = false;
|
||||
|
||||
public loadWavWorker() {
|
||||
if(this.wavWorker) return;
|
||||
|
||||
this.wavWorker = new Worker('waveWorker.min.js');
|
||||
this.wavWorker.addEventListener('message', (e) => {
|
||||
const data = e.data;
|
||||
|
||||
if(data && data.page) {
|
||||
const bytes = data.page;
|
||||
this.onTaskEnd(this.tasks.shift(), bytes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public loadWorker() {
|
||||
if(this.worker) return;
|
||||
|
||||
this.worker = new Worker('decoderWorker.min.js');
|
||||
this.worker.addEventListener('message', (e) => {
|
||||
const data = e.data;
|
||||
|
||||
if(data.type == 'done') {
|
||||
this.wavWorker.postMessage({command: 'done'});
|
||||
|
||||
if(data.waveform) {
|
||||
this.tasks[0].waveform = data.waveform;
|
||||
}
|
||||
} else { // e.data contains decoded buffers as float32 values
|
||||
this.wavWorker.postMessage({
|
||||
command: 'encode',
|
||||
buffers: e.data
|
||||
}, data.map((typedArray: Uint8Array) => typedArray.buffer));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public setKeepAlive(keepAlive: boolean) {
|
||||
this.keepAlive = keepAlive;
|
||||
if(this.keepAlive) {
|
||||
this.loadWorker();
|
||||
this.loadWavWorker();
|
||||
} else if(!this.tasks.length) {
|
||||
this.terminateWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
public onTaskEnd(task: Task, result: Uint8Array) {
|
||||
task.callback.resolve({bytes: result, waveform: task.waveform});
|
||||
|
||||
if(this.tasks.length) {
|
||||
this.executeNewTask(this.tasks[0]);
|
||||
}
|
||||
|
||||
this.terminateWorkers();
|
||||
}
|
||||
|
||||
public terminateWorkers() {
|
||||
if(this.keepAlive || this.tasks.length) return;
|
||||
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
||||
|
||||
this.wavWorker.terminate();
|
||||
this.wavWorker = null;
|
||||
}
|
||||
|
||||
public executeNewTask(task: Task) {
|
||||
this.worker.postMessage({
|
||||
command: 'init',
|
||||
decoderSampleRate: this.sampleRate,
|
||||
outputBufferSampleRate: this.sampleRate
|
||||
});
|
||||
|
||||
this.wavWorker.postMessage({
|
||||
command: 'init',
|
||||
wavBitDepth: 16,
|
||||
wavSampleRate: this.sampleRate
|
||||
});
|
||||
|
||||
//console.log('sending command to worker:', task);
|
||||
//setTimeout(() => {
|
||||
this.worker.postMessage({
|
||||
command: 'decode',
|
||||
pages: task.pages,
|
||||
waveform: task.withWaveform
|
||||
}, [task.pages.buffer]);
|
||||
//}, 1e3);
|
||||
}
|
||||
|
||||
public pushDecodeTask(pages: Uint8Array, withWaveform: boolean) {
|
||||
return new Promise<Result>((resolve, reject) => {
|
||||
const task = {
|
||||
pages,
|
||||
withWaveform,
|
||||
callback: {resolve, reject}
|
||||
};
|
||||
|
||||
this.loadWorker();
|
||||
this.loadWavWorker();
|
||||
|
||||
if(this.tasks.push(task) == 1) {
|
||||
this.executeNewTask(task);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async decode(typedArray: Uint8Array, withWaveform = false) {
|
||||
return this.pushDecodeTask(typedArray, withWaveform).then(result => {
|
||||
const dataBlob = new Blob([result.bytes], {type: "audio/wav"});
|
||||
return {url: URL.createObjectURL(dataBlob), waveform: result.waveform};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new OpusDecodeController();
|
@ -1,5 +0,0 @@
|
||||
export class OpusProcessor {
|
||||
|
||||
}
|
||||
|
||||
export default new OpusProcessor();
|
@ -107,12 +107,18 @@ $time-background: rgba(0, 0, 0, .35);
|
||||
opacity: 0;
|
||||
transition: width .1s .1s, margin-right .1s .1s, visibility 0s .1s, opacity .1s 0s;
|
||||
padding: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.btn-send-container {
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
align-self: flex-end;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#btn-send {
|
||||
flex: 0 0 auto;
|
||||
color: #9e9e9e;
|
||||
align-self: flex-end;
|
||||
|
||||
&.tgico-send {
|
||||
color: $color-blue;
|
||||
@ -141,6 +147,19 @@ $time-background: rgba(0, 0, 0, .35);
|
||||
animation: recordBlink 1.25s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.record-ripple {
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, .2);
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
transform: scale(0);
|
||||
position: absolute;
|
||||
top: -48px;
|
||||
left: -48px;
|
||||
transition: transform .03s, visibility .1s;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&.is-recording {
|
||||
#btn-record-cancel {
|
||||
@ -157,6 +176,11 @@ $time-background: rgba(0, 0, 0, .35);
|
||||
.record-time {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.record-ripple {
|
||||
transition: transform .03s, visibility 0s;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.is-recording) {
|
||||
@ -249,6 +273,7 @@ $time-background: rgba(0, 0, 0, .35);
|
||||
caret-color: $button-primary-background;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
|
@ -1253,6 +1253,10 @@
|
||||
&-toggle, &-download {
|
||||
background-color: #4FAE4E;
|
||||
}
|
||||
|
||||
&-download:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.photo, &.video:not(.round) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user