Skip to content

Commit

Permalink
Fix audio related issues (wulkano#710)
Browse files Browse the repository at this point in the history
* Ensure we have microphone access before opening cropper

* Only open cropper at startup if access allows it

* Add custom message when requesting microphone access

* Fix hover and active colors for unmute button

* Handle turning record sound setting on

* Ensure muting and play/pause works correctly in the editor

* Add system permissions helper

* Text changes

Co-Authored-By: Sindre Sorhus <[email protected]>

* asked -> hasAsked and early return
  • Loading branch information
karaggeorge authored Sep 6, 2019
1 parent ddba608 commit 3551779
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 18 deletions.
54 changes: 54 additions & 0 deletions main/common/system-permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const {systemPreferences, shell, dialog} = require('electron');

const getMicrophoneAccess = () => systemPreferences.getMediaAccessStatus('microphone');

const promptSystemPreferences = async ({hasAsked} = {}) => {
if (hasAsked) {
return false;
}

const {response} = await dialog.showMessageBox({
type: 'warning',
buttons: ['Open System Preferences', 'Cancel'],
defaultId: 0,
message: 'Kap cannot access the microphone.',
detail: 'Kap requires microphone access to be able to record audio. You can grant this in the System Preferences. Afterwards, relaunch Kap for the changes to take effect.',
cancelId: 1
});

if (response === 0) {
openSystemPreferences();
}

return false;
};

const ensureMicrophonePermissions = async (fallback = promptSystemPreferences) => {
const access = getMicrophoneAccess();

if (access === 'granted') {
return true;
}

if (access !== 'denied') {
const granted = await systemPreferences.askForMediaAccess('microphone');

if (granted) {
return true;
}

return fallback({hasAsked: true});
}

return fallback();
};

const openSystemPreferences = () => shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone');

const hasMicrophoneAccess = () => getMicrophoneAccess() === 'granted';

module.exports = {
ensureMicrophonePermissions,
hasMicrophoneAccess,
openSystemPreferences
};
36 changes: 34 additions & 2 deletions main/cropper.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ const electron = require('electron');
const delay = require('delay');

const settings = require('./common/settings');
const {hasMicrophoneAccess, ensureMicrophonePermissions, openSystemPreferences} = require('./common/system-permissions');
const loadRoute = require('./utils/routes');

const {BrowserWindow, systemPreferences} = electron;
const {BrowserWindow, systemPreferences, dialog} = electron;

const croppers = new Map();
let notificationId = null;
Expand Down Expand Up @@ -79,9 +80,40 @@ const openCropper = (display, activeDisplayId) => {
return cropper;
};

const openCropperWindow = () => {
const openCropperWindow = async () => {
closeAllCroppers();

const recordAudio = settings.get('recordAudio');

if (recordAudio && !hasMicrophoneAccess()) {
const granted = await ensureMicrophonePermissions(async () => {
const {response} = await dialog.showMessageBox({
type: 'warning',
buttons: ['Open System Preferences', 'Continue'],
defaultId: 1,
message: 'Kap cannot access the microphone.',
detail: 'Audio recording is enabled but Kap does not have access to the microphone. Continue without audio or grant Kap access to the microphone the System Preferences.',
cancelId: 2
});

if (response === 0) {
openSystemPreferences();
return false;
}

if (response === 1) {
settings.set('recordAudio', false);
return true;
}

return false;
});

if (!granted) {
return;
}
}

const {screen} = electron;
const displays = screen.getAllDisplays();
const activeDisplayId = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()).id;
Expand Down
7 changes: 6 additions & 1 deletion main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const {initializeGlobalAccelerators} = require('./global-accelerators');
const {setApplicationMenu} = require('./menus');
const openFiles = require('./utils/open-files');
const {initializeExportOptions} = require('./export-options');
const settings = require('./common/settings');
const {hasMicrophoneAccess} = require('./common/system-permissions');

require('./utils/sentry');

Expand Down Expand Up @@ -86,7 +88,10 @@ const checkForUpdates = () => {
if (filesToOpen.length > 0) {
track('editor/opened/startup');
openFiles(...filesToOpen);
} else if (!app.getLoginItemSettings().wasOpenedAtLogin) {
} else if (
!app.getLoginItemSettings().wasOpenedAtLogin &&
(!settings.get('recordAudio') || hasMicrophoneAccess())
) {
openCropperWindow();
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"minimumSystemVersion": "10.12.0",
"darkModeSupport": true,
"extendInfo": {
"NSMicrophoneUsageDescription": "Kap needs access to the microphone to be able to record audio for screen recordings.",
"NSUserNotificationAlertStyle": "alert",
"CFBundleDocumentTypes": [
{
Expand Down
6 changes: 3 additions & 3 deletions renderer/components/editor/controls/right.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import formatTime from '../../../utils/format-time';
class RightControls extends React.Component {
render() {
const {isMuted, mute, unmute, format, duration, hasAudio} = this.props;
const canUnmute = !['gif', 'apng'].includes(format);
const unmuteColor = canUnmute && hasAudio ? '#fff' : 'rgba(255, 255, 255, 0.40)';
const canUnmute = !['gif', 'apng'].includes(format) && hasAudio;
const unmuteColor = canUnmute ? '#fff' : 'rgba(255, 255, 255, 0.40)';
return (
<div className="container">
<div className="time">{formatTime(duration)}</div>
<div className="mute">
{
isMuted || !hasAudio ?
<VolumeOffIcon shadow fill={unmuteColor} hoverFill={unmuteColor} onClick={canUnmute && hasAudio ? unmute : undefined}/> :
<VolumeOffIcon shadow fill={unmuteColor} hoverFill={unmuteColor} tabIndex={canUnmute ? undefined : -1} onClick={canUnmute ? unmute : undefined}/> :
<VolumeHighIcon shadow fill="#fff" hoverFill="#fff" onClick={mute}/>
}
</div>
Expand Down
6 changes: 5 additions & 1 deletion renderer/components/preferences/categories/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class General extends React.Component {
recordKeyboardShortcut,
loopExports,
toggleSetting,
toggleRecordAudio,
audioInputDeviceId,
setAudioInputDeviceId,
audioDevices,
Expand Down Expand Up @@ -151,7 +152,7 @@ class General extends React.Component {
<Switch
tabIndex={tabIndex}
checked={recordAudio}
onClick={() => toggleSetting('recordAudio')}/>
onClick={toggleRecordAudio}/>
</Item>
<Item key="audioInputDeviceId" subtitle="Select input device">
<Select
Expand Down Expand Up @@ -209,6 +210,7 @@ General.propTypes = {
record60fps: PropTypes.bool,
recordKeyboardShortcut: PropTypes.bool,
toggleSetting: PropTypes.elementType.isRequired,
toggleRecordAudio: PropTypes.elementType.isRequired,
audioInputDeviceId: PropTypes.string,
setAudioInputDeviceId: PropTypes.elementType.isRequired,
audioDevices: PropTypes.array,
Expand Down Expand Up @@ -268,13 +270,15 @@ export default connect(
}),
({
toggleSetting,
toggleRecordAudio,
setAudioInputDeviceId,
pickKapturesDir,
setOpenOnStartup,
updateShortcut,
toggleShortcuts
}) => ({
toggleSetting,
toggleRecordAudio,
setAudioInputDeviceId,
pickKapturesDir,
setOpenOnStartup,
Expand Down
12 changes: 7 additions & 5 deletions renderer/containers/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,13 @@ export default class EditorContainer extends Container {
const {plugins} = options.find(option => option.format === format);
const newPlugin = plugins.find(p => p.title === plugin) ? plugin : plugins[0].title;

if (isMuted(format) && !isMuted(this.state.format)) {
this.setState({wasMuted: this.videoContainer.state.isMuted});
this.videoContainer.mute();
} else if (!isMuted(format) && isMuted(this.state.format) && !wasMuted) {
this.videoContainer.unmute();
if (this.videoContainer.state.hasAudio) {
if (isMuted(format) && !isMuted(this.state.format)) {
this.setState({wasMuted: this.videoContainer.state.isMuted});
this.videoContainer.mute();
} else if (!isMuted(format) && isMuted(this.state.format) && !wasMuted) {
this.videoContainer.unmute();
}
}

this.setState({format, plugin: newPlugin});
Expand Down
11 changes: 11 additions & 0 deletions renderer/containers/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default class PreferencesContainer extends Container {
mount = async setOverlay => {
this.setOverlay = setOverlay;
this.settings = this.remote.require('./common/settings');
this.systemPermissions = this.remote.require('./common/system-permissions');
this.plugins = this.remote.require('./common/plugins');
this.track = this.remote.require('./common/analytics').track;
this.ipc = require('electron-better-ipc').ipcRenderer;
Expand Down Expand Up @@ -145,6 +146,16 @@ export default class PreferencesContainer extends Container {
this.settings.set(setting, newVal);
}

toggleRecordAudio = async () => {
const newVal = !this.state.recordAudio;
this.track(`preferences/setting/recordAudio/${newVal}`);

if (!newVal || await this.systemPermissions.ensureMicrophonePermissions()) {
this.setState({recordAudio: newVal});
this.settings.set('recordAudio', newVal);
}
}

toggleShortcuts = async () => {
const setting = 'recordKeyboardShortcut';
const newVal = !this.state[setting];
Expand Down
18 changes: 13 additions & 5 deletions renderer/containers/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,18 @@ export default class VideoContainer extends Container {
video.addEventListener('loadedmetadata', () => {
const {videoWidth, videoHeight, duration} = video;
this.editorContainer.setDimensions(videoWidth, videoHeight);
this.setState({duration, startTime: 0, endTime: duration});
});

video.addEventListener('loadeddata', () => {
const hasAudio = video.webkitAudioDecodedByteCount > 0 ||
Boolean(video.audioTracks && video.audioTracks.length > 0);
this.setState({duration, startTime: 0, endTime: duration, hasAudio});
this.mute();

if (!hasAudio) {
this.mute();
}

this.setState({hasAudio});
});

video.addEventListener('canplaythrough', () => {
Expand Down Expand Up @@ -90,14 +98,14 @@ export default class VideoContainer extends Container {
});
}

play = () => {
play = async () => {
await this.video.play();
this.setState({isPaused: false});
this.video.play();
}

pause = () => {
this.setState({isPaused: true});
this.video.pause();
this.setState({isPaused: true});
}

mute = () => {
Expand Down
2 changes: 1 addition & 1 deletion renderer/vectors/svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class Svg extends React.Component {
.active,
.active:hover,
div:focus svg {
div.focusable:focus svg {
fill: ${activeFill};
}
`}</style>
Expand Down

0 comments on commit 3551779

Please sign in to comment.