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 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 peerID = appImManager.peerID;
|
let perf = performance.now();
|
||||||
appMessagesManager.sendFile(peerID, dataBlob, {
|
opusDecodeController.decode(typedArray, true).then(result => {
|
||||||
isVoiceMessage: true,
|
console.log('WAVEFORM!:', /* waveform, */performance.now() - perf);
|
||||||
duration: 0,
|
|
||||||
isMedia: true
|
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);
|
/* const url = URL.createObjectURL(dataBlob);
|
||||||
|
@ -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;
|
|
||||||
|
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 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,141 +530,149 @@ 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 = () => {
|
|
||||||
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);
|
let onLoad = () => {
|
||||||
|
let audio = document.createElement('audio');
|
||||||
lastIndex = Math.round(audio.currentTime / audio.duration * 47);
|
let source = document.createElement('source');
|
||||||
|
source.src = doc.url;
|
||||||
//svg.children[lastIndex].setAttributeNS(null, 'fill', '#000');
|
//source.type = doc.mime_type;
|
||||||
//svg.children[lastIndex].classList.add('active'); #Иногда пропускает полоски..
|
source.type = 'audio/wav';
|
||||||
(Array.from(svg.children) as HTMLElement[]).slice(0,lastIndex+1).forEach(node => node.classList.add('active'));
|
|
||||||
//++lastIndex;
|
audio.volume = 1;
|
||||||
//console.log('lastIndex:', lastIndex, audio.currentTime);
|
|
||||||
//}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */);
|
let toggle = div.querySelector('.audio-toggle') as HTMLDivElement;
|
||||||
}, 20);
|
|
||||||
} else {
|
let interval = 0;
|
||||||
audio.pause();
|
let lastIndex = 0;
|
||||||
toggle.classList.add('tgico-largeplay');
|
|
||||||
toggle.classList.remove('tgico-largepause');
|
toggle.addEventListener('click', () => {
|
||||||
|
if(audio.paused) {
|
||||||
clearInterval(interval);
|
if(lastAudioToggle && lastAudioToggle.classList.contains('tgico-largepause')) {
|
||||||
}
|
lastAudioToggle.click();
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
audio.style.display = 'none';
|
audio.currentTime = 0;
|
||||||
audio.append(source);
|
audio.play();
|
||||||
div.append(audio);
|
|
||||||
});
|
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');
|
timeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true);
|
||||||
} else {
|
});
|
||||||
downloadDiv.classList.remove('downloading');
|
|
||||||
promise.cancel();
|
let mousedown = false, mousemove = false;
|
||||||
promise = null;
|
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);
|
if(!uploading) {
|
||||||
div.click();
|
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;
|
return div;
|
||||||
}
|
}
|
||||||
|
@ -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,34 +209,49 @@ 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
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadPromise.then((blob) => {
|
downloadPromise.then((blob) => {
|
||||||
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;
|
||||||
});
|
});
|
||||||
|
|
||||||
/* downloadPromise.notify = (progress) => {
|
/* downloadPromise.notify = (progress) => {
|
||||||
console.log('dl progress', progress);
|
console.log('dl progress', progress);
|
||||||
historyDoc.progress.enabled = true;
|
historyDoc.progress.enabled = true;
|
||||||
@ -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) {
|
||||||
|
@ -1737,11 +1737,18 @@ 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');
|
||||||
|
@ -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 = {
|
||||||
@ -674,10 +679,10 @@ export class AppMessagesManager {
|
|||||||
flags: flags,
|
flags: flags,
|
||||||
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,
|
||||||
|
@ -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
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;
|
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-container {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
|
align-self: flex-end;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#btn-send {
|
#btn-send {
|
||||||
flex: 0 0 auto;
|
|
||||||
color: #9e9e9e;
|
color: #9e9e9e;
|
||||||
align-self: flex-end;
|
|
||||||
|
|
||||||
&.tgico-send {
|
&.tgico-send {
|
||||||
color: $color-blue;
|
color: $color-blue;
|
||||||
@ -141,6 +147,19 @@ $time-background: rgba(0, 0, 0, .35);
|
|||||||
animation: recordBlink 1.25s infinite;
|
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 {
|
&.is-recording {
|
||||||
#btn-record-cancel {
|
#btn-record-cancel {
|
||||||
@ -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;
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user