123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- const VoiceEngine = require('./discord_voice_'+process.platform+'.node');
- const fs = require('fs');
- const path = require('path');
- const yargs = require('yargs');
- const isElectronRenderer =
- typeof window !== 'undefined' && window != null && window.DiscordNative && window.DiscordNative.isRenderer;
- const appSettings = isElectronRenderer ? window.DiscordNative.settings : global.appSettings;
- const features = isElectronRenderer ? window.DiscordNative.features : global.features;
- const mainArgv = isElectronRenderer ? window.DiscordNative.processUtils.getMainArgvSync() : [];
- let dataDirectory;
- try {
- dataDirectory =
- isElectronRenderer && window.DiscordNative.fileManager.getModuleDataPathSync
- ? path.join(window.DiscordNative.fileManager.getModuleDataPathSync(), 'discord_voice')
- : null;
- } catch (e) {
- console.error('Failed to get data directory: ', e);
- }
- const releaseChannel = isElectronRenderer ? window.DiscordNative.app.getReleaseChannel() : '';
- const useLegacyAudioDevice = appSettings ? appSettings.getSync('useLegacyAudioDevice') : false;
- const audioSubsystemSelected = appSettings
- ? appSettings.getSync('audioSubsystem') === 'legacy'
- ? 'legacy'
- : 'standard'
- : 'standard';
- const audioSubsystem = useLegacyAudioDevice || audioSubsystemSelected;
- const debugLogging = appSettings ? appSettings.getSync('debugLogging') : false;
- const argv = yargs(mainArgv.slice(1))
- .describe('log-level', 'Logging level.')
- .default('log-level', -1)
- .help('h')
- .alias('h', 'help')
- .exitProcess(false).argv;
- const logLevel = argv['log-level'] == -1 ? (debugLogging ? 2 : -1) : argv['log-level'];
- if (dataDirectory != null) {
- try {
- fs.mkdirSync(dataDirectory, {recursive: true});
- } catch (e) {
- console.warn("Couldn't create voice data directory ", dataDirectory, ':', e);
- }
- }
- if (debugLogging && console.discordVoiceHooked == null) {
- console.discordVoiceHooked = true;
- for (const logFn of ['trace', 'debug', 'info', 'warn', 'error', 'log']) {
- const originalLogFn = console[logFn];
- if (originalLogFn != null) {
- console[logFn] = function () {
- originalLogFn.apply(this, arguments);
- try {
- VoiceEngine.consoleLog(
- logFn,
- JSON.stringify(Array.from(arguments).map((v) => (v != null ? v.toString() : v)))
- );
- } catch (e) {
- // Drop errors from toString()/stringify.
- }
- };
- }
- }
- }
- features.declareSupported('voice_panning');
- features.declareSupported('voice_multiple_connections');
- features.declareSupported('media_devices');
- features.declareSupported('media_video');
- features.declareSupported('debug_logging');
- features.declareSupported('set_audio_device_by_id');
- features.declareSupported('set_video_device_by_id');
- features.declareSupported('loopback');
- features.declareSupported('experiment_config');
- features.declareSupported('remote_locus_network_control');
- features.declareSupported('connection_replay');
- features.declareSupported('simulcast');
- features.declareSupported('direct_video');
- if (process.platform === 'win32') {
- features.declareSupported('voice_legacy_subsystem');
- features.declareSupported('soundshare');
- features.declareSupported('wumpus_video');
- features.declareSupported('hybrid_video');
- features.declareSupported('elevated_hook');
- features.declareSupported('soundshare_loopback');
- features.declareSupported('screen_previews');
- features.declareSupported('window_previews');
- features.declareSupported('audio_debug_state');
- features.declareSupported('video_effects');
- // NOTE(jvass): currently there's no experimental encoders! Add this back if you
- // add one and want to re-enable the UI for them.
- // features.declareSupported('experimental_encoders');
- }
- function bindConnectionInstance(instance) {
- return {
- destroy: () => instance.destroy(),
- setTransportOptions: (options) => instance.setTransportOptions(options),
- setSelfMute: (mute) => instance.setSelfMute(mute),
- setSelfDeafen: (deaf) => instance.setSelfDeafen(deaf),
- mergeUsers: (users) => instance.mergeUsers(users),
- destroyUser: (userId) => instance.destroyUser(userId),
- setLocalVolume: (userId, volume) => instance.setLocalVolume(userId, volume),
- setLocalMute: (userId, mute) => instance.setLocalMute(userId, mute),
- setLocalPan: (userId, left, right) => instance.setLocalPan(userId, left, right),
- setDisableLocalVideo: (userId, disabled) => instance.setDisableLocalVideo(userId, disabled),
- setMinimumOutputDelay: (delay) => instance.setMinimumOutputDelay(delay),
- getEncryptionModes: (callback) => instance.getEncryptionModes(callback),
- configureConnectionRetries: (baseDelay, maxDelay, maxAttempts) =>
- instance.configureConnectionRetries(baseDelay, maxDelay, maxAttempts),
- setOnSpeakingCallback: (callback) => instance.setOnSpeakingCallback(callback),
- setOnSpeakingWhileMutedCallback: (callback) => instance.setOnSpeakingWhileMutedCallback(callback),
- setPingInterval: (interval) => instance.setPingInterval(interval),
- setPingCallback: (callback) => instance.setPingCallback(callback),
- setPingTimeoutCallback: (callback) => instance.setPingTimeoutCallback(callback),
- setRemoteUserSpeakingStatus: (userId, speaking) => instance.setRemoteUserSpeakingStatus(userId, speaking),
- setRemoteUserCanHavePriority: (userId, canHavePriority) =>
- instance.setRemoteUserCanHavePriority(userId, canHavePriority),
- setOnVideoCallback: (callback) => instance.setOnVideoCallback(callback),
- setVideoBroadcast: (broadcasting) => instance.setVideoBroadcast(broadcasting),
- setDesktopSource: (id, videoHook, type) => instance.setDesktopSource(id, videoHook, type),
- setDesktopSourceStatusCallback: (callback) => instance.setDesktopSourceStatusCallback(callback),
- setOnDesktopSourceEnded: (callback) => instance.setOnDesktopSourceEnded(callback),
- setOnSoundshare: (callback) => instance.setOnSoundshare(callback),
- setOnSoundshareEnded: (callback) => instance.setOnSoundshareEnded(callback),
- setOnSoundshareFailed: (callback) => instance.setOnSoundshareFailed(callback),
- setPTTActive: (active, priority) => instance.setPTTActive(active, priority),
- getStats: (callback) => instance.getStats(callback),
- getFilteredStats: (filter, callback) => instance.getFilteredStats(filter, callback),
- startReplay: () => instance.startReplay(),
- };
- }
- VoiceEngine.createTransport = VoiceEngine._createTransport;
- if (isElectronRenderer) {
- VoiceEngine.setImageDataAllocator((width, height) => new window.ImageData(width, height));
- }
- VoiceEngine.createVoiceConnection = function (audioSSRC, userId, address, port, onConnectCallback, experiments, rids) {
- let instance = null;
- if (rids != null) {
- instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback, experiments, rids);
- } else if (experiments != null) {
- instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback, experiments);
- } else {
- instance = new VoiceEngine.VoiceConnection(audioSSRC, userId, address, port, onConnectCallback);
- }
- return bindConnectionInstance(instance);
- };
- VoiceEngine.createOwnStreamConnection = VoiceEngine.createVoiceConnection;
- VoiceEngine.createReplayConnection = function (audioEngineId, callback, replayLog) {
- if (replayLog == null) {
- return null;
- }
- return bindConnectionInstance(new VoiceEngine.VoiceReplayConnection(replayLog, audioEngineId, callback));
- };
- VoiceEngine.setAudioSubsystem = function (subsystem) {
- if (appSettings == null) {
- console.warn('Unable to access app settings.');
- return;
- }
- // TODO: With experiment controlling ADM selection, this may be incorrect since
- // audioSubsystem is read from settings (or default if does not exists)
- // and not the actual ADM used.
- if (subsystem === audioSubsystem) {
- return;
- }
- appSettings.set('audioSubsystem', subsystem);
- appSettings.set('useLegacyAudioDevice', false);
- if (isElectronRenderer) {
- window.DiscordNative.app.relaunch();
- }
- };
- VoiceEngine.setDebugLogging = function (enable) {
- if (appSettings == null) {
- console.warn('Unable to access app settings.');
- return;
- }
- if (debugLogging === enable) {
- return;
- }
- appSettings.set('debugLogging', enable);
- if (isElectronRenderer) {
- window.DiscordNative.app.relaunch();
- }
- };
- VoiceEngine.getDebugLogging = function () {
- return debugLogging;
- };
- const videoStreams = {};
- const ensureCanvasContext = function (sinkId) {
- let canvas = document.getElementById(sinkId);
- if (canvas == null) {
- for (const popout of window.popouts.values()) {
- const element = popout.document != null && popout.document.getElementById(sinkId);
- if (element != null) {
- canvas = element;
- break;
- }
- }
- if (canvas == null) {
- return null;
- }
- }
- const context = canvas.getContext('2d');
- if (context == null) {
- console.log(`Failed to initialize context for sinkId ${sinkId}`);
- return null;
- }
- return context;
- };
- // [adill] NB: with context isolation it has become extremely costly (both memory & performance) to provide the image
- // data directly to clients at any reasonably fast interval so we've replaced setVideoOutputSink with a direct canvas
- // renderer via addVideoOutputSink
- const setVideoOutputSink = VoiceEngine.setVideoOutputSink;
- const clearVideoOutputSink = (streamId) => {
- // [adill] NB: if you don't pass a frame callback setVideoOutputSink clears the sink
- setVideoOutputSink(streamId);
- };
- const signalVideoOutputSinkReady = VoiceEngine.signalVideoOutputSinkReady;
- delete VoiceEngine.setVideoOutputSink;
- delete VoiceEngine.signalVideoOutputSinkReady;
- function addVideoOutputSinkInternal(sinkId, streamId, frameCallback) {
- let sinks = videoStreams[streamId];
- if (sinks == null) {
- sinks = videoStreams[streamId] = new Map();
- }
- if (sinks.size === 0) {
- console.log(`Subscribing to frames for streamId ${streamId}`);
- const onFrame = (imageData) => {
- const sinks = videoStreams[streamId];
- if (sinks != null) {
- for (const callback of sinks.values()) {
- if (callback != null) {
- callback(imageData);
- }
- }
- }
- signalVideoOutputSinkReady(streamId);
- };
- setVideoOutputSink(streamId, onFrame, true);
- }
- sinks.set(sinkId, frameCallback);
- }
- VoiceEngine.addVideoOutputSink = function (sinkId, streamId, frameCallback) {
- let canvasContext = null;
- addVideoOutputSinkInternal(sinkId, streamId, (imageData) => {
- if (canvasContext == null) {
- canvasContext = ensureCanvasContext(sinkId);
- if (canvasContext == null) {
- return;
- }
- }
- if (frameCallback != null) {
- frameCallback(imageData.width, imageData.height);
- }
- // [adill] NB: Electron 9+ on macOS would show massive leaks in the the GPU helper process when a non-Discord
- // window completely occludes the Discord window. Adding this tiny readback ameliorates the issue. We tried WebGL
- // rendering which did not exhibit the issue, however, the context limit of 16 was too small to be a real
- // alternative.
- const leak = canvasContext.getImageData(0, 0, 1, 1);
- canvasContext.putImageData(imageData, 0, 0);
- });
- };
- VoiceEngine.removeVideoOutputSink = function (sinkId, streamId) {
- const sinks = videoStreams[streamId];
- if (sinks != null) {
- sinks.delete(sinkId);
- if (sinks.size === 0) {
- delete videoStreams[streamId];
- console.log(`Unsubscribing from frames for streamId ${streamId}`);
- clearVideoOutputSink(streamId);
- }
- }
- };
- let sinkId = 0;
- VoiceEngine.getNextVideoOutputFrame = function (streamId) {
- const nextVideoFrameSinkId = `getNextVideoFrame_${++sinkId}`;
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- VoiceEngine.removeVideoOutputSink(nextVideoFrameSinkId, streamId);
- reject(new Error('getNextVideoOutputFrame timeout'));
- }, 5000);
- addVideoOutputSinkInternal(nextVideoFrameSinkId, streamId, (imageData) => {
- VoiceEngine.removeVideoOutputSink(nextVideoFrameSinkId, streamId);
- resolve({
- width: imageData.width,
- height: imageData.height,
- data: new Uint8ClampedArray(imageData.data.buffer),
- });
- });
- });
- };
- console.log(`Initializing voice engine with audio subsystem: ${audioSubsystem}`);
- VoiceEngine.initialize({audioSubsystem, logLevel, dataDirectory});
- module.exports = VoiceEngine;
|