Skip to content

Commit

Permalink
Record Desktop Audio
Browse files Browse the repository at this point in the history
This PR allows Studio to record desktop audio and resolves #551

Unfortunately, not all browsers are supported yet, as you can see here: https://caniuse.com/mdn-api_mediadevices_getdisplaymedia_audio_capture_support and as Lukas mentioned here: [#551](#551)

The users get a hint after selecting display as source, with which browsers they can record desktop audio.

Output of the recordings for download:

Only camera recorded: Nothing changed, the finished camera video will contain microphone audio (if recorded)

Only display recorded: If the user recorded both, desktop audio and microphone audio, the finished video will have both audio tracks.

Display and camera recorded: The finished desktop video will contain desktop audio and the finished camera video will contain the microphone audio

One thing/problem:
In the "Review & trim" step (also after uploading to Opencast), if both the display AND the camera were recorded: you only hear the microphone audio or nothing (depending on whether microphone audio was recorded or not).
I'm not entirely sure why.
The audio is correct and audible for each of the videos that can be downloaded in the last step in Studio.
  • Loading branch information
narickmann authored and JulianKniephoff committed Nov 16, 2023
1 parent 539d62f commit db735d1
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 5 deletions.
2 changes: 1 addition & 1 deletion src/capturer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function startDisplayCapture(
...videoConstraints,
...height,
},
audio: false,
audio: true,
};

