Skip to content

Commit 25114c2

Browse files
steveseguinclaude
andcommitted
Support both sandbox and non-sandbox modes for native modules
- Try direct require first (works when sandbox disabled) - Fall back to IPC-based approach when in sandbox mode - Add async variants of ASIO functions for IPC mode - Add onAsioAudioData/onAsioError subscriptions for IPC mode - Graceful degradation: no more "module not found" errors in sandbox mode Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 51b0d9d commit 25114c2

File tree

1 file changed

+134
-22
lines changed

1 file changed

+134
-22
lines changed

preload.js

Lines changed: 134 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
// preload.js
22
const { ipcRenderer, contextBridge } = require('electron');
33

4+
// Native modules - try direct require first (works when sandbox disabled),
5+
// fall back to IPC-based approach when in sandbox mode.
6+
47
let WindowAudioStream = null;
8+
let ElectronAsio = null;
9+
let useIpcForWindowAudio = false;
10+
let useIpcForAsio = false;
11+
12+
// Try loading WindowAudioStream directly (sandbox disabled mode)
513
try {
614
WindowAudioStream = require('./window-audio-stream.js');
715
if (WindowAudioStream && typeof WindowAudioStream !== 'function' && WindowAudioStream.default) {
816
WindowAudioStream = WindowAudioStream.default;
917
}
18+
console.log('[Electron Capture] WindowAudioStream loaded directly');
1019
} catch (error) {
11-
console.error('Failed to load WindowAudioStream module:', error);
20+
// Sandbox mode - use IPC fallback
21+
useIpcForWindowAudio = true;
22+
console.log('[Electron Capture] WindowAudioStream will use IPC (sandbox mode)');
1223
}
1324

