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:
morethanwords 2020-06-02 01:19:35 +03:00
parent 787c845c68
commit 0782b18cb0
15 changed files with 521 additions and 189 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -542,14 +542,15 @@
<div class="btn-menu-item menu-poll tgico-poll rp">Poll</div>
</div>
</button>
<div class="record-time">
0:02,43
</div>
<div class="record-time"></div>
<input type="file" id="input-file" style="display: none;" multiple />
</div>
</div>
<button class="btn-circle z-depth-1 btn-icon tgico-delete danger" id="btn-record-cancel"></button>
<button class="btn-circle z-depth-1 btn-icon tgico-microphone2" id="btn-send"></button>
<div class="btn-send-container">
<div class="record-ripple"></div>
<button class="btn-circle z-depth-1 btn-icon tgico-microphone2" id="btn-send"></button>
</div>
</div>
</div>
<div class="sidebar sidebar-right" id="column-right">

1
public/recorder.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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;
}

View File

@ -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) {

View File

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

View File

@ -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,

View File

@ -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(() => {

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

View File

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

View File

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

View File

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