Add an early voice recorder utility class

This commit is contained in:
Travis Ralston 2021-03-11 22:05:47 -07:00
parent 097c2d8be0
commit be2e30df0d
5 changed files with 126 additions and 0 deletions

View file

@ -83,6 +83,7 @@
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^0.1.0-beta.13", "matrix-widget-api": "^0.1.0-beta.13",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3", "pako": "^2.0.3",
"parse5": "^6.0.1", "parse5": "^6.0.1",
"png-chunks-extract": "^1.0.0", "png-chunks-extract": "^1.0.0",

View file

@ -39,6 +39,7 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper"; import VoipUserMapper from "../VoipUserMapper";
import {SpaceStoreClass} from "../stores/SpaceStore"; import {SpaceStoreClass} from "../stores/SpaceStore";
import {VoiceRecorder} from "../voice/VoiceRecorder";
declare global { declare global {
interface Window { interface Window {
@ -70,6 +71,7 @@ declare global {
mxModalWidgetStore: ModalWidgetStore; mxModalWidgetStore: ModalWidgetStore;
mxVoipUserMapper: VoipUserMapper; mxVoipUserMapper: VoipUserMapper;
mxSpaceStore: SpaceStoreClass; mxSpaceStore: SpaceStoreClass;
mxVoiceRecorder: typeof VoiceRecorder;
} }
interface Document { interface Document {

View file

@ -28,3 +28,5 @@ export function resetSkin() {
export function getComponent(componentName) { export function getComponent(componentName) {
return Skinner.getComponent(componentName); return Skinner.getComponent(componentName);
} }
import "./voice/VoiceRecorder"; // TODO: @@ REMOVE

116
src/voice/VoiceRecorder.ts Normal file
View file

@ -0,0 +1,116 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as Recorder from 'opus-recorder';
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
import {MatrixClient} from "matrix-js-sdk/src/client";
import CallMediaHandler from "../CallMediaHandler";
import {sleep} from "../utils/promise";
export class VoiceRecorder {
private recorder = new Recorder({
encoderPath, // magic from webpack
mediaTrackConstraints: <MediaTrackConstraints>{
deviceId: CallMediaHandler.getAudioInput(),
},
encoderSampleRate: 16000, // we could go down to 12khz, but we lose quality
encoderApplication: 2048, // voice (default is "audio")
streamPages: true, // so we can have a live EQ for the user
encoderFrameSize: 10, // we want updates fairly regularly for the UI
});
private buffer = new Uint8Array(0);
private mxc: string;
private recording = false;
public constructor(private client: MatrixClient) {
this.recorder.ondataavailable = (a: ArrayBuffer) => {
// TODO: @@ We'll have to decode each frame and convert it to an EQ to observe
console.log(a);
const buf = new Uint8Array(a);
const newBuf = new Uint8Array(this.buffer.length + buf.length);
newBuf.set(this.buffer, 0);
newBuf.set(buf, this.buffer.length);
this.buffer = newBuf;
};
}
public get isSupported(): boolean {
return !!Recorder.isRecordingSupported();
}
public get hasRecording(): boolean {
return this.buffer.length > 0;
}
public get mxcUri(): string {
if (!this.mxc) {
throw new Error("Recording has not been uploaded yet");
}
return this.mxc;
}
public async start(): Promise<void> {
if (this.mxc || this.hasRecording) {
throw new Error("Recording already prepared");
}
if (this.recording) {
throw new Error("Recording already in progress");
}
return this.recorder.start().then(() => this.recording = true);
}
public async stop(): Promise<Uint8Array> {
if (!this.recording) {
throw new Error("No recording to stop");
}
return new Promise<Uint8Array>(resolve => {
this.recorder.stop().then(() => {
this.recording = false;
return this.recorder.close();
}).then(() => resolve(this.buffer));
});
}
public async upload(): Promise<string> {
if (!this.hasRecording) {
throw new Error("No recording available to upload");
}
if (this.mxc) return this.mxc;
this.mxc = await this.client.uploadContent(new Blob([this.buffer], {
type: "audio/ogg",
}), {
onlyContentUri: false, // to stop the warnings in the console
}).then(r => r['content_uri']);
return this.mxc;
}
// TODO: @@ REMOVE
public async test() {
this.start()
.then(() => sleep(5000))
.then(() => this.stop())
.then(() => this.upload())
.then(() => this.client.sendMessage("!HKjSnKDluFnCCnjayl:localhost", {
body: "Voice message",
msgtype: "m.audio", // TODO
url: this.mxc,
}));
}
}
window.mxVoiceRecorder = VoiceRecorder;

View file

@ -6096,6 +6096,11 @@ optionator@^0.9.1:
type-check "^0.4.0" type-check "^0.4.0"
word-wrap "^1.2.3" word-wrap "^1.2.3"
opus-recorder@^8.0.3:
version "8.0.3"
resolved "https://registry.yarnpkg.com/opus-recorder/-/opus-recorder-8.0.3.tgz#f7b44f8f68500c9b96a15042a69f915fd9c1716d"
integrity sha512-8vXGiRwlJAavT9D3yYzukNVXQ8vEcKHcsQL/zXO24DQtJ0PLXvoPHNQPJrbMCdB4ypJgWDExvHF4JitQDL7dng==
os-tmpdir@~1.0.2: os-tmpdir@~1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"