audio messages are fully functional but buggy and ugly
This commit is contained in:
+13
-2
@@ -2672,12 +2672,23 @@ ul.tabbar li.active > a {
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-player canvas.visualiser {
|
.audio-player .visualiser {
|
||||||
display: block;
|
|
||||||
width: 12rem;
|
width: 12rem;
|
||||||
height: 2.6rem;
|
height: 2.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-player canvas.visualiser {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player div.visualiser {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #757575;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
.audio-player .timer {
|
.audio-player .timer {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
float: right;
|
float: right;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -281,3 +281,27 @@ export function base64ToBlob(str, mime) {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function intArrayToBase64(arr) {
|
||||||
|
if (!Array.isArray(arr)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let bin = '';
|
||||||
|
new Uint8Array(arr).forEach(b => bin += String.fromCharCode(b));
|
||||||
|
return window.btoa(bin);
|
||||||
|
} catch (err) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64ToIntArray(b64) {
|
||||||
|
const arr = [];
|
||||||
|
try {
|
||||||
|
const bin = window.atob(b64);
|
||||||
|
[...bin].forEach(c => {
|
||||||
|
arr.push(c.charCodeAt(0));
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
} catch (err) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ export function fullFormatter(style, data, values, key, stack) {
|
|||||||
// Show audio player.
|
// Show audio player.
|
||||||
if (attr.src) {
|
if (attr.src) {
|
||||||
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'audio'));
|
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'audio'));
|
||||||
|
attr.duration = data.duration > 0 ? (data.duration | 0) : undefined;
|
||||||
|
attr.preview = data.preview;
|
||||||
attr.loading = 'lazy';
|
attr.loading = 'lazy';
|
||||||
}
|
}
|
||||||
el = AudioPlayer;
|
el = AudioPlayer;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import SendMessage from '../widgets/send-message.jsx';
|
|||||||
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
|
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
|
||||||
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
|
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
|
||||||
READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
|
READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
|
||||||
import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
|
import { blobToBase64, fileToBase64, imageScaled, intArrayToBase64, makeImageUrl } from '../lib/blob-helpers.js';
|
||||||
import HashNavigation from '../lib/navigation.js';
|
import HashNavigation from '../lib/navigation.js';
|
||||||
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
|
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
|
||||||
|
|
||||||
@@ -872,12 +872,7 @@ class MessagesView extends React.Component {
|
|||||||
|
|
||||||
// sendAudioAttachment sends audio bits inband as Drafty message (no preview).
|
// sendAudioAttachment sends audio bits inband as Drafty message (no preview).
|
||||||
sendAudioAttachment(url, preview, duration) {
|
sendAudioAttachment(url, preview, duration) {
|
||||||
let previewEncoded;
|
fetch(url)
|
||||||
blobToBase64(new Blob(Uint8Array.from(preview)))
|
|
||||||
.then(pv64 => {
|
|
||||||
previewEncoded = pv64 ? pv64.bits : undefined;
|
|
||||||
return fetch(url);
|
|
||||||
})
|
|
||||||
.then(result => result.blob())
|
.then(result => result.blob())
|
||||||
.then(blob => {
|
.then(blob => {
|
||||||
// Server-provided limit reduced for base64 encoding and overhead.
|
// Server-provided limit reduced for base64 encoding and overhead.
|
||||||
@@ -894,7 +889,7 @@ class MessagesView extends React.Component {
|
|||||||
mime: blob.type,
|
mime: blob.type,
|
||||||
size: blob.size,
|
size: blob.size,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
preview: previewEncoded,
|
preview: intArrayToBase64(preview),
|
||||||
urlPromise: uploadCompletionPromise
|
urlPromise: uploadCompletionPromise
|
||||||
});
|
});
|
||||||
// Pass data and the uploader to the TinodeWeb.
|
// Pass data and the uploader to the TinodeWeb.
|
||||||
@@ -908,7 +903,7 @@ class MessagesView extends React.Component {
|
|||||||
size: blob.size,
|
size: blob.size,
|
||||||
data: b64.bits,
|
data: b64.bits,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
preview: previewEncoded,
|
preview: intArrayToBase64(preview),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,142 +3,142 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { secondsToTime } from '../lib/strformat';
|
import { secondsToTime } from '../lib/strformat';
|
||||||
|
import { base64ToIntArray } from '../lib/blob-helpers';
|
||||||
|
|
||||||
|
// Make canvas bigger than the element size to reduce blurring.
|
||||||
|
const CANVAS_UPSCALING = 2.0;
|
||||||
// Thickness of a visualization bar.
|
// Thickness of a visualization bar.
|
||||||
const LINE_WIDTH = 4;
|
const LINE_WIDTH = 3 * CANVAS_UPSCALING;
|
||||||
// Spacing between two visualization bars.
|
// Spacing between two visualization bars.
|
||||||
const SPACING = 2;
|
const SPACING = 2 * CANVAS_UPSCALING;
|
||||||
// Duration represented by one visualization bar.
|
|
||||||
const MILLIS_PER_BAR = 100;
|
|
||||||
// Color of histogram bars
|
// Color of histogram bars
|
||||||
const BAR_COLOR = '#8fbed6';
|
const BAR_COLOR = '#8fbed6';
|
||||||
// Scaling for visualization bars.
|
const BAR_COLOR_DARK = '#5f8ea5';
|
||||||
const BAR_SCALE = 96.0;
|
|
||||||
// Background color
|
// Background color
|
||||||
const BKG_COLOR = '#eeeeee';
|
const BKG_COLOR = '#eee';
|
||||||
|
// Minimum number of amplitude bars to draw.
|
||||||
|
const MIN_PREVIEW_LENGTH = 16;
|
||||||
|
|
||||||
export default class AudioPlayer extends React.PureComponent {
|
export default class AudioPlayer extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
let preview = base64ToIntArray(this.props.preview);
|
||||||
|
if (Array.isArray(preview) && preview.length < MIN_PREVIEW_LENGTH) {
|
||||||
|
preview = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
canPlay: false,
|
canPlay: false,
|
||||||
playing: false,
|
playing: false,
|
||||||
currentTime: '0:00',
|
currentTime: '0:00',
|
||||||
duration: secondsToTime(this.props.duration)
|
duration: this.props.duration > 0 ? secondsToTime(this.props.duration / 1000) : '-:--',
|
||||||
|
preview: preview
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.calcPreviewBars = this.calcPreviewBars.bind(this);
|
||||||
this.visualize = this.visualize.bind(this);
|
this.visualize = this.visualize.bind(this);
|
||||||
this.initVisualizer = this.initVisualizer.bind(this);
|
|
||||||
|
|
||||||
this.handlePlay = this.handlePlay.bind(this);
|
this.handlePlay = this.handlePlay.bind(this);
|
||||||
this.handlePause = this.handlePause.bind(this);
|
this.handleSeek = this.handleSeek.bind(this);
|
||||||
this.handleError = this.handleError.bind(this);
|
this.handleError = this.handleError.bind(this);
|
||||||
|
|
||||||
this.audioPlayer = null;
|
this.audioPlayer = null;
|
||||||
|
|
||||||
this.durationMillis = 0;
|
|
||||||
this.startedOn = null;
|
|
||||||
this.viewBuffer = [];
|
this.viewBuffer = [];
|
||||||
this.canvasRef = React.createRef();
|
this.canvasRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.canvasContext = this.canvasRef.current.getContext('2d');
|
|
||||||
this.canvasContext.lineCap = 'round';
|
|
||||||
// To reduce line blurring.
|
|
||||||
this.canvasContext.translate(0.5, 0.5);
|
|
||||||
|
|
||||||
if (this.props.src) {
|
if (this.props.src) {
|
||||||
this.audioPlayer = new Audio(this.props.src);
|
this.audioPlayer = new Audio(this.props.src);
|
||||||
this.audioPlayer.addEventListener('loadedmetadata', _ => this.setState({duration: secondsToTime(this.audioPlayer.duration)}));
|
|
||||||
this.audioPlayer.addEventListener('canplay', _ => this.setState({canPlay: true}));
|
this.audioPlayer.addEventListener('canplay', _ => this.setState({canPlay: true}));
|
||||||
this.audioPlayer.addEventListener('timeupdate', _ => console.log(this.audioPlayer.currentTime));
|
this.audioPlayer.addEventListener('timeupdate', _ => this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime)}));
|
||||||
this.audioPlayer.addEventListener('ended', _ => this.setState({playing: false, currentTime: secondsToTime(0)}));
|
this.audioPlayer.addEventListener('ended', _ => this.setState({playing: false, currentTime: secondsToTime(0)}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.src) {
|
if (this.state.preview) {
|
||||||
this.initVisualizer();
|
// Force canvas aspect ration to match one of the element + upscale canvas to reduce blurring.
|
||||||
|
this.canvasRef.current.width = this.canvasRef.current.offsetWidth * CANVAS_UPSCALING;
|
||||||
|
this.canvasRef.current.height = this.canvasRef.current.offsetHeight * CANVAS_UPSCALING;
|
||||||
|
|
||||||
|
this.canvasContext = this.canvasRef.current.getContext('2d');
|
||||||
|
this.canvasContext.lineCap = 'round';
|
||||||
|
|
||||||
|
this.viewBuffer = this.calcPreviewBars(this.state.preview);
|
||||||
|
this.visualize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Draw amplitude of sound.
|
// Draw amplitude of sound.
|
||||||
visualize() {
|
visualize() {
|
||||||
|
const width = this.canvasRef.current.width;
|
||||||
|
const height = this.canvasRef.current.height;
|
||||||
|
|
||||||
this.canvasContext.fillStyle = BKG_COLOR;
|
|
||||||
this.canvasContext.lineWidth = LINE_WIDTH;
|
this.canvasContext.lineWidth = LINE_WIDTH;
|
||||||
this.canvasContext.strokeStyle = BAR_COLOR;
|
|
||||||
|
|
||||||
let prevBarCount = 0;
|
|
||||||
const drawFrame = () => {
|
const drawFrame = () => {
|
||||||
if (!this.startedOn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.requestAnimationFrame(drawFrame);
|
|
||||||
|
|
||||||
const duration = this.durationMillis + (Date.now() - this.startedOn);
|
|
||||||
// Update record length timer.
|
|
||||||
this.setState({duration: secondsToTime(duration / 1000)});
|
|
||||||
|
|
||||||
// Check if record is too long.
|
|
||||||
if (duration > MAX_DURATION) {
|
|
||||||
this.mediaRecorder.pause();
|
|
||||||
this.durationMillis += Date.now() - this.startedOn;
|
|
||||||
this.startedOn = null;
|
|
||||||
this.setState({enabled: false, recording: false, duration: secondsToTime(this.durationMillis / 1000)});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw histogram.
|
|
||||||
|
|
||||||
// Get current waveform and calculate its amplitude.
|
|
||||||
this.analyser.getByteTimeDomainData(pcmData);
|
|
||||||
let volume = 0.0;
|
|
||||||
for (const amplitude of pcmData) {
|
|
||||||
volume += (amplitude - 127) ** 2;
|
|
||||||
}
|
|
||||||
volume = Math.sqrt(volume/pcmData.length);
|
|
||||||
|
|
||||||
let barCount = (duration / MILLIS_PER_BAR) | 0;
|
|
||||||
// Shift of the histogram along x-axis to make scrolling smooth. No need to shift if recording is too short.
|
|
||||||
const dx = viewDuration > duration ? 0 :
|
|
||||||
(duration - MILLIS_PER_BAR * barCount) / MILLIS_PER_BAR * (LINE_WIDTH + SPACING);
|
|
||||||
|
|
||||||
if (prevBarCount != barCount) {
|
|
||||||
prevBarCount = barCount;
|
|
||||||
// Add new amplitude visualization bar.
|
|
||||||
this.viewBuffer.push(volume);
|
|
||||||
if (this.viewBuffer.length > viewLength) {
|
|
||||||
// Keep at most 'viewLength' amplitude bars.
|
|
||||||
this.viewBuffer.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear canvas.
|
// Clear canvas.
|
||||||
|
|
||||||
|
this.canvasContext.fillStyle = BKG_COLOR;
|
||||||
|
this.canvasContext.strokeStyle = BAR_COLOR;
|
||||||
this.canvasContext.fillRect(0, 0, width, height);
|
this.canvasContext.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
// Draw amplitude bars.
|
// Start new drawing.
|
||||||
this.canvasContext.beginPath();
|
this.canvasContext.beginPath();
|
||||||
for (let i = 0; i < this.viewBuffer.length; i++) {
|
|
||||||
let x = i * (LINE_WIDTH + SPACING) - dx;
|
|
||||||
let y = this.viewBuffer[i] / BAR_SCALE * height;
|
|
||||||
|
|
||||||
this.canvasContext.moveTo(x, (height - y) * 0.5);
|
if (this.viewBuffer) {
|
||||||
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
|
window.requestAnimationFrame(drawFrame);
|
||||||
|
|
||||||
|
// Draw amplitude bars.
|
||||||
|
for (let i = 0; i < this.viewBuffer.length; i++) {
|
||||||
|
let x = 1 + i * (LINE_WIDTH + SPACING) + LINE_WIDTH * 0.5;
|
||||||
|
let y = this.viewBuffer[i] * height * 0.9;
|
||||||
|
|
||||||
|
this.canvasContext.moveTo(x, (height - y) * 0.5);
|
||||||
|
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Actually draw the bars on canvas.
|
// Actually draw the bars on canvas.
|
||||||
this.canvasContext.stroke();
|
this.canvasContext.stroke();
|
||||||
|
|
||||||
|
if (this.props.duration) {
|
||||||
|
this.canvasContext.beginPath();
|
||||||
|
|
||||||
|
const x = Math.max(0, Math.min(this.audioPlayer.currentTime * 1000 / this.props.duration, 1)) * width;
|
||||||
|
this.canvasContext.arc(x + LINE_WIDTH * 0.5, height * 0.5, LINE_WIDTH * 2, 0, 2 * Math.PI);
|
||||||
|
this.canvasContext.fillStyle = BAR_COLOR_DARK;
|
||||||
|
this.canvasContext.fill();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawFrame();
|
drawFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
initVisualizer() {
|
calcPreviewBars(original) {
|
||||||
console.log("Draw amplitude bars");
|
// Quick and dirty downsampling of the original preview bars into a smaller (or equal) number of bars we can display here.
|
||||||
}
|
const barCount = Math.min(original.length, ((this.canvasRef.current.width - 1) / (LINE_WIDTH + SPACING)) | 0);
|
||||||
|
console.log("Bar count:", barCount, this.canvasRef.current.width, LINE_WIDTH + SPACING);
|
||||||
|
const factor = original.length / barCount;
|
||||||
|
let amps = [];
|
||||||
|
let max = -1;
|
||||||
|
for (let i=0;i<barCount;i++) {
|
||||||
|
let lo = (i * factor) | 0; // low bound;
|
||||||
|
let hi = ((i + 1) * factor) | 0; // high bound;
|
||||||
|
let amp = 0.0;
|
||||||
|
for (let j=lo; j<hi; j++) {
|
||||||
|
amp += original[j]
|
||||||
|
}
|
||||||
|
amps[i] = Math.max(0, amp / (hi - lo));
|
||||||
|
max = Math.max(amps[i], max);
|
||||||
|
}
|
||||||
|
|
||||||
handlePause(e) {
|
if (max > 0) {
|
||||||
e.preventDefault();
|
return amps.map(a => a / max);
|
||||||
this.setState({recording: false, duration: secondsToTime(this.durationMillis / 1000)});
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlay(e) {
|
handlePlay(e) {
|
||||||
@@ -148,8 +148,10 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
if (this.state.playing) {
|
if (this.state.playing) {
|
||||||
this.audioPlayer.pause();
|
this.audioPlayer.pause();
|
||||||
|
this.startedOn = null;
|
||||||
} else {
|
} else {
|
||||||
this.audioPlayer.play();
|
this.audioPlayer.play();
|
||||||
|
this.startedOn = Date.now();
|
||||||
}
|
}
|
||||||
this.setState({playing: !this.state.playing});
|
this.setState({playing: !this.state.playing});
|
||||||
}
|
}
|
||||||
@@ -158,6 +160,16 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSeek(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.target && this.props.duration) {
|
||||||
|
const rect = e.target.getBoundingClientRect();
|
||||||
|
const offset = (e.clientX - rect.left) / rect.width;
|
||||||
|
this.audioPlayer.currentTime = this.props.duration * offset / 1000;
|
||||||
|
this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const playClass = 'material-icons large' + (this.state.canPlay ? '' : ' disabled');
|
const playClass = 'material-icons large' + (this.state.canPlay ? '' : ' disabled');
|
||||||
return (
|
return (
|
||||||
@@ -166,7 +178,10 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
<i className={playClass}>{this.state.playing ? 'pause_circle' : 'play_circle'}</i>
|
<i className={playClass}>{this.state.playing ? 'pause_circle' : 'play_circle'}</i>
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<canvas className="visualiser" style={{border: '1px solid #666'}} ref={this.canvasRef} />
|
{this.state.preview ?
|
||||||
|
<canvas className="visualiser" ref={this.canvasRef} onClick={this.handleSeek} /> :
|
||||||
|
<div className="visualiser">unavailable</div>
|
||||||
|
}
|
||||||
<div className="timer">{this.state.currentTime}/{this.state.duration}</div>
|
<div className="timer">{this.state.currentTime}/{this.state.duration}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+128
-92
@@ -2083,7 +2083,9 @@ __webpack_require__.r(__webpack_exports__);
|
|||||||
/* harmony export */ "base64EncodedLen": () => (/* binding */ base64EncodedLen),
|
/* harmony export */ "base64EncodedLen": () => (/* binding */ base64EncodedLen),
|
||||||
/* harmony export */ "base64DecodedLen": () => (/* binding */ base64DecodedLen),
|
/* harmony export */ "base64DecodedLen": () => (/* binding */ base64DecodedLen),
|
||||||
/* harmony export */ "base64ReEncode": () => (/* binding */ base64ReEncode),
|
/* harmony export */ "base64ReEncode": () => (/* binding */ base64ReEncode),
|
||||||
/* harmony export */ "base64ToBlob": () => (/* binding */ base64ToBlob)
|
/* harmony export */ "base64ToBlob": () => (/* binding */ base64ToBlob),
|
||||||
|
/* harmony export */ "intArrayToBase64": () => (/* binding */ intArrayToBase64),
|
||||||
|
/* harmony export */ "base64ToIntArray": () => (/* binding */ base64ToIntArray)
|
||||||
/* harmony export */ });
|
/* harmony export */ });
|
||||||
const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
|
const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
|
||||||
const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg'];
|
const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg'];
|
||||||
@@ -2359,6 +2361,32 @@ function base64ToBlob(str, mime) {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function intArrayToBase64(arr) {
|
||||||
|
if (!Array.isArray(arr)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let bin = '';
|
||||||
|
new Uint8Array(arr).forEach(b => bin += String.fromCharCode(b));
|
||||||
|
return window.btoa(bin);
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function base64ToIntArray(b64) {
|
||||||
|
const arr = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bin = window.atob(b64);
|
||||||
|
[...bin].forEach(c => {
|
||||||
|
arr.push(c.charCodeAt(0));
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
@@ -2489,6 +2517,8 @@ function fullFormatter(style, data, values, key, stack) {
|
|||||||
case 'AU':
|
case 'AU':
|
||||||
if (attr.src) {
|
if (attr.src) {
|
||||||
attr.src = this.authorizeURL((0,_utils_js__WEBPACK_IMPORTED_MODULE_9__.sanitizeUrlForMime)(attr.src, 'audio'));
|
attr.src = this.authorizeURL((0,_utils_js__WEBPACK_IMPORTED_MODULE_9__.sanitizeUrlForMime)(attr.src, 'audio'));
|
||||||
|
attr.duration = data.duration > 0 ? data.duration | 0 : undefined;
|
||||||
|
attr.preview = data.preview;
|
||||||
attr.loading = 'lazy';
|
attr.loading = 'lazy';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6198,11 +6228,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendAudioAttachment(url, preview, duration) {
|
sendAudioAttachment(url, preview, duration) {
|
||||||
let previewEncoded;
|
fetch(url).then(result => result.blob()).then(blob => {
|
||||||
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_16__.blobToBase64)(new Blob(Uint8Array.from(preview))).then(pv64 => {
|
|
||||||
previewEncoded = pv64 ? pv64.bits : undefined;
|
|
||||||
return fetch(url);
|
|
||||||
}).then(result => result.blob()).then(blob => {
|
|
||||||
const maxInbandAttachmentSize = this.props.tinode.getServerLimit('maxMessageSize', _config_js__WEBPACK_IMPORTED_MODULE_15__.MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024;
|
const maxInbandAttachmentSize = this.props.tinode.getServerLimit('maxMessageSize', _config_js__WEBPACK_IMPORTED_MODULE_15__.MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024;
|
||||||
|
|
||||||
if (blob.size > maxInbandAttachmentSize) {
|
if (blob.size > maxInbandAttachmentSize) {
|
||||||
@@ -6218,7 +6244,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
|||||||
mime: blob.type,
|
mime: blob.type,
|
||||||
size: blob.size,
|
size: blob.size,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
preview: previewEncoded,
|
preview: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_16__.intArrayToBase64)(preview),
|
||||||
urlPromise: uploadCompletionPromise
|
urlPromise: uploadCompletionPromise
|
||||||
});
|
});
|
||||||
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
||||||
@@ -6229,7 +6255,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
|||||||
size: blob.size,
|
size: blob.size,
|
||||||
data: b64.bits,
|
data: b64.bits,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
preview: previewEncoded
|
preview: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_16__.intArrayToBase64)(preview)
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -9636,134 +9662,129 @@ __webpack_require__.r(__webpack_exports__);
|
|||||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
|
||||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
||||||
/* harmony import */ var _lib_strformat__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../lib/strformat */ "./src/lib/strformat.js");
|
/* harmony import */ var _lib_strformat__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../lib/strformat */ "./src/lib/strformat.js");
|
||||||
|
/* harmony import */ var _lib_blob_helpers__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../lib/blob-helpers */ "./src/lib/blob-helpers.js");
|
||||||
|
|
||||||
|
|
||||||
const LINE_WIDTH = 4;
|
|
||||||
const SPACING = 2;
|
const CANVAS_UPSCALING = 2.0;
|
||||||
const MILLIS_PER_BAR = 100;
|
const LINE_WIDTH = 3 * CANVAS_UPSCALING;
|
||||||
|
const SPACING = 2 * CANVAS_UPSCALING;
|
||||||
const BAR_COLOR = '#8fbed6';
|
const BAR_COLOR = '#8fbed6';
|
||||||
const BAR_SCALE = 96.0;
|
const BAR_COLOR_DARK = '#5f8ea5';
|
||||||
const BKG_COLOR = '#eeeeee';
|
const BKG_COLOR = '#eee';
|
||||||
|
const MIN_PREVIEW_LENGTH = 16;
|
||||||
class AudioPlayer extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) {
|
class AudioPlayer extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
let preview = (0,_lib_blob_helpers__WEBPACK_IMPORTED_MODULE_2__.base64ToIntArray)(this.props.preview);
|
||||||
|
|
||||||
|
if (Array.isArray(preview) && preview.length < MIN_PREVIEW_LENGTH) {
|
||||||
|
preview = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
canPlay: false,
|
canPlay: false,
|
||||||
playing: false,
|
playing: false,
|
||||||
currentTime: '0:00',
|
currentTime: '0:00',
|
||||||
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_1__.secondsToTime)(this.props.duration)
|
duration: this.props.duration > 0 ? (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_1__.secondsToTime)(this.props.duration / 1000) : '-:--',
|
||||||
|
preview: preview
|
||||||
};
|
};
|
||||||
|
this.calcPreviewBars = this.calcPreviewBars.bind(this);
|
||||||
this.visualize = this.visualize.bind(this);
|
this.visualize = this.visualize.bind(this);
|
||||||
this.initVisualizer = this.initVisualizer.bind(this);
|
|
||||||
this.handlePlay = this.handlePlay.bind(this);
|
this.handlePlay = this.handlePlay.bind(this);
|
||||||
this.handlePause = this.handlePause.bind(this);
|
this.handleSeek = this.handleSeek.bind(this);
|
||||||
this.handleError = this.handleError.bind(this);
|
this.handleError = this.handleError.bind(this);
|
||||||
this.audioPlayer = null;
|
this.audioPlayer = null;
|
||||||
this.durationMillis = 0;
|
|
||||||
this.startedOn = null;
|
|
||||||
this.viewBuffer = [];
|
this.viewBuffer = [];
|
||||||
this.canvasRef = react__WEBPACK_IMPORTED_MODULE_0___default().createRef();
|
this.canvasRef = react__WEBPACK_IMPORTED_MODULE_0___default().createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.canvasContext = this.canvasRef.current.getContext('2d');
|
|
||||||
this.canvasContext.lineCap = 'round';
|
|
||||||
this.canvasContext.translate(0.5, 0.5);
|
|
||||||
|
|
||||||
if (this.props.src) {
|
if (this.props.src) {
|
||||||
this.audioPlayer = new Audio(this.props.src);
|
this.audioPlayer = new Audio(this.props.src);
|
||||||
this.audioPlayer.addEventListener('loadedmetadata', _ => this.setState({
|
|
||||||
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_1__.secondsToTime)(this.audioPlayer.duration)
|
|
||||||
}));
|
|
||||||
this.audioPlayer.addEventListener('canplay', _ => this.setState({
|
this.audioPlayer.addEventListener('canplay', _ => this.setState({
|
||||||
canPlay: true
|
canPlay: true
|
||||||
}));
|
}));
|
||||||
this.audioPlayer.addEventListener('timeupdate', _ => console.log(this.audioPlayer.currentTime));
|
this.audioPlayer.addEventListener('timeupdate', _ => this.setState({
|
||||||
|
currentTime: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_1__.secondsToTime)(this.audioPlayer.currentTime)
|
||||||
|
}));
|
||||||
this.audioPlayer.addEventListener('ended', _ => this.setState({
|
this.audioPlayer.addEventListener('ended', _ => this.setState({
|
||||||
playing: false,
|
playing: false,
|
||||||
currentTime: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_1__.secondsToTime)(0)
|
currentTime: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_1__.secondsToTime)(0)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.src) {
|
if (this.state.preview) {
|
||||||
this.initVisualizer();
|
this.canvasRef.current.width = this.canvasRef.current.offsetWidth * CANVAS_UPSCALING;
|
||||||
|
this.canvasRef.current.height = this.canvasRef.current.offsetHeight * CANVAS_UPSCALING;
|
||||||
|
this.canvasContext = this.canvasRef.current.getContext('2d');
|
||||||
|
this.canvasContext.lineCap = 'round';
|
||||||
|
this.viewBuffer = this.calcPreviewBars(this.state.preview);
|
||||||
|
this.visualize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
visualize() {
|
visualize() {
|
||||||
this.canvasContext.fillStyle = BKG_COLOR;
|
const width = this.canvasRef.current.width;
|
||||||
|
const height = this.canvasRef.current.height;
|
||||||
this.canvasContext.lineWidth = LINE_WIDTH;
|
this.canvasContext.lineWidth = LINE_WIDTH;
|
||||||
this.canvasContext.strokeStyle = BAR_COLOR;
|
|
||||||
let prevBarCount = 0;
|
|
||||||
|
|
||||||
const drawFrame = () => {
|
const drawFrame = () => {
|
||||||
if (!this.startedOn) {
|
this.canvasContext.fillStyle = BKG_COLOR;
|
||||||
return;
|
this.canvasContext.strokeStyle = BAR_COLOR;
|
||||||
}
|
|
||||||
|
|
||||||
window.requestAnimationFrame(drawFrame);
|
|
||||||
const duration = this.durationMillis + (Date.now() - this.startedOn);
|
|
||||||
this.setState({
|
|
||||||
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_1__.secondsToTime)(duration / 1000)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (duration > MAX_DURATION) {
|
|
||||||
this.mediaRecorder.pause();
|
|
||||||
this.durationMillis += Date.now() - this.startedOn;
|
|
||||||
this.startedOn = null;
|
|
||||||
this.setState({
|
|
||||||
enabled: false,
|
|
||||||
recording: false,
|
|
||||||
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_1__.secondsToTime)(this.durationMillis / 1000)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.analyser.getByteTimeDomainData(pcmData);
|
|
||||||
let volume = 0.0;
|
|
||||||
|
|
||||||
for (const amplitude of pcmData) {
|
|
||||||
volume += (amplitude - 127) ** 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
volume = Math.sqrt(volume / pcmData.length);
|
|
||||||
let barCount = duration / MILLIS_PER_BAR | 0;
|
|
||||||
const dx = viewDuration > duration ? 0 : (duration - MILLIS_PER_BAR * barCount) / MILLIS_PER_BAR * (LINE_WIDTH + SPACING);
|
|
||||||
|
|
||||||
if (prevBarCount != barCount) {
|
|
||||||
prevBarCount = barCount;
|
|
||||||
this.viewBuffer.push(volume);
|
|
||||||
|
|
||||||
if (this.viewBuffer.length > viewLength) {
|
|
||||||
this.viewBuffer.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.canvasContext.fillRect(0, 0, width, height);
|
this.canvasContext.fillRect(0, 0, width, height);
|
||||||
this.canvasContext.beginPath();
|
this.canvasContext.beginPath();
|
||||||
|
|
||||||
for (let i = 0; i < this.viewBuffer.length; i++) {
|
if (this.viewBuffer) {
|
||||||
let x = i * (LINE_WIDTH + SPACING) - dx;
|
window.requestAnimationFrame(drawFrame);
|
||||||
let y = this.viewBuffer[i] / BAR_SCALE * height;
|
|
||||||
this.canvasContext.moveTo(x, (height - y) * 0.5);
|
for (let i = 0; i < this.viewBuffer.length; i++) {
|
||||||
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
|
let x = 1 + i * (LINE_WIDTH + SPACING) + LINE_WIDTH * 0.5;
|
||||||
|
let y = this.viewBuffer[i] * height * 0.9;
|
||||||
|
this.canvasContext.moveTo(x, (height - y) * 0.5);
|
||||||
|
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canvasContext.stroke();
|
this.canvasContext.stroke();
|
||||||
|
|
||||||
|
if (this.props.duration) {
|
||||||
|
this.canvasContext.beginPath();
|
||||||
|
const x = Math.max(0, Math.min(this.audioPlayer.currentTime * 1000 / this.props.duration, 1)) * width;
|
||||||
|
this.canvasContext.arc(x + LINE_WIDTH * 0.5, height * 0.5, LINE_WIDTH * 2, 0, 2 * Math.PI);
|
||||||
|
this.canvasContext.fillStyle = BAR_COLOR_DARK;
|
||||||
|
this.canvasContext.fill();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
drawFrame();
|
drawFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
initVisualizer() {
|
calcPreviewBars(original) {
|
||||||
console.log("Draw amplitude bars");
|
const barCount = Math.min(original.length, (this.canvasRef.current.width - 1) / (LINE_WIDTH + SPACING) | 0);
|
||||||
}
|
console.log("Bar count:", barCount, this.canvasRef.current.width, LINE_WIDTH + SPACING);
|
||||||
|
const factor = original.length / barCount;
|
||||||
|
let amps = [];
|
||||||
|
let max = -1;
|
||||||
|
|
||||||
handlePause(e) {
|
for (let i = 0; i < barCount; i++) {
|
||||||
e.preventDefault();
|
let lo = i * factor | 0;
|
||||||
this.setState({
|
let hi = (i + 1) * factor | 0;
|
||||||
recording: false,
|
let amp = 0.0;
|
||||||
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_1__.secondsToTime)(this.durationMillis / 1000)
|
|
||||||
});
|
for (let j = lo; j < hi; j++) {
|
||||||
|
amp += original[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
amps[i] = Math.max(0, amp / (hi - lo));
|
||||||
|
max = Math.max(amps[i], max);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max > 0) {
|
||||||
|
return amps.map(a => a / max);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlay(e) {
|
handlePlay(e) {
|
||||||
@@ -9775,8 +9796,10 @@ class AudioPlayer extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
|
|||||||
|
|
||||||
if (this.state.playing) {
|
if (this.state.playing) {
|
||||||
this.audioPlayer.pause();
|
this.audioPlayer.pause();
|
||||||
|
this.startedOn = null;
|
||||||
} else {
|
} else {
|
||||||
this.audioPlayer.play();
|
this.audioPlayer.play();
|
||||||
|
this.startedOn = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -9788,6 +9811,19 @@ class AudioPlayer extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSeek(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (e.target && this.props.duration) {
|
||||||
|
const rect = e.target.getBoundingClientRect();
|
||||||
|
const offset = (e.clientX - rect.left) / rect.width;
|
||||||
|
this.audioPlayer.currentTime = this.props.duration * offset / 1000;
|
||||||
|
this.setState({
|
||||||
|
currentTime: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_1__.secondsToTime)(this.audioPlayer.currentTime)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const playClass = 'material-icons large' + (this.state.canPlay ? '' : ' disabled');
|
const playClass = 'material-icons large' + (this.state.canPlay ? '' : ' disabled');
|
||||||
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||||
@@ -9798,13 +9834,13 @@ class AudioPlayer extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
|
|||||||
title: "Play"
|
title: "Play"
|
||||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||||
className: playClass
|
className: playClass
|
||||||
}, this.state.playing ? 'pause_circle' : 'play_circle')), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("canvas", {
|
}, this.state.playing ? 'pause_circle' : 'play_circle')), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, this.state.preview ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("canvas", {
|
||||||
className: "visualiser",
|
className: "visualiser",
|
||||||
style: {
|
ref: this.canvasRef,
|
||||||
border: '1px solid #666'
|
onClick: this.handleSeek
|
||||||
},
|
}) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||||
ref: this.canvasRef
|
className: "visualiser"
|
||||||
}), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
}, "unavailable"), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||||
className: "timer"
|
className: "timer"
|
||||||
}, this.state.currentTime, "/", this.state.duration)));
|
}, this.state.currentTime, "/", this.state.duration)));
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user