try {
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"warning-recorder-not-supported": "Your browser does not support recording media streams.",
"warning-recorder-safari-hint": "If you are using Safari, you can enable the experimental feature 'MediaRecorder' in settings. However, on macOS, we recommend switching to Chrome or Firefox.",

"desktop-audio-info": "Desktop audio is recorded",
"no-desktop-audio-info": "Desktop audio is not recorded. \n If you want to record desktop audio, please use one of the following browsers: \n - Google Chrome, Edge and Opera: On Windows, the entire system audio can be captured, but on Linux and macOS only the audio of a tab can be captured. \n - Firefox, Safari and Internet Explorer do not support recording desktop audio.",

"header": {
"info": {
"label": "Info"
Expand Down
40 changes: 38 additions & 2 deletions src/steps/recording/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,34 @@ const addRecordOnStop = (
};
};

const mixAudioIntoBothVideos = (audioStream: MediaStream | null, videoStream: MediaStream) => {
if (videoStream.getAudioTracks().length) {
return videoStream;
}

if (!(audioStream?.getAudioTracks().length)) {
return videoStream;
}

return new MediaStream([...videoStream.getVideoTracks(), ...audioStream.getAudioTracks()]);
};

const mixAudioIntoVideo = (audioStream: MediaStream | null, videoStream: MediaStream) => {
if (!(audioStream?.getAudioTracks().length)) {
return videoStream;
}

if (videoStream?.getAudioTracks().length) {
const audioContext = new AudioContext();
const desktopAudio = audioContext.createMediaStreamSource(videoStream);
const micAudio = audioContext.createMediaStreamSource(audioStream);
const dest = audioContext.createMediaStreamDestination();
desktopAudio.connect(dest);
micAudio.connect(dest);

return new MediaStream([...videoStream.getVideoTracks(), ...dest.stream.getAudioTracks()]);
}

return new MediaStream([...videoStream.getVideoTracks(), ...audioStream.getAudioTracks()]);
};

Expand Down Expand Up @@ -68,14 +92,26 @@ export const Recording: React.FC<StepProps> = ({ goToNextStep, goToPrevStep }) =
// sure, in case of a bug elsewhere, we clear the recordings here.
dispatch({ type: "CLEAR_RECORDINGS" });

if (displayStream) {
if (displayStream && userStream){

Check warning on line 95 in src/steps/recording/index.tsx

View workflow job for this annotation

GitHub Actions / main

Missing space before opening brace
const onStopDesktop = addRecordOnStop(dispatch, "desktop");
const onStopCamera = addRecordOnStop(dispatch, "video");
const desktopStream = mixAudioIntoBothVideos(state.audioStream, displayStream);
const cameraStream = mixAudioIntoBothVideos(state.audioStream, userStream);

desktopRecorder.current = new Recorder(desktopStream, settings.recording, onStopDesktop);
desktopRecorder.current.start();
videoRecorder.current = new Recorder(cameraStream, settings.recording, onStopCamera);
videoRecorder.current.start();
}

Check warning on line 105 in src/steps/recording/index.tsx

View workflow job for this annotation

GitHub Actions / main

Closing curly brace does not appear on the same line as the subsequent block

else if (displayStream) {
const onStop = addRecordOnStop(dispatch, "desktop");
const stream = mixAudioIntoVideo(state.audioStream, displayStream);
desktopRecorder.current = new Recorder(stream, settings.recording, onStop);
desktopRecorder.current.start();
}

Check warning on line 112 in src/steps/recording/index.tsx

View workflow job for this annotation

GitHub Actions / main

Closing curly brace does not appear on the same line as the subsequent block

if (userStream) {
else if (userStream) {
const onStop = addRecordOnStop(dispatch, "video");
const stream = mixAudioIntoVideo(state.audioStream, userStream);
videoRecorder.current = new Recorder(stream, settings.recording, onStop);
Expand Down
52 changes: 51 additions & 1 deletion src/steps/video-setup/prefs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Floating, FloatingContainer, FloatingHandle, FloatingTrigger, ProtoButton,
WithTooltip, screenWidthAtMost, useColorScheme,
} from "@opencast/appkit";
import { FiSettings, FiX } from "react-icons/fi";
import { FiSettings, FiX, FiInfo, FiVolumeX, FiVolume2 } from "react-icons/fi";

import { Settings, useSettings } from "../../settings";
import { COLORS, getUniqueDevices } from "../../util";
Expand Down Expand Up @@ -124,6 +124,56 @@ type StreamSettingsProps = {
stream: MediaStream | null;
}

export const DesktopAudioInfo: React.FC<StreamSettingsProps> = ({ isDesktop, stream }) => {
const { t } = useTranslation();
const audioLength = stream?.getAudioTracks().length;

const hasDesktopAudio = () => {
if (audioLength !== undefined && audioLength > 0) {
return true;
}
return false;
};

return (
<div css={{
display: isDesktop ? "initial" : "none",
position: "absolute",
top: 12,
right: 12,
whiteSpace: "pre-line",
}}>
<WithTooltip
placement="bottom"
tooltip={hasDesktopAudio() ? t("desktop-audio-info") : t("no-desktop-audio-info")}
>
<ProtoButton
css={{
display: "inline-block",
backgroundColor: "rgba(0, 0, 0, 0.7)",
color: "white",
borderRadius: "5px",
padding: 4,
fontSize: 15,
backdropFilter: "invert(1)",
lineHeight: 0,
cursor: "pointer",
"&:hover, &:focus-visible": {
backgroundColor: "rgba(0, 0, 0, 0.9)",
},
"&:focus-visible": {
outline: "5px dashed white",
outlineOffset: -2.5,
},
}}
>
<FiInfo /> {hasDesktopAudio() ? <FiVolume2 /> : <FiVolumeX />}
</ProtoButton>
</WithTooltip>
</div>
);
};

export const StreamSettings: React.FC<StreamSettingsProps> = ({ isDesktop, stream }) => {
const dispatch = useDispatch();
const settings = useSettings();
Expand Down
3 changes: 2 additions & 1 deletion src/steps/video-setup/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Spinner, match, unreachable, useColorScheme } from "@opencast/appkit";
import { useTranslation } from "react-i18next";

import { COLORS, dimensionsOf } from "../../util";
import { StreamSettings } from "./prefs";
import { StreamSettings, DesktopAudioInfo } from "./prefs";
import { Input } from ".";
import { VideoBox, useVideoBoxResize } from "../../ui/VideoBox";
import { ErrorBox } from "../../ui/ErrorBox";
Expand Down Expand Up @@ -63,6 +63,7 @@ const StreamPreview: React.FC<{ input: Input }> = ({ input }) => {
},
}}>
<PreviewVideo input={input} />
<DesktopAudioInfo isDesktop={input.isDesktop} stream={input.stream} />
{input.stream && <StreamSettings isDesktop={input.isDesktop} stream={input.stream} />}
</div>
);
Expand Down

0 comments on commit db735d1

Please sign in to comment.