14-
// ASIO Audio Capture (Windows only - for professional audio interfaces)
15-
let ElectronAsio = null;
16-
try {
17-
ElectronAsio = require('./native-modules/electron-asio/index.js');
18-
if (ElectronAsio && ElectronAsio.initialize) {
19-
ElectronAsio.initialize();
20-
console.log('[Electron Capture] ASIO module loaded:', ElectronAsio.getVersionInfo());
21-
}
22-
} catch (error) {
23-
// ASIO is optional and Windows-only
24-
if (process.platform === 'win32') {
25-
console.warn('ASIO module not available:', error.message);
25+
// Try loading ElectronAsio directly (sandbox disabled mode, Windows only)
26+
if (process.platform === 'win32') {
27+
try {
28+
ElectronAsio = require('./native-modules/electron-asio/index.js');
29+
if (ElectronAsio && ElectronAsio.initialize) {
30+
ElectronAsio.initialize();
31+
console.log('[Electron Capture] ASIO module loaded directly:', ElectronAsio.getVersionInfo());
32+
}
33+
} catch (error) {
34+
// Sandbox mode - use IPC fallback
35+
useIpcForAsio = true;
36+
console.log('[Electron Capture] ASIO will use IPC (sandbox mode)');
2637
}
2738
}
2839

@@ -715,7 +726,19 @@ function createElectronApi() {
715726
return { success: true, target: updated };
716727
},
717728
'getAppAudioTarget': () => appAudioTarget,
718-
'isWindowAudioCaptureAvailable': () => Boolean(WindowAudioStream),
729+
'isWindowAudioCaptureAvailable': () => {
730+
// Direct mode: check module loaded
731+
if (!useIpcForWindowAudio) return Boolean(WindowAudioStream);
732+
// IPC mode: assume available if main process loaded it (async check done at startup)
733+
return true; // Actual availability checked via windowAudio:getTargets
734+
},
735+
'isWindowAudioCaptureAvailableAsync': async () => {
736+
if (!useIpcForWindowAudio) return Boolean(WindowAudioStream);
737+
try {
738+
const result = await ipcRenderer.invoke('windowAudio:getTargets');
739+
return result && result.success !== false;
740+
} catch { return false; }
741+
},
719742
// Custom Electron capture preferences (v39.2.7+)
720743
'getCapturePreferences': () => ({ ...capturePreferences }),
721744
'getPlayoutDelay': () => capturePreferences.playoutDelay,
@@ -731,16 +754,105 @@ function createElectronApi() {
731754
}
732755
return false;
733756
},
734-
// ASIO Audio Capture (Windows only)
735-
'isAsioAvailable': () => Boolean(ElectronAsio && ElectronAsio.isAvailable && ElectronAsio.isAvailable()),
736-
'getAsioDevices': () => ElectronAsio ? ElectronAsio.getDevices() : [],
737-
'getAsioDeviceInfo': (deviceIndex) => ElectronAsio ? ElectronAsio.getDeviceInfo(deviceIndex) : null,
738-
'getAsioVersionInfo': () => ElectronAsio ? ElectronAsio.getVersionInfo() : 'ASIO not available',
757+
// ASIO Audio Capture (Windows only) - supports both direct and IPC modes
758+
'isAsioAvailable': () => {
759+
if (!useIpcForAsio) {
760+
return Boolean(ElectronAsio && ElectronAsio.isAvailable && ElectronAsio.isAvailable());
761+
}
762+
// IPC mode: return false synchronously, use async version for accurate check
763+
return false;
764+
},
765+
'isAsioAvailableAsync': async () => {
766+
if (!useIpcForAsio) {
767+
return Boolean(ElectronAsio && ElectronAsio.isAvailable && ElectronAsio.isAvailable());
768+
}
769+
try {
770+
return await ipcRenderer.invoke('asio:isAvailable');
771+
} catch { return false; }
772+
},
773+
'getAsioDevices': () => {
774+
if (!useIpcForAsio) return ElectronAsio ? ElectronAsio.getDevices() : [];
775+
console.warn('getAsioDevices: Use getAsioDevicesAsync in sandbox mode');
776+
return [];
777+
},
778+
'getAsioDevicesAsync': async () => {
779+
if (!useIpcForAsio) return ElectronAsio ? ElectronAsio.getDevices() : [];
780+
try {
781+
return await ipcRenderer.invoke('asio:getDevices');
782+
} catch { return []; }
783+
},
784+
'getAsioDeviceInfo': (deviceIndex) => {
785+
if (!useIpcForAsio) return ElectronAsio ? ElectronAsio.getDeviceInfo(deviceIndex) : null;
786+
console.warn('getAsioDeviceInfo: Use getAsioDeviceInfoAsync in sandbox mode');
787+
return null;
788+
},
789+
'getAsioDeviceInfoAsync': async (deviceIndex) => {
790+
if (!useIpcForAsio) return ElectronAsio ? ElectronAsio.getDeviceInfo(deviceIndex) : null;
791+
try {
792+
return await ipcRenderer.invoke('asio:getDeviceInfo', deviceIndex);
793+
} catch { return null; }
794+
},
795+
'getAsioVersionInfo': () => {
796+
if (!useIpcForAsio) return ElectronAsio ? ElectronAsio.getVersionInfo() : 'ASIO not available';
797+
return 'ASIO (IPC mode)';
798+
},
799+
'getAsioVersionInfoAsync': async () => {
800+
if (!useIpcForAsio) return ElectronAsio ? ElectronAsio.getVersionInfo() : 'ASIO not available';
801+
try {
802+
return await ipcRenderer.invoke('asio:getVersionInfo');
803+
} catch { return 'ASIO not available'; }
804+
},
739805
'createAsioStream': (options) => {
740-
if (!ElectronAsio || !ElectronAsio.AsioStream) {
741-
throw new Error('ASIO module not available');
806+
if (!useIpcForAsio) {
807+
if (!ElectronAsio || !ElectronAsio.AsioStream) {
808+
throw new Error('ASIO module not available');
809+
}
810+
return new ElectronAsio.AsioStream(options);
742811
}
743-
return new ElectronAsio.AsioStream(options);
812+
throw new Error('createAsioStream: Use createAsioStreamAsync in sandbox mode');
813+
},
814+
'createAsioStreamAsync': async (options) => {
815+
if (!useIpcForAsio) {
816+
if (!ElectronAsio || !ElectronAsio.AsioStream) {
817+
throw new Error('ASIO module not available');
818+
}
819+
return new ElectronAsio.AsioStream(options);
820+
}
821+
// IPC mode: create stream in main process, return control object
822+
try {
823+
const streamInfo = await ipcRenderer.invoke('asio:createStream', options);
824+
return {
825+
streamId: streamInfo.streamId,
826+
inputLatency: streamInfo.inputLatency,
827+
outputLatency: streamInfo.outputLatency,
828+
sampleRate: streamInfo.sampleRate,
829+
bufferSize: streamInfo.bufferSize,
830+
start: () => ipcRenderer.invoke('asio:startStream', streamInfo.streamId),
831+
stop: () => ipcRenderer.invoke('asio:stopStream', streamInfo.streamId),
832+
close: () => ipcRenderer.invoke('asio:closeStream', streamInfo.streamId),
833+
getStats: () => ipcRenderer.invoke('asio:getStreamStats', streamInfo.streamId),
834+
write: (buffers) => {
835+
const serialized = buffers.map(buf => Array.from(buf));
836+
return ipcRenderer.invoke('asio:writeStream', streamInfo.streamId, serialized);
837+
}
838+
};
839+
} catch (err) {
840+
throw new Error('Failed to create ASIO stream: ' + err.message);
841+
}
842+
},
843+
// Subscribe to ASIO audio data (IPC mode only)
844+
'onAsioAudioData': (callback) => {
845+
const handler = (event, { streamId, buffers }) => {
846+
const float32Buffers = buffers.map(arr => new Float32Array(arr));
847+
callback(streamId, float32Buffers);
848+
};
849+
ipcRenderer.on('asio:audioData', handler);
850+
return () => ipcRenderer.removeListener('asio:audioData', handler);
851+
},
852+
'onAsioError': (callback) => {
853+
const handler = (event, { streamId, error }) => callback(streamId, error);
854+
ipcRenderer.on('asio:error', handler);
855+
return () => ipcRenderer.removeListener('asio:error', handler);
744856
}
745857
};
746858
}

0 commit comments

Comments
 (0)