Add an early voice recorder utility class
This commit is contained in:
parent
097c2d8be0
commit
be2e30df0d
5 changed files with 126 additions and 0 deletions
|
@ -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",
|
||||||
|
|
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
|
@ -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 {
|
||||||
|
|
|
@ -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
116
src/voice/VoiceRecorder.ts
Normal 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;
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue