Skip to content

Commit

Permalink
FG-2933: Add microphone capture (opus) to MCAP recording demo
Browse files Browse the repository at this point in the history
  • Loading branch information
fgwt202412 committed Dec 15, 2024
1 parent 36f7053 commit ad01ad1
Show file tree
Hide file tree
Showing 6 changed files with 314 additions and 15 deletions.
2 changes: 2 additions & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"@mcap/core": "workspace:*",
"@mdx-js/react": "1.6.22",
"@tsconfig/docusaurus": "1.0.7",
"@types/dom-mediacapture-transform": "0.1.10",
"@types/dom-webcodecs": "0.1.13",
"@types/promise-queue": "2.2.0",
"buffer": "6.0.3",
"classnames": "2.3.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
margin: 0;
}

.videoContainer {
.mediaContainer {
background-color: #6f3be80f;
border: 1px solid #e0e0e0;
font-size: 0.8rem;
Expand All @@ -120,11 +120,11 @@
overflow: hidden;
}

[data-theme="dark"] .videoContainer {
[data-theme="dark"] .mediaContainer {
background-color: transparent;
}

.videoContainer video {
.mediaContainer video {
width: 100%;
height: 100%;
position: absolute;
Expand All @@ -133,7 +133,7 @@
z-index: 0;
}

.videoContainer .videoErrorContainer {
.mediaContainer .mediaErrorContainer {
width: 100%;
height: 100%;
position: absolute;
Expand All @@ -143,16 +143,16 @@
z-index: 1;
}

[data-theme="dark"] .videoContainer .videoErrorContainer {
[data-theme="dark"] .mediaContainer .mediaErrorContainer {
background-color: #17151ec4;
}

.videoPlaceholderText {
.mediaPlaceholderText {
font-weight: 600;
cursor: pointer;
}

.videoLoadingIndicator {
.mediaLoadingIndicator {
position: absolute;
top: 50%;
left: 50%;
Expand Down
117 changes: 109 additions & 8 deletions website/src/components/McapRecordingDemo/McapRecordingDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import {
Recorder,
toProtobufTime,
} from "./Recorder";
import {
recordAudioStream,
startAudioStream,
CompressedAudioData,
startAudioCapture,
} from "./audioCapture";
import {
CompressedVideoFrame,
startVideoCapture,
Expand All @@ -37,6 +43,7 @@ type State = {
addPoseMessage: (msg: DeviceOrientationEvent) => void;
addJpegFrame: (blob: Blob) => void;
addVideoFrame: (frame: CompressedVideoFrame) => void;
addAudioData: (data: CompressedAudioData) => void;
closeAndRestart: () => Promise<Blob>;
};

Expand Down Expand Up @@ -70,6 +77,9 @@ const useStore = create<State>((set) => {
addVideoFrame(frame: CompressedVideoFrame) {
void recorder.addVideoFrame(frame);
},
addAudioData(data: CompressedAudioData) {
void recorder.addAudioData(data);
},
async closeAndRestart() {
return await recorder.closeAndRestart();
},
Expand Down Expand Up @@ -128,19 +138,27 @@ export function McapRecordingDemo(): JSX.Element {

const videoRef = useRef<HTMLVideoElement | undefined>();
const videoContainerRef = useRef<HTMLDivElement>(null);
const audioProgressRef = useRef<HTMLProgressElement>(null);
const [recordJpeg, setRecordJpeg] = useState(false);
const [recordH264, setRecordH264] = useState(false);
const [recordH265, setRecordH265] = useState(false);
const [recordVP9, setRecordVP9] = useState(false);
const [recordAV1, setRecordAV1] = useState(false);
const [recordOpus, setRecordOpus] = useState(false);
const [recordMouse, setRecordMouse] = useState(true);
const [recordOrientation, setRecordOrientation] = useState(true);
const [videoStarted, setVideoStarted] = useState(false);
const [videoError, setVideoError] = useState<Error | undefined>();
const [audioError, setAudioError] = useState<Error | undefined>();
const [showDownloadInfo, setShowDownloadInfo] = useState(false);

const { addJpegFrame, addVideoFrame, addMouseEventMessage, addPoseMessage } =
state;
const {
addJpegFrame,
addVideoFrame,
addMouseEventMessage,
addPoseMessage,
addAudioData,
} = state;

const { data: h264Support } = useAsync(supportsH264Encoding);
const { data: h265Support } = useAsync(supportsH265Encoding);
Expand All @@ -154,7 +172,8 @@ export function McapRecordingDemo(): JSX.Element {
(recordVP9 && !videoError) ||
(recordH265 && !videoError) ||
(recordH264 && !videoError) ||
(recordJpeg && !videoError);
(recordJpeg && !videoError) ||
(recordOpus && !audioError);

// Automatically pause recording after 30 seconds to avoid unbounded growth
useEffect(() => {
Expand Down Expand Up @@ -274,11 +293,62 @@ export function McapRecordingDemo(): JSX.Element {
recordH264,
recordH265,
recordAV1,
recordVP9,
recording,
videoStarted,
recordJpeg,
]);

const [audioStream, setAudioStream] = useState<MediaStream | undefined>(
undefined,
);

const enableMicrophone = recordOpus;
useEffect(() => {
const progress = audioProgressRef.current;
if (!progress || !enableMicrophone) {
return;
}

const cleanup = startAudioStream({
progress,
onAudioStream: (stream) => {
setAudioStream(stream);
},
onError: (err) => {
setAudioError(err);
console.error(err);
},
});

return () => {
cleanup();
setAudioStream(undefined);
setAudioError(undefined);
};
}, [enableMicrophone, recordOpus]);

useEffect(() => {
if (!enableMicrophone || !recording || !audioStream) {
return;
}

const cleanup = startAudioCapture({
enableOpus: recordOpus,
stream: audioStream,
onAudioData: (data) => {
addAudioData(data);
},
onError: (error) => {
setAudioError(error);
},
});

return () => {
cleanup();
};
}, [addAudioData, enableMicrophone, recordOpus, audioStream, recording]);

const onRecordClick = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
Expand Down Expand Up @@ -417,6 +487,16 @@ export function McapRecordingDemo(): JSX.Element {
/>
Camera (JPEG)
</label>
<label>
<input
type="checkbox"
checked={recordOpus}
onChange={(event) => {
setRecordOpus(event.target.checked);
}}
/>
Microphone (Opus)
</label>
{!hasMouse && (
<label>
<input
Expand Down Expand Up @@ -545,22 +625,24 @@ export function McapRecordingDemo(): JSX.Element {
</div>
</div>
</div>
</div>

<div className={styles.recordingControls}>
<div className={styles.recordingControlsColumn}>
<div className={styles.videoContainer} ref={videoContainerRef}>
<div className={styles.mediaContainer} ref={videoContainerRef}>
{videoError ? (
<div className={cx(styles.error, styles.videoErrorContainer)}>
<div className={cx(styles.error, styles.mediaErrorContainer)}>
{videoError.toString()}
</div>
) : recordH264 || recordJpeg ? (
) : enableCamera ? (
<>
{!videoStarted && (
<progress className={styles.videoLoadingIndicator} />
<progress className={styles.mediaLoadingIndicator} />
)}
</>
) : (
<span
className={styles.videoPlaceholderText}
className={styles.mediaPlaceholderText}
onClick={() => {
if (av1Support?.supported === true) {
setRecordAV1(true);
Expand All @@ -578,6 +660,25 @@ export function McapRecordingDemo(): JSX.Element {
)}
</div>
</div>

<div className={styles.recordingControlsColumn}>
<div className={styles.mediaContainer}>
{audioError ? (
<div className={cx(styles.error, styles.mediaErrorContainer)}>
{audioError.toString()}
</div>
) : enableMicrophone ? (
<progress
className={styles.mediaLoadingIndicator}
ref={audioProgressRef}
/>
) : (
<span className={styles.mediaPlaceholderText}>
Enable “Microphone” to record audio
</span>
)}
</div>
</div>
</div>
</div>
</section>
Expand Down
Loading

0 comments on commit ad01ad1

Please sign in to comment.