web/components/Recorder/index.tsx (131 lines of code) (raw):

/** * Copyright 2021 Google LLC * * 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 { createStyles, makeStyles, Theme, IconButton, Grid, TextField, } from "@material-ui/core"; import { Mic, Stop } from "@material-ui/icons"; import { ChangeEvent, useEffect, useRef, useState } from "react"; import { Autocomplete } from "@material-ui/lab"; import { DisplayRecorder, Canvas } from "./Recorder"; const useStyles = makeStyles((theme: Theme) => createStyles({ formControl: { minWidth: 350, }, canvas: { borderColor: theme.palette.action.disabled, borderWidth: 1, borderStyle: "solid", }, }) ); export type Language = { name: string; code: string; }; export type OnStopCallback = ( lang: string, duration: number | null, blob: Blob ) => Promise<void>; type Props = { languages: Language[]; defaultLanguage: string; onStart: (lang: string) => void; onStop: OnStopCallback; }; const Recorder: React.FC<Props> = ({ languages, defaultLanguage, onStart, onStop, }) => { const classes = useStyles(); const canvasRef = useRef<HTMLCanvasElement | null>(null); const [lang, setLang] = useState(defaultLanguage); const [isRecording, setIsRecording] = useState(false); const [isStopping, setIsStopping] = useState(false); const [recorder, setRecorder] = useState<DisplayRecorder | null>(null); const [startTime, setStartTime] = useState<number | null>(null); useEffect(() => { const canvas = canvasRef.current; if (canvas === null) { return; } const context = canvas.getContext("2d"); if (context !== null) { const arg: Canvas = { width: canvas.width, height: canvas.height, context: context, }; setRecorder(new DisplayRecorder(arg)); } }, []); const onChangeLang = ( _event: ChangeEvent<Record<string, never>>, value: Language | null ) => { if (value === null) { return; } setLang(value.code); }; const onClickStart = async () => { if (recorder === null) return; setStartTime(Date.now()); onStart(lang); setIsRecording(true); await recorder.start(); }; const onClickStop = () => { if (recorder === null) return; setIsStopping(true); let duration: number | null; if (startTime) { duration = Date.now() - startTime; } recorder.stop(async (blob: Blob) => await onStop(lang, duration, blob)); setIsRecording(false); setIsStopping(false); }; return ( <Grid container direction="row" alignItems="center" spacing={2}> <Grid item> <Autocomplete options={languages} getOptionLabel={(option) => option.name} getOptionSelected={(option, value) => option.code === value.code} renderInput={(params) => ( <TextField {...params} label="I speak" variant="outlined" className={classes.formControl} /> )} onChange={onChangeLang} /> </Grid> <Grid item> {isRecording ? ( <IconButton aria-label="stop-recording" onClick={onClickStop}> <Stop /> </IconButton> ) : ( <IconButton aria-label="start-recording" onClick={onClickStart} disabled={isStopping || lang === ""} > <Mic /> </IconButton> )} </Grid> <Grid item> <canvas ref={canvasRef} height="60px" className={classes.canvas} /> </Grid> </Grid> ); }; export default Recorder;