Browse Source

Completed task #3:

Recording voice messages:
1) Record and play on Safari
2) Ripple effect
3) Encoding and decoding new waveform
master
morethanwords 5 years ago
parent
commit
ca4946f32e
  1. 60
      src/components/chatInput.ts
  2. 146
      src/components/wrappers.ts
  3. 38
      src/lib/appManagers/appDocsManager.ts
  4. 11
      src/lib/appManagers/appImManager.ts
  5. 31
      src/lib/appManagers/appMessagesManager.ts
  6. 4
      src/lib/mtproto/apiFileManager.ts
  7. 134
      src/lib/opusDecodeController.ts
  8. 5
      src/lib/opusProcessor.ts
  9. 29
      src/scss/partials/_chat.scss
  10. 4
      src/scss/partials/_chatBubble.scss

60
src/components/chatInput.ts

@ -11,7 +11,9 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager";
import initEmoticonsDropdown, { EMOTICONSSTICKERGROUP } from "./emoticonsDropdown"; import initEmoticonsDropdown, { EMOTICONSSTICKERGROUP } from "./emoticonsDropdown";
import lottieLoader from "../lib/lottieLoader"; import lottieLoader from "../lib/lottieLoader";
import { Layouter, RectPart } from "./groupedLayout"; 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 { export class ChatInput {
public pageEl = document.getElementById('page-chats') as HTMLDivElement; 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 inputMessageContainer = document.getElementsByClassName('input-message-container')[0] as HTMLDivElement;
public inputScroll = new Scrollable(this.inputMessageContainer); public inputScroll = new Scrollable(this.inputMessageContainer);
public btnSend = document.getElementById('btn-send') as HTMLButtonElement; 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 emoticonsDropdown: HTMLDivElement = null;
public emoticonsTimeout: number = 0; public emoticonsTimeout: number = 0;
public toggleEmoticons: HTMLButtonElement; public toggleEmoticons: HTMLButtonElement;
@ -28,7 +30,7 @@ export class ChatInput {
public lastUrl = ''; public lastUrl = '';
public lastTimeType = 0; public lastTimeType = 0;
private inputContainer = this.btnSend.parentElement as HTMLDivElement; private inputContainer = this.btnSend.parentElement.parentElement as HTMLDivElement;
public attachMenu: { public attachMenu: {
container?: HTMLButtonElement, container?: HTMLButtonElement,
@ -68,6 +70,8 @@ export class ChatInput {
private recording = false; private recording = false;
private recordCanceled = false; private recordCanceled = false;
private recordTimeEl = this.inputContainer.querySelector('.record-time') as HTMLDivElement; private recordTimeEl = this.inputContainer.querySelector('.record-time') as HTMLDivElement;
private recordRippleEl = this.inputContainer.querySelector('.record-ripple') as HTMLDivElement;
private recordStartTime = 0;
constructor() { constructor() {
this.toggleEmoticons = this.pageEl.querySelector('.toggle-emoticons') as HTMLButtonElement; this.toggleEmoticons = this.pageEl.querySelector('.toggle-emoticons') as HTMLButtonElement;
@ -470,12 +474,37 @@ export class ChatInput {
this.btnSend.classList.add('tgico-send'); this.btnSend.classList.add('tgico-send');
this.inputContainer.classList.add('is-recording'); this.inputContainer.classList.add('is-recording');
this.recording = true; 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 = () => { let r = () => {
if(!this.recording) return; 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 ms = diff % 1000;
let formatted = ('' + (diff / 1000)).toHHMMSS() + ',' + ('00' + Math.round(ms / 10)).slice(-2); let formatted = ('' + (diff / 1000)).toHHMMSS() + ',' + ('00' + Math.round(ms / 10)).slice(-2);
@ -495,20 +524,23 @@ export class ChatInput {
this.btnCancelRecord.addEventListener('click', () => { this.btnCancelRecord.addEventListener('click', () => {
this.recordCanceled = true; this.recordCanceled = true;
this.recorder.stop(); this.recorder.stop();
opusDecodeController.setKeepAlive(false);
}); });
this.recorder.onstop = () => { this.recorder.onstop = () => {
this.recording = false; this.recording = false;
this.inputContainer.classList.remove('is-recording'); this.inputContainer.classList.remove('is-recording');
this.btnSend.classList.remove('tgico-send'); this.btnSend.classList.remove('tgico-send');
this.recordRippleEl.style.transform = '';
}; };
this.recorder.ondataavailable = (typedArray: Uint8Array) => { this.recorder.ondataavailable = (typedArray: Uint8Array) => {
if(this.recordCanceled) return; if(this.recordCanceled) return;
const duration = (Date.now() - this.recordStartTime) / 1000 | 0;
const dataBlob = new Blob([typedArray], {type: 'audio/ogg'}); const dataBlob = new Blob([typedArray], {type: 'audio/ogg'});
const fileName = new Date().toISOString() + ".opus"; /* const fileName = new Date().toISOString() + ".opus";
console.log('Recorder data received', typedArray, dataBlob); console.log('Recorder data received', typedArray, dataBlob); */
/* var url = URL.createObjectURL( dataBlob ); /* var url = URL.createObjectURL( dataBlob );
@ -529,11 +561,21 @@ export class ChatInput {
return; */ return; */
let perf = performance.now();
opusDecodeController.decode(typedArray, true).then(result => {
console.log('WAVEFORM!:', /* waveform, */performance.now() - perf);
opusDecodeController.setKeepAlive(false);
let peerID = appImManager.peerID; let peerID = appImManager.peerID;
// тут objectURL ставится уже с audio/wav
appMessagesManager.sendFile(peerID, dataBlob, { appMessagesManager.sendFile(peerID, dataBlob, {
isVoiceMessage: true, isVoiceMessage: true,
duration: 0, isMedia: true,
isMedia: true duration,
waveform: result.waveform,
objectURL: result.url
});
}); });
/* const url = URL.createObjectURL(dataBlob); /* const url = URL.createObjectURL(dataBlob);

146
src/components/wrappers.ts

@ -192,7 +192,7 @@ let formatDate = (timestamp: number) => {
export function wrapDocument(doc: MTDocument, withTime = false, uploading = false): HTMLDivElement { export function wrapDocument(doc: MTDocument, withTime = false, uploading = false): HTMLDivElement {
if(doc.type == 'voice') { if(doc.type == 'voice') {
return wrapVoiceMessage(doc, withTime); return wrapVoiceMessage(doc, uploading);
} else if(doc.type == 'audio') { } else if(doc.type == 'audio') {
return wrapAudio(doc, withTime); return wrapAudio(doc, withTime);
} }
@ -384,8 +384,42 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement {
return div; 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; 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'); let div = document.createElement('div');
div.classList.add('audio', 'is-voice'); div.classList.add('audio', 'is-voice');
@ -395,7 +429,7 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem
div.innerHTML = ` div.innerHTML = `
<div class="audio-toggle audio-ico tgico-largeplay"></div> <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> <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'); svg.setAttributeNS(null, 'viewBox', '0 0 190 23');
div.insertBefore(svg, div.lastElementChild); div.insertBefore(svg, div.lastElementChild);
let wave = doc.attributes[0].waveform as Uint8Array;
let index = 0; 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 skipped = 0; let skipped = 0;
let h = ''; let h = '';
for(let uint8 of wave) { for(let uint8 of wave) {
@ -425,10 +516,11 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem
++skipped; ++skipped;
continue; continue;
} }
let percents = uint8 / 255; //let percents = uint8 / 255;
let percents = uint8 / 31;
let height = 23 * percents; let height = 23 * percents;
if(/* !height || */height < 2) { if(height < 2) {
height = 2; height = 2;
} }
@ -438,31 +530,19 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem
++index; ++index;
} }
svg.insertAdjacentHTML('beforeend', h); svg.insertAdjacentHTML('beforeend', h); */
let progress = div.querySelector('.audio-waveform') as HTMLDivElement; let progress = div.querySelector('.audio-waveform') as HTMLDivElement;
let onClick = () => { let onLoad = () => {
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 audio = document.createElement('audio');
let source = document.createElement('source'); let source = document.createElement('source');
source.src = doc.url; source.src = doc.url;
source.type = doc.mime_type; //source.type = doc.mime_type;
source.type = 'audio/wav';
audio.volume = 1; audio.volume = 1;
div.removeEventListener('click', onClick);
let toggle = div.querySelector('.audio-toggle') as HTMLDivElement; let toggle = div.querySelector('.audio-toggle') as HTMLDivElement;
let interval = 0; let interval = 0;
@ -561,6 +641,23 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem
audio.style.display = 'none'; audio.style.display = 'none';
audio.append(source); audio.append(source);
div.append(audio); div.append(audio);
};
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'); downloadDiv.classList.add('downloading');
@ -573,6 +670,9 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem
div.addEventListener('click', onClick); div.addEventListener('click', onClick);
div.click(); div.click();
} else {
onLoad();
}
return div; return div;
} }

38
src/lib/appManagers/appDocsManager.ts

@ -1,9 +1,10 @@
import apiFileManager from '../mtproto/apiFileManager'; import apiFileManager from '../mtproto/apiFileManager';
import FileManager from '../filemanager'; import FileManager from '../filemanager';
import {RichTextProcessor} from '../richtextprocessor'; import {RichTextProcessor} from '../richtextprocessor';
import { CancellablePromise } from '../polyfill'; import { CancellablePromise, deferredPromise } from '../polyfill';
import { MTDocument } from '../../components/wrappers'; import { MTDocument } from '../../components/wrappers';
import { isObject } from '../utils'; import { isObject } from '../utils';
import opusDecodeController from '../opusDecodeController';
class AppDocsManager { class AppDocsManager {
private docs: {[docID: string]: MTDocument} = {}; private docs: {[docID: string]: MTDocument} = {};
@ -208,8 +209,14 @@ class AppDocsManager {
//historyDoc.progress = {enabled: !historyDoc.downloaded, percent: 1, total: doc.size}; //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', mimeType: doc.mime_type || 'application/octet-stream',
toFileEntry: toFileEntry, toFileEntry: toFileEntry,
stickerType: doc.sticker stickerType: doc.sticker
@ -219,18 +226,27 @@ class AppDocsManager {
if(blob) { if(blob) {
doc.downloaded = true; 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.url = URL.createObjectURL(blob);
} }
} }
/* doc.progress.percent = 100; deferred.resolve(blob);
setTimeout(() => {
delete doc.progress;
}, 0); */
// console.log('file save done')
return blob;
}, (e) => { }, (e) => {
console.log('document download failed', e); console.log('document download failed', e);
//historyDoc.progress.enabled = false; //historyDoc.progress.enabled = false;
@ -248,7 +264,7 @@ class AppDocsManager {
//console.log('return downloadPromise:', downloadPromise); //console.log('return downloadPromise:', downloadPromise);
return downloadPromise; return deferred;
} }
public downloadDocThumb(docID: any, thumbSize: string) { public downloadDocThumb(docID: any, thumbSize: string) {

11
src/lib/appManagers/appImManager.ts

@ -1737,12 +1737,19 @@ export class AppImManager {
} }
case 'audio': case 'audio':
case 'voice':
case 'document': { 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); preloader.attach(icoDiv, false);
if(pending.type == 'voice') {
bubble.classList.add('bubble-audio');
}
bubble.classList.remove('is-message-empty'); bubble.classList.remove('is-message-empty');
messageDiv.classList.add((pending.type || 'document') + '-message'); messageDiv.classList.add((pending.type || 'document') + '-message');
messageDiv.append(docDiv); messageDiv.append(docDiv);

31
src/lib/appManagers/appMessagesManager.ts

@ -597,9 +597,11 @@ export class AppMessagesManager {
height: number, height: number,
objectURL: string, objectURL: string,
isRoundMessage: boolean, isRoundMessage: boolean,
isVoiceMessage: boolean,
duration: number, duration: number,
background: boolean background: boolean,
isVoiceMessage: boolean,
waveform: Uint8Array
}> = {}) { }> = {}) {
peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID; peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID;
var messageID = this.tempID--; var messageID = this.tempID--;
@ -621,6 +623,8 @@ export class AppMessagesManager {
let date = tsNow(true) + ServerTimeManager.serverTimeOffset; let date = tsNow(true) + ServerTimeManager.serverTimeOffset;
console.log('sendFile', file, fileType);
if(caption) { if(caption) {
let entities = options.entities || []; let entities = options.entities || [];
caption = RichTextProcessor.parseMarkdown(caption, entities); caption = RichTextProcessor.parseMarkdown(caption, entities);
@ -667,6 +671,7 @@ export class AppMessagesManager {
if(options.isVoiceMessage) { if(options.isVoiceMessage) {
flags |= 1 << 10; flags |= 1 << 10;
flags |= 1 << 2; flags |= 1 << 2;
attachType = 'voice';
} }
let attribute = { let attribute = {
@ -675,9 +680,9 @@ export class AppMessagesManager {
pFlags: { // that's only for client, not going to telegram pFlags: { // that's only for client, not going to telegram
voice: options.isVoiceMessage 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, voice: options.isVoiceMessage,
duration: options.duration || 0, duration: options.duration || 0
}; };
attributes.push(attribute); attributes.push(attribute);
@ -703,7 +708,15 @@ export class AppMessagesManager {
}; };
attributes.push(videoAttribute); 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 = { let doc: any = {
_: 'document', _: 'document',
id: '' + messageID, id: '' + messageID,
@ -719,10 +732,6 @@ export class AppMessagesManager {
}; };
appDocsManager.saveDoc(doc); appDocsManager.saveDoc(doc);
} else {
attachType = 'document';
apiFileName = 'document.' + fileType.split('/')[1];
actionName = 'sendMessageUploadDocumentAction';
} }
console.log('AMM: sendFile', attachType, apiFileName, file.type, options); 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 = () => { preloader.preloader.onclick = () => {
console.log('cancelling upload', media); console.log('cancelling upload', media);
appImManager.setTyping('sendMessageCancelAction'); appImManager.setTyping('sendMessageCancelAction');
@ -786,12 +793,12 @@ export class AppMessagesManager {
pFlags: pFlags, pFlags: pFlags,
date: date, date: date,
message: caption, message: caption,
media: isDocument ? { media: /* isDocument ? {
_: 'messageMediaDocument', _: 'messageMediaDocument',
pFlags: {}, pFlags: {},
flags: 1, flags: 1,
document: file document: file
} : media, } : */media,
random_id: randomIDS, random_id: randomIDS,
reply_to_msg_id: replyToMsgID, reply_to_msg_id: replyToMsgID,
views: asChannel && 1, views: asChannel && 1,

4
src/lib/mtproto/apiFileManager.ts

@ -413,8 +413,8 @@ export class ApiFileManager {
limit: limit limit: limit
}, { }, {
dcID: dcID, dcID: dcID,
fileDownload: true, fileDownload: true/* ,
singleInRequest: 'safari' in window singleInRequest: 'safari' in window */
}); });
}, dcID).then((result: any) => { }, dcID).then((result: any) => {
writeFilePromise.then(() => { writeFilePromise.then(() => {

134
src/lib/opusDecodeController.ts

@ -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();

5
src/lib/opusProcessor.ts

@ -1,5 +0,0 @@
export class OpusProcessor {
}
export default new OpusProcessor();

29
src/scss/partials/_chat.scss

@ -107,12 +107,18 @@ $time-background: rgba(0, 0, 0, .35);
opacity: 0; opacity: 0;
transition: width .1s .1s, margin-right .1s .1s, visibility 0s .1s, opacity .1s 0s; transition: width .1s .1s, margin-right .1s .1s, visibility 0s .1s, opacity .1s 0s;
padding: 0; padding: 0;
z-index: 3;
} }
#btn-send { .btn-send-container {
flex: 0 0 auto; flex: 0 0 auto;
color: #9e9e9e; position: relative;
align-self: flex-end; align-self: flex-end;
z-index: 2;
}
#btn-send {
color: #9e9e9e;
&.tgico-send { &.tgico-send {
color: $color-blue; color: $color-blue;
@ -142,6 +148,19 @@ $time-background: rgba(0, 0, 0, .35);
} }
} }
.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 { &.is-recording {
#btn-record-cancel { #btn-record-cancel {
opacity: 1; opacity: 1;
@ -157,6 +176,11 @@ $time-background: rgba(0, 0, 0, .35);
.record-time { .record-time {
display: block; display: block;
} }
.record-ripple {
transition: transform .03s, visibility 0s;
visibility: visible;
}
} }
&:not(.is-recording) { &:not(.is-recording) {
@ -249,6 +273,7 @@ $time-background: rgba(0, 0, 0, .35);
caret-color: $button-primary-background; caret-color: $button-primary-background;
flex: 1; flex: 1;
position: relative; position: relative;
z-index: 3;
&:after { &:after {
position: absolute; position: absolute;

4
src/scss/partials/_chatBubble.scss

@ -1253,6 +1253,10 @@
&-toggle, &-download { &-toggle, &-download {
background-color: #4FAE4E; background-color: #4FAE4E;
} }
&-download:empty {
display: none;
}
} }
&.photo, &.video:not(.round) { &.photo, &.video:not(.round) {

Loading…
Cancel
Save