Skip to content

You are muted and no signal notifications #5388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions app/components-react/root/StudioEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import AutoProgressBar from 'components-react/shared/AutoProgressBar';
import { useSubscription } from 'components-react/hooks/useSubscription';
import { message } from 'antd';
import { useRealmObject } from 'components-react/hooks/realm';
import { ENotificationType } from 'services/notifications';
import { Service } from 'services/core/service';
import { AudioNotificationType } from 'services/audio/audio';

export default function StudioEditor() {
const {
Expand All @@ -21,6 +24,9 @@ export default function StudioEditor() {
ScenesService,
DualOutputService,
StreamingService,
AudioService,
NotificationsService,
JsonrpcService,
} = Services;
const performanceMode = useRealmObject(CustomizationService.state).performanceMode;
const v = useVuex(() => ({
Expand Down Expand Up @@ -49,6 +55,49 @@ export default function StudioEditor() {
return v.studioMode && !dualOutputMode ? studioModeTransitionName : undefined;
}, [v.showHorizontalDisplay, v.showVerticalDisplay, v.studioMode]);

useEffect(() => {
const timeoutHandles = new Array<NodeJS.Timeout | undefined>(AudioNotificationType.ElementsCount).fill(undefined);

const subscription = AudioService.audioNotificationUpdated.subscribe((notificationType) => {
if (timeoutHandles[notificationType])
return;

timeoutHandles[notificationType] = setTimeout(() => {
timeoutHandles[notificationType] = undefined;
}, 5 * 60 * 1000)

let message = "";
switch (notificationType) {
case AudioNotificationType.YouAreMuted:
message = $t('Unmute your Microphone source in Mixer');
break;
case AudioNotificationType.NoSignalFromAudioInput:
message = $t('Your Microphone is unmuted but has no signal');
break;
default:
console.warn('Unknown audio notification type:', notificationType);
return;
}

const action = JsonrpcService.createRequest(
Service.getResourceId(NotificationsService),
'showNotifications',
);

NotificationsService.actions.push({
type: ENotificationType.WARNING,
message,
action,
});

});

return () => {
subscription.unsubscribe();
timeoutHandles.forEach(v => clearTimeout(v));
}
}, []);

// Track vertical orientation for placeholder
useEffect(() => {
let timeout: number;
Expand Down
4 changes: 3 additions & 1 deletion app/i18n/en-US/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -431,5 +431,7 @@
"And more": "And more",
"The Virtual Webcam plugin needs to be updated before it can be started. This requires administrator privileges.": "The Virtual Webcam plugin needs to be updated before it can be started. This requires administrator privileges.",
"Virtual camera output selection": "Virtual camera output selection",
"Virtual camera output type": "Virtual camera output type"
"Virtual camera output type": "Virtual camera output type",
"Unmute your Microphone source in Mixer": "Unmute your Microphone source in Mixer",
"Your Microphone is unmuted but has no signal": "Your Microphone is unmuted but has no signal"
}
68 changes: 68 additions & 0 deletions app/services/audio/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,22 @@ class AudioViews extends ViewHandler<IAudioSourcesState> {
}
}

export enum AudioNotificationType {
YouAreMuted,
NoSignalFromAudioInput,

// This should be the last element
ElementsCount,
}

@InitAfter('SourcesService')
export class AudioService extends StatefulService<IAudioSourcesState> {
static initialState: IAudioSourcesState = {
audioSources: {},
};

audioSourceUpdated = new Subject<IAudioSource>();
audioNotificationUpdated = new Subject<AudioNotificationType>();

sourceData: Dictionary<IAudioSourceData> = {};

Expand Down Expand Up @@ -273,7 +282,21 @@ export class AudioService extends StatefulService<IAudioSourcesState> {
this.audioSourceUpdated.next(this.state.audioSources[sourceId]);
}

private peakHistoryMap = new Map<string, Array<number>>();

private handleVolmeterCallback(objs: IObsVolmeterCallbackInfo[]) {
const hasUnmutedAudioInput = objs.some(info => {
if (!info.sourceName.startsWith('wasapi_input')) {
return false;
}

const source = this.views.getSource(info.sourceName);
return source && !source.muted;
});

let shouldNotifyYouAreMuted = false;
let shouldNotifyNoSignal = false;

objs.forEach(info => {
const source = this.views.getSource(info.sourceName);
// A source we don't care about
Expand All @@ -282,8 +305,53 @@ export class AudioService extends StatefulService<IAudioSourcesState> {
}

const volmeter: IVolmeter = info;

if (info.sourceName.startsWith('wasapi_input')) {
if (!this.peakHistoryMap.has(info.sourceName)) {
this.peakHistoryMap.set(info.sourceName, []);
}

const peakHistory = this.peakHistoryMap.get(info.sourceName);

// Subtraction of source.fader.db is used here to compensate possible input level change by slider in UI
peakHistory.push(volmeter.peak[0] - source.fader.db);
const averagePeakValue = peakHistory.reduce((a, b) => a + b) / peakHistory.length;

const hasEnoughData = peakHistory.length >= 50;
if (hasEnoughData) {
peakHistory.shift();
}

if (source.muted) {
if (hasEnoughData && !hasUnmutedAudioInput && averagePeakValue >= -30 /* db */) {
this.peakHistoryMap.set(info.sourceName, []);
shouldNotifyYouAreMuted = true;
}

// This is needed to not render audio peaks in UI for muted audio inputs
volmeter.inputPeak.forEach((item, index) => (volmeter.inputPeak[index] = -65535));
volmeter.peak.forEach((item, index) => (volmeter.peak[index] = -65535));
volmeter.magnitude.forEach((item, index) => (volmeter.magnitude[index] = -65535));
} else {
if (hasEnoughData && averagePeakValue <= -1000 /* db */) {
this.peakHistoryMap.set(info.sourceName, []);
shouldNotifyNoSignal = true;
}
}
}

this.sendVolmeterData(info.sourceName, volmeter);
});

// Note: in practice these events will never fire simultaneously because of opposite
// conditions for thier activations
if (shouldNotifyYouAreMuted) {
this.audioNotificationUpdated.next(AudioNotificationType.YouAreMuted);
}

if (shouldNotifyNoSignal) {
this.audioNotificationUpdated.next(AudioNotificationType.NoSignalFromAudioInput);
}
}

private createAudioSource(source: Source) {
Expand Down
4 changes: 2 additions & 2 deletions scripts/repositories.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"name": "obs-studio-node",
"url": "https://s3-us-west-2.amazonaws.com/obsstudionodes3.streamlabs.com/",
"archive": "osn-[VERSION]-release-[OS][ARCH].tar.gz",
"version": "0.25.24",
"mac_version": "0.25.24",
"version": "0.25.26test2",
"mac_version": "0.25.26test2",
"win64": true,
"osx": true
},
Expand Down
Loading