putting it all together
This commit is contained in:
@@ -2666,10 +2666,12 @@ ul.tabbar li.active > a {
|
|||||||
|
|
||||||
.audio-player .material-icons.large {
|
.audio-player .material-icons.large {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
|
color: #009688;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-player .material-icons.disabled {
|
.audio-player .material-icons.disabled {
|
||||||
color: #999;
|
color: #999;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-player .visualiser {
|
.audio-player .visualiser {
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+9
-2
@@ -1362,10 +1362,17 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_unknown": {
|
"drafty_unknown": {
|
||||||
"translation": "",
|
"translation": "Nicht unterstützt",
|
||||||
"defaultMessage": "Unsupported",
|
"defaultMessage": "Unsupported",
|
||||||
"description": "Unsupported entity in drafty",
|
"description": "Unsupported entity in drafty",
|
||||||
"missing": true,
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"preview_unavailable": {
|
||||||
|
"translation": "nicht verfügbar",
|
||||||
|
"defaultMessage": "unavailable",
|
||||||
|
"description": "Message shown when media preview is not available",
|
||||||
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1367,5 +1367,12 @@
|
|||||||
"description": "Unsupported entity in drafty",
|
"description": "Unsupported entity in drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"preview_unavailable": {
|
||||||
|
"translation": "unavailable",
|
||||||
|
"defaultMessage": "unavailable",
|
||||||
|
"description": "Message shown when media preview is not available",
|
||||||
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -1362,10 +1362,17 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_unknown": {
|
"drafty_unknown": {
|
||||||
"translation": "",
|
"translation": "No soportado",
|
||||||
"defaultMessage": "Unsupported",
|
"defaultMessage": "Unsupported",
|
||||||
"description": "Unsupported entity in drafty",
|
"description": "Unsupported entity in drafty",
|
||||||
"missing": true,
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"preview_unavailable": {
|
||||||
|
"translation": "indisponible",
|
||||||
|
"defaultMessage": "unavailable",
|
||||||
|
"description": "Message shown when media preview is not available",
|
||||||
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -608,6 +608,10 @@
|
|||||||
"defaultMessage": "phone",
|
"defaultMessage": "phone",
|
||||||
"description": "Dative case of 'phone', i.e. 'phone' in 'by phone'"
|
"description": "Dative case of 'phone', i.e. 'phone' in 'by phone'"
|
||||||
},
|
},
|
||||||
|
"preview_unavailable": {
|
||||||
|
"defaultMessage": "unavailable",
|
||||||
|
"description": "Message shown when media preview is not available"
|
||||||
|
},
|
||||||
"private_editing_placeholder": {
|
"private_editing_placeholder": {
|
||||||
"defaultMessage": "Visible to you only",
|
"defaultMessage": "Visible to you only",
|
||||||
"description": "Placeholder for editing 'private'"
|
"description": "Placeholder for editing 'private'"
|
||||||
|
|||||||
+9
-2
@@ -1362,10 +1362,17 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_unknown": {
|
"drafty_unknown": {
|
||||||
"translation": "",
|
"translation": "지원되지 않음",
|
||||||
"defaultMessage": "Unsupported",
|
"defaultMessage": "Unsupported",
|
||||||
"description": "Unsupported entity in drafty",
|
"description": "Unsupported entity in drafty",
|
||||||
"missing": true,
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"preview_unavailable": {
|
||||||
|
"translation": "없는",
|
||||||
|
"defaultMessage": "unavailable",
|
||||||
|
"description": "Message shown when media preview is not available",
|
||||||
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -1362,10 +1362,17 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_unknown": {
|
"drafty_unknown": {
|
||||||
"translation": "",
|
"translation": "Neacceptat",
|
||||||
"defaultMessage": "Unsupported",
|
"defaultMessage": "Unsupported",
|
||||||
"description": "Unsupported entity in drafty",
|
"description": "Unsupported entity in drafty",
|
||||||
"missing": true,
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"preview_unavailable": {
|
||||||
|
"translation": "indisponibil",
|
||||||
|
"defaultMessage": "unavailable",
|
||||||
|
"description": "Message shown when media preview is not available",
|
||||||
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -1362,10 +1362,17 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_unknown": {
|
"drafty_unknown": {
|
||||||
"translation": "",
|
"translation": "Не поддерживается",
|
||||||
"defaultMessage": "Unsupported",
|
"defaultMessage": "Unsupported",
|
||||||
"description": "Unsupported entity in drafty",
|
"description": "Unsupported entity in drafty",
|
||||||
"missing": true,
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"preview_unavailable": {
|
||||||
|
"translation": "недоступно",
|
||||||
|
"defaultMessage": "unavailable",
|
||||||
|
"description": "Message shown when media preview is not available",
|
||||||
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -1362,10 +1362,17 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_unknown": {
|
"drafty_unknown": {
|
||||||
"translation": "",
|
"translation": "不支持",
|
||||||
"defaultMessage": "Unsupported",
|
"defaultMessage": "Unsupported",
|
||||||
"description": "Unsupported entity in drafty",
|
"description": "Unsupported entity in drafty",
|
||||||
"missing": true,
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"preview_unavailable": {
|
||||||
|
"translation": "不可用",
|
||||||
|
"defaultMessage": "unavailable",
|
||||||
|
"description": "Message shown when media preview is not available",
|
||||||
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -1362,10 +1362,17 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_unknown": {
|
"drafty_unknown": {
|
||||||
"translation": "",
|
"translation": "不支持",
|
||||||
"defaultMessage": "Unsupported",
|
"defaultMessage": "Unsupported",
|
||||||
"description": "Unsupported entity in drafty",
|
"description": "Unsupported entity in drafty",
|
||||||
"missing": true,
|
"missing": false,
|
||||||
|
"obsolete": false
|
||||||
|
},
|
||||||
|
"preview_unavailable": {
|
||||||
|
"translation": "不可用",
|
||||||
|
"defaultMessage": "unavailable",
|
||||||
|
"description": "Message shown when media preview is not available",
|
||||||
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-10
@@ -115,6 +115,9 @@ export function fullFormatter(style, data, values, key, stack) {
|
|||||||
case 'BR':
|
case 'BR':
|
||||||
values = null;
|
values = null;
|
||||||
break;
|
break;
|
||||||
|
case 'EX':
|
||||||
|
// Ignore.
|
||||||
|
break;
|
||||||
case 'HL':
|
case 'HL':
|
||||||
// Highlighted text. Assign class name.
|
// Highlighted text. Assign class name.
|
||||||
attr.className = 'highlight';
|
attr.className = 'highlight';
|
||||||
@@ -161,15 +164,17 @@ export function fullFormatter(style, data, values, key, stack) {
|
|||||||
attr.onClick = this.onQuoteClick;
|
attr.onClick = this.onQuoteClick;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Unknown element.
|
if (!el) {
|
||||||
el = React.Fragment;
|
// Unknown element.
|
||||||
attr = {key: key};
|
el = React.Fragment;
|
||||||
// Generate comment for unknown element.
|
attr = {key: key};
|
||||||
let body = values;
|
// Generate comment for unknown element.
|
||||||
if (!Array.isArray(values) || !values.join('').trim()) {
|
let body = values;
|
||||||
body = [<span key="x1" className="gray">{this.formatMessage(messages.drafty_unknown)}</span>];
|
if (!Array.isArray(values) || !values.join('').trim()) {
|
||||||
|
body = [<span key="x1" className="gray">{this.formatMessage(messages.drafty_unknown)}</span>];
|
||||||
|
}
|
||||||
|
values = [<i key="x0" className="material-icons gray">extension</i>, ' '].concat(body);
|
||||||
}
|
}
|
||||||
values = [<i key="x0" className="material-icons gray">extension</i>, ' '].concat(body);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -246,8 +251,11 @@ export function previewFormatter(style, data, values, key) {
|
|||||||
values = null;
|
values = null;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
el = React.Fragment;
|
if (!el) {
|
||||||
values = [<i key="x0" className="material-icons gray">extension</i>, ' ', this.formatMessage(messages.drafty_unknown)];
|
// Unknown element.
|
||||||
|
el = React.Fragment;
|
||||||
|
values = [<i key="x0" className="material-icons gray">extension</i>, ' ', this.formatMessage(messages.drafty_unknown)];
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!el) {
|
if (!el) {
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -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, intArrayToBase64, makeImageUrl } from '../lib/blob-helpers.js';
|
import { blobToBase64, fileToBase64, imageScaled, 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';
|
||||||
|
|
||||||
@@ -889,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: intArrayToBase64(preview),
|
preview: preview,
|
||||||
urlPromise: uploadCompletionPromise
|
urlPromise: uploadCompletionPromise
|
||||||
});
|
});
|
||||||
// Pass data and the uploader to the TinodeWeb.
|
// Pass data and the uploader to the TinodeWeb.
|
||||||
@@ -903,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: intArrayToBase64(preview),
|
preview: preview,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+115
-41
@@ -1,6 +1,7 @@
|
|||||||
// Audio recorder widget.
|
// Audio recorder widget.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { secondsToTime } from '../lib/strformat';
|
import { secondsToTime } from '../lib/strformat';
|
||||||
import { base64ToIntArray } from '../lib/blob-helpers';
|
import { base64ToIntArray } from '../lib/blob-helpers';
|
||||||
@@ -11,9 +12,10 @@ const CANVAS_UPSCALING = 2.0;
|
|||||||
const LINE_WIDTH = 3 * CANVAS_UPSCALING;
|
const LINE_WIDTH = 3 * CANVAS_UPSCALING;
|
||||||
// Spacing between two visualization bars.
|
// Spacing between two visualization bars.
|
||||||
const SPACING = 2 * CANVAS_UPSCALING;
|
const SPACING = 2 * CANVAS_UPSCALING;
|
||||||
// Color of histogram bars
|
// Color of histogram bars (base color #009688).
|
||||||
const BAR_COLOR = '#8fbed6';
|
const BAR_COLOR = '#85c7c5';
|
||||||
const BAR_COLOR_DARK = '#5f8ea5';
|
const BAR_COLOR_DARK = '#559695';
|
||||||
|
const THUMB_COLOR = '#246867';
|
||||||
|
|
||||||
// Background color
|
// Background color
|
||||||
const BKG_COLOR = '#fff';
|
const BKG_COLOR = '#fff';
|
||||||
@@ -37,6 +39,8 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
preview: preview
|
preview: preview
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.initAudio = this.initAudio.bind(this);
|
||||||
|
this.initCanvas = this.initCanvas.bind(this);
|
||||||
this.calcPreviewBars = this.calcPreviewBars.bind(this);
|
this.calcPreviewBars = this.calcPreviewBars.bind(this);
|
||||||
this.visualize = this.visualize.bind(this);
|
this.visualize = this.visualize.bind(this);
|
||||||
|
|
||||||
@@ -52,60 +56,109 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.src) {
|
if (this.props.src) {
|
||||||
this.audioPlayer = new Audio(this.props.src);
|
this.initAudio();
|
||||||
this.audioPlayer.addEventListener('canplay', _ => this.setState({canPlay: true}));
|
|
||||||
this.audioPlayer.addEventListener('timeupdate', _ => this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime)}));
|
|
||||||
this.audioPlayer.addEventListener('ended', _ => this.setState({playing: false, currentTime: secondsToTime(0)}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.preview) {
|
if (this.state.preview) {
|
||||||
// Force canvas aspect ration to match one of the element + upscale canvas to reduce blurring.
|
this.initCanvas();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.audioPlayer) {
|
||||||
|
this.audioPlayer.oncanplay = null;
|
||||||
|
this.audioPlayer.ontimeupdate = null;
|
||||||
|
this.audioPlayer.onended = null;
|
||||||
|
this.audioPlayer.pause();
|
||||||
|
this.audioPlayer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.src != prevProps.src) {
|
||||||
|
this.initAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.preview != prevProps.preview) {
|
||||||
|
let preview = base64ToIntArray(this.props.preview);
|
||||||
|
if (Array.isArray(preview) && preview.length < MIN_PREVIEW_LENGTH) {
|
||||||
|
preview = null;
|
||||||
|
}
|
||||||
|
this.setState({preview: preview}, this.initCanvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initAudio() {
|
||||||
|
this.audioPlayer = new Audio(this.props.src);
|
||||||
|
this.audioPlayer.oncanplay = _ => this.setState({canPlay: true});
|
||||||
|
this.audioPlayer.ontimeupdate = _ => this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime)});
|
||||||
|
this.audioPlayer.onended = _ => {
|
||||||
|
this.audioPlayer.currentTime = 0;
|
||||||
|
this.setState({playing: false, currentTime: secondsToTime(0)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initCanvas() {
|
||||||
|
// 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 width = this.effectiveWidth;
|
||||||
const height = this.canvasRef.current.height;
|
const height = this.canvasRef.current.height;
|
||||||
|
|
||||||
this.canvasContext.lineWidth = LINE_WIDTH;
|
this.canvasContext.lineWidth = LINE_WIDTH;
|
||||||
|
|
||||||
const drawFrame = () => {
|
const drawFrame = () => {
|
||||||
this.canvasContext.clearRect(0, 0, width, height);
|
if (!this.canvasRef.current) {
|
||||||
|
// The component is unmounted.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Start new drawing.
|
this.canvasContext.clearRect(0, 0, this.canvasRef.current.width, height);
|
||||||
this.canvasContext.beginPath();
|
|
||||||
|
|
||||||
if (this.viewBuffer) {
|
if (this.viewBuffer) {
|
||||||
window.requestAnimationFrame(drawFrame);
|
if (this.state.playing) {
|
||||||
|
window.requestAnimationFrame(drawFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current pplayback position.
|
||||||
|
const thumbAt = this.props.duration ?
|
||||||
|
Math.max(0, Math.min(this.audioPlayer.currentTime * 1000 / this.props.duration, 1)) * (width - LINE_WIDTH * 2) : -1;
|
||||||
|
|
||||||
this.canvasContext.strokeStyle = BAR_COLOR;
|
|
||||||
// Draw amplitude bars.
|
// Draw amplitude bars.
|
||||||
|
this.canvasContext.beginPath();
|
||||||
|
this.canvasContext.strokeStyle = BAR_COLOR_DARK;
|
||||||
for (let i = 0; i < this.viewBuffer.length; i++) {
|
for (let i = 0; i < this.viewBuffer.length; i++) {
|
||||||
let x = 1 + i * (LINE_WIDTH + SPACING) + LINE_WIDTH * 0.5;
|
let x = 1 + i * (LINE_WIDTH + SPACING) + LINE_WIDTH * 0.5;
|
||||||
let y = this.viewBuffer[i] * height * 0.9;
|
let y = this.viewBuffer[i] * height * 0.9;
|
||||||
|
|
||||||
|
const color = x < thumbAt ? BAR_COLOR_DARK : BAR_COLOR;
|
||||||
|
if (this.canvasContext.strokeStyle != color) {
|
||||||
|
this.canvasContext.stroke();
|
||||||
|
this.canvasContext.beginPath();
|
||||||
|
this.canvasContext.strokeStyle = color;
|
||||||
|
}
|
||||||
|
|
||||||
this.canvasContext.moveTo(x, (height - y) * 0.5);
|
this.canvasContext.moveTo(x, (height - y) * 0.5);
|
||||||
this.canvasContext.lineTo(x, height * 0.5 + 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();
|
||||||
|
|
||||||
|
// Draw thumb.
|
||||||
if (this.props.duration) {
|
if (this.props.duration) {
|
||||||
this.canvasContext.beginPath();
|
this.canvasContext.beginPath();
|
||||||
|
this.canvasContext.arc(thumbAt + LINE_WIDTH * 2, height * 0.5, LINE_WIDTH * 2, 0, 2 * Math.PI);
|
||||||
const x = Math.max(0, Math.min(this.audioPlayer.currentTime * 1000 / this.props.duration, 1)) * width;
|
this.canvasContext.fillStyle = THUMB_COLOR;
|
||||||
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();
|
this.canvasContext.fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +169,7 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
|
|
||||||
// Quick and dirty downsampling of the original preview bars into a smaller (or equal) number of bars we can display here.
|
// Quick and dirty downsampling of the original preview bars into a smaller (or equal) number of bars we can display here.
|
||||||
calcPreviewBars(original) {
|
calcPreviewBars(original) {
|
||||||
const barCount = Math.min(original.length, ((this.canvasRef.current.width - 1) / (LINE_WIDTH + SPACING)) | 0);
|
const barCount = Math.min(original.length, ((this.canvasRef.current.width - SPACING) / (LINE_WIDTH + SPACING)) | 0);
|
||||||
const factor = original.length / barCount;
|
const factor = original.length / barCount;
|
||||||
let amps = [];
|
let amps = [];
|
||||||
let max = -1;
|
let max = -1;
|
||||||
@@ -132,6 +185,8 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (max > 0) {
|
if (max > 0) {
|
||||||
|
// Remove extra padding on the right due to fractional bar which is not drawn.
|
||||||
|
this.effectiveWidth = barCount * (LINE_WIDTH + SPACING) + SPACING;
|
||||||
return amps.map(a => a / max);
|
return amps.map(a => a / max);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -144,12 +199,11 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
if (this.state.playing) {
|
if (this.state.playing) {
|
||||||
this.audioPlayer.pause();
|
this.audioPlayer.pause();
|
||||||
this.startedOn = null;
|
this.setState({playing: false});
|
||||||
} else {
|
} else {
|
||||||
this.audioPlayer.play();
|
this.audioPlayer.play();
|
||||||
this.startedOn = Date.now();
|
this.setState({playing: true}, this.visualize);
|
||||||
}
|
}
|
||||||
this.setState({playing: !this.state.playing});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleError(err) {
|
handleError(err) {
|
||||||
@@ -160,27 +214,47 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.target && this.props.duration) {
|
if (e.target && this.props.duration) {
|
||||||
const rect = e.target.getBoundingClientRect();
|
const rect = e.target.getBoundingClientRect();
|
||||||
const offset = (e.clientX - rect.left) / rect.width;
|
const offset = (e.clientX - rect.left) / this.effectiveWidth * CANVAS_UPSCALING;
|
||||||
this.audioPlayer.currentTime = this.props.duration * offset / 1000;
|
this.audioPlayer.currentTime = this.props.duration * offset / 1000;
|
||||||
this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime)});
|
this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime)});
|
||||||
|
if (!this.state.playing && this.state.preview) {
|
||||||
|
this.visualize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const playClass = 'material-icons large' + (this.state.canPlay ? '' : ' disabled');
|
const playClass = 'material-icons' +
|
||||||
return (
|
(this.props.short ? '' : ' large') +
|
||||||
<div className="audio-player">
|
(this.state.canPlay ? '' : ' disabled');
|
||||||
<a href="#" onClick={this.handlePlay} title="Play">
|
const play = (<a href="#" onClick={this.handlePlay} title="Play">
|
||||||
<i className={playClass}>{this.state.playing ? 'pause_circle' : 'play_circle'}</i>
|
<i className={playClass}>{this.state.playing ? 'pause_circle' : 'play_circle'}</i>
|
||||||
</a>
|
</a>);
|
||||||
|
return (<div className="audio-player">{this.props.short ?
|
||||||
|
<>
|
||||||
<div>
|
<div>
|
||||||
{this.state.preview ?
|
{this.state.preview ?
|
||||||
<canvas className="visualiser" ref={this.canvasRef} onClick={this.handleSeek} /> :
|
<canvas className="visualiser" ref={this.canvasRef} onClick={this.handleSeek} /> :
|
||||||
<div className="visualiser">unavailable</div>
|
<div className="visualiser"> - </div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{play}
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<>
|
||||||
|
{play}
|
||||||
|
<div>
|
||||||
|
{this.state.preview ?
|
||||||
|
<canvas className="visualiser" ref={this.canvasRef} onClick={this.handleSeek} /> :
|
||||||
|
<div className="visualiser">
|
||||||
|
<FormattedMessage
|
||||||
|
id="preview_unavailable" defaultMessage="unavailable" description="Message shown when media preview is not available" />
|
||||||
|
</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>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import AudioPlayer from './audio-player.jsx';
|
||||||
|
|
||||||
|
import { intArrayToBase64 } from '../lib/blob-helpers.js'
|
||||||
import { secondsToTime } from '../lib/strformat';
|
import { secondsToTime } from '../lib/strformat';
|
||||||
|
|
||||||
// FFT resolution.
|
// FFT resolution.
|
||||||
@@ -34,11 +37,14 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
audioRecord: null,
|
audioRecord: null,
|
||||||
recording: true,
|
recording: true,
|
||||||
paused: false,
|
paused: false,
|
||||||
duration: '0:00'
|
duration: '0:00',
|
||||||
|
blobUrl: null,
|
||||||
|
preview: null
|
||||||
};
|
};
|
||||||
|
|
||||||
this.visualize = this.visualize.bind(this);
|
this.visualize = this.visualize.bind(this);
|
||||||
this.initMediaRecording = this.initMediaRecording.bind(this);
|
this.initMediaRecording = this.initMediaRecording.bind(this);
|
||||||
|
this.getRecording = this.getRecording.bind(this);
|
||||||
this.cleanUp = this.cleanUp.bind(this);
|
this.cleanUp = this.cleanUp.bind(this);
|
||||||
|
|
||||||
this.handleResume = this.handleResume.bind(this);
|
this.handleResume = this.handleResume.bind(this);
|
||||||
@@ -159,9 +165,10 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
handlePause(e) {
|
handlePause(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.mediaRecorder.pause();
|
this.mediaRecorder.pause();
|
||||||
|
this.mediaRecorder.requestData();
|
||||||
this.durationMillis += Date.now() - this.startedOn;
|
this.durationMillis += Date.now() - this.startedOn;
|
||||||
this.startedOn = null;
|
this.startedOn = null;
|
||||||
this.setState({recording: false, duration: secondsToTime(this.durationMillis / 1000)});
|
this.setState({recording: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResume(e) {
|
handleResume(e) {
|
||||||
@@ -207,13 +214,9 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
this.audioInput.connect(this.analyser);
|
this.audioInput.connect(this.analyser);
|
||||||
|
|
||||||
this.mediaRecorder.onstop = _ => {
|
this.mediaRecorder.onstop = _ => {
|
||||||
const blob = new Blob(this.audioChunks, {'type' : 'audio/mp3; codecs=mp3'});
|
|
||||||
this.audioChunks = [];
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
if (this.durationMillis > MIN_DURATION) {
|
if (this.durationMillis > MIN_DURATION) {
|
||||||
blob.arrayBuffer().then(ab => this.audioContext.decodeAudioData(ab))
|
this.getRecording()
|
||||||
.then(data => this.createPreview(data))
|
.then(result => this.props.onFinished(result.url, result.preview, this.durationMillis));
|
||||||
.then(preview => this.props.onFinished(url, preview, this.durationMillis));
|
|
||||||
} else {
|
} else {
|
||||||
this.props.onDeleted();
|
this.props.onDeleted();
|
||||||
}
|
}
|
||||||
@@ -224,6 +227,15 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
if (e.data.size > 0) {
|
if (e.data.size > 0) {
|
||||||
this.audioChunks.push(e.data);
|
this.audioChunks.push(e.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mediaRecorder.state != 'inactive') {
|
||||||
|
this.getRecording().then(result => {
|
||||||
|
this.setState({
|
||||||
|
blobUrl: result.url,
|
||||||
|
preview: result.preview
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.durationMillis = 0;
|
this.durationMillis = 0;
|
||||||
@@ -232,6 +244,17 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
this.visualize();
|
this.visualize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Obtain data in a form sutable for sending or playing back.
|
||||||
|
getRecording() {
|
||||||
|
const blob = new Blob(this.audioChunks, {'type' : 'audio/mp3; codecs=mp3'});
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
return blob.arrayBuffer().then(ab => this.audioContext.decodeAudioData(ab))
|
||||||
|
.then(data => this.createPreview(data))
|
||||||
|
.then(preview => {
|
||||||
|
return {url: url, preview: intArrayToBase64(preview)}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Preview must be calculated at the source: Chrome does not allow background AudioContext.
|
// Preview must be calculated at the source: Chrome does not allow background AudioContext.
|
||||||
createPreview(audio) {
|
createPreview(audio) {
|
||||||
const data = audio.getChannelData(0);
|
const data = audio.getChannelData(0);
|
||||||
@@ -269,7 +292,15 @@ export default class AudioRecorder extends React.PureComponent {
|
|||||||
<a href="#" onClick={this.handleDelete} title="Delete">
|
<a href="#" onClick={this.handleDelete} title="Delete">
|
||||||
<i className="material-icons">delete_outline</i>
|
<i className="material-icons">delete_outline</i>
|
||||||
</a>
|
</a>
|
||||||
<canvas className="visualiser" ref={this.canvasRef} />
|
{this.state.recording ?
|
||||||
|
<canvas className="visualiser" ref={this.canvasRef} />
|
||||||
|
:
|
||||||
|
<AudioPlayer
|
||||||
|
src={this.state.blobUrl}
|
||||||
|
preview={this.state.preview}
|
||||||
|
duration={this.durationMillis}
|
||||||
|
short={true} />
|
||||||
|
}
|
||||||
<div className="duration">{this.state.duration}</div>
|
<div className="duration">{this.state.duration}</div>
|
||||||
{this.state.recording ?
|
{this.state.recording ?
|
||||||
<a href="#" onClick={this.handlePause} title="Pause">
|
<a href="#" onClick={this.handlePause} title="Pause">
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class SendMessage extends React.PureComponent {
|
|||||||
quote: null,
|
quote: null,
|
||||||
message: '',
|
message: '',
|
||||||
audioRec: false,
|
audioRec: false,
|
||||||
audioEnabled: !!navigator.mediaDevices.getUserMedia,
|
audioEnabled: !!navigator.mediaDevices.getUserMedia && this.props.onAttachAudio,
|
||||||
// Make initial keypress time as if it happened 5001 milliseconds in the past.
|
// Make initial keypress time as if it happened 5001 milliseconds in the past.
|
||||||
keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1
|
keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1
|
||||||
};
|
};
|
||||||
|
|||||||
+203
-85
File diff suppressed because one or more lines are too long
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