Show "please grant mic access" info

If the browser does not have the permission to use the microphone,
OpenSlides will show a spinner to inform the user to grant
mic access
This commit is contained in:
Sean 2020-11-10 15:25:52 +01:00
parent a51103b7b7
commit 26ac618ddf
4 changed files with 174 additions and 11 deletions

View File

@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { UserMediaPermService } from './user-media-perm.service';
describe('UserMediaPermService', () => {
let service: UserMediaPermService;
beforeEach(() => {
TestBed.configureTestingModule({ imports: [E2EImportsModule] });
service = TestBed.inject(UserMediaPermService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,110 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { OverlayService } from './overlay.service';
const givePermsMessage = _('Please allow OpenSlides to access your microphone and/or camera');
const accessDeniedMessage = _('Media access is denied');
const noMicMessage = _('Your device has no Microphone');
@Injectable({
providedIn: 'root'
})
export class UserMediaPermService {
private hasAudioDevice: boolean;
private hasVideoDevice: boolean;
public constructor(private translate: TranslateService, private overlayService: OverlayService) {}
public async requestMediaAccess(): Promise<void> {
await this.detectAvailableDevices();
if (!this.hasAudioDevice) {
throw new Error(noMicMessage);
}
const hasMediaAccess: PermissionState | null = await this.detectPermStatus();
if (!hasMediaAccess || hasMediaAccess === 'prompt') {
await this.tryMediaAccess();
} else if (hasMediaAccess === 'denied') {
this.throwPermError();
}
}
/**
* `navigator.permissions.query` does only work in chrome
* This function detects if this method works at all.
* If it does not work, we try to access the media anyways without
* detecting the set permission beforehand.
* The negative result would be, that the user sees the
* overlay for a very short time.
* This cannot be avoided, but rather solves itself if more browsers
* start to support the given media query
*/
private async detectPermStatus(): Promise<PermissionState | null> {
try {
const micPermStatus = await navigator.permissions.query({ name: 'microphone' });
const camPermStatus = await navigator.permissions.query({ name: 'camera' });
if (!this.hasVideoDevice || micPermStatus.state === camPermStatus.state) {
return micPermStatus.state;
} else if (micPermStatus.state === 'denied' || camPermStatus.state === 'denied') {
return 'denied';
} else {
return null;
}
} catch (e) {
return null;
}
}
private async detectAvailableDevices(): Promise<void> {
const mediaDevices = await navigator.mediaDevices.enumerateDevices();
this.hasAudioDevice = !!mediaDevices.find(device => device.kind === 'audioinput');
this.hasVideoDevice = !!mediaDevices.find(device => device.kind === 'videoinput');
}
private async tryMediaAccess(): Promise<void> {
this.showAwaitPermInfo();
try {
const stream: MediaStream = await navigator.mediaDevices.getUserMedia({
audio: this.hasAudioDevice,
video: this.hasVideoDevice
});
this.hideAwaitPermInfo();
if (stream) {
stream.getTracks().forEach(track => {
track.stop();
});
}
} catch (e) {
if (e instanceof DOMException) {
if (e.message === 'Permission denied') {
this.throwPermError();
}
} else {
this.throwPermError(e);
}
}
}
private throwPermError(error: Error = new Error(accessDeniedMessage)): void {
this.hideAwaitPermInfo();
console.error(error);
throw new Error(this.translate.instant(error.message));
}
/**
* Show:
* Please allow OpenSlides to use your microphone
*/
private showAwaitPermInfo(): void {
this.overlayService.showSpinner(this.translate.instant(givePermsMessage), true);
}
private hideAwaitPermInfo(): void {
this.overlayService.hideSpinner();
}
}

View File

@ -12,6 +12,7 @@ import { OperatorService } from 'app/core/core-services/operator.service';
import { Deferred } from 'app/core/promises/deferred'; import { Deferred } from 'app/core/promises/deferred';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { UserMediaPermService } from 'app/core/ui-services/user-media-perm.service';
import { UserListIndexType } from 'app/site/agenda/models/view-list-of-speakers'; import { UserListIndexType } from 'app/site/agenda/models/view-list-of-speakers';
import { BaseViewComponentDirective } from 'app/site/base/base-view'; import { BaseViewComponentDirective } from 'app/site/base/base-view';
import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-list-of-speakers.service'; import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-list-of-speakers.service';
@ -80,6 +81,8 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
public enableJitsi: boolean; public enableJitsi: boolean;
private autoconnect: boolean; private autoconnect: boolean;
private startWithMicMuted: boolean;
private startWithVideoMuted: boolean;
private roomName: string; private roomName: string;
private roomPassword: string; private roomPassword: string;
private jitsiDomain: string; private jitsiDomain: string;
@ -184,8 +187,8 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
startAudioOnly: false, startAudioOnly: false,
// allows jitsi on mobile devices // allows jitsi on mobile devices
disableDeepLinking: true, disableDeepLinking: true,
startWithAudioMuted: true, startWithAudioMuted: this.startWithMicMuted,
startWithVideoMuted: true, startWithVideoMuted: this.startWithVideoMuted,
useNicks: true, useNicks: true,
enableWelcomePage: false, enableWelcomePage: false,
enableUserRolesBasedOnToken: false, enableUserRolesBasedOnToken: false,
@ -236,7 +239,8 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
private userRepo: UserRepositoryService, private userRepo: UserRepositoryService,
private constantsService: ConstantsService, private constantsService: ConstantsService,
private configService: ConfigService, private configService: ConfigService,
private closService: CurrentListOfSpeakersService private closService: CurrentListOfSpeakersService,
private userMediaPermService: UserMediaPermService
) { ) {
super(titleService, translate, snackBar); super(titleService, translate, snackBar);
} }
@ -340,6 +344,14 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
this.configService.get<string>('general_system_stream_url').subscribe(url => { this.configService.get<string>('general_system_stream_url').subscribe(url => {
this.videoStreamUrl = url; this.videoStreamUrl = url;
this.configsLoaded.resolve(); this.configsLoaded.resolve();
}),
this.configService.get<boolean>('general_system_conference_open_microphon').subscribe(open => {
this.startWithMicMuted = !open;
console.log('this.startWithMicMuted ', this.startWithMicMuted);
}),
this.configService.get<boolean>('general_system_conference_open_video').subscribe(open => {
this.startWithVideoMuted = !open;
console.log('this.startWithVideoMuted ', this.startWithVideoMuted);
}) })
); );
@ -393,6 +405,8 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
public async enterConversation(): Promise<void> { public async enterConversation(): Promise<void> {
await this.operator.loaded; await this.operator.loaded;
try {
await this.userMediaPermService.requestMediaAccess();
this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {}); this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
this.setConferenceState(ConferenceState.jitsi); this.setConferenceState(ConferenceState.jitsi);
this.setOptions(); this.setOptions();
@ -401,6 +415,9 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
const jitsiname = this.userRepo.getShortName(this.operator.user); const jitsiname = this.userRepo.getShortName(this.operator.user);
this.api.executeCommand('displayName', jitsiname); this.api.executeCommand('displayName', jitsiname);
this.loadApiCallbacks(); this.loadApiCallbacks();
} catch (e) {
this.raiseError(e);
}
} }
private loadApiCallbacks(): void { private loadApiCallbacks(): void {

View File

@ -121,13 +121,31 @@ def get_config_variables():
subgroup="Live conference", subgroup="Live conference",
) )
yield ConfigVariable(
name="general_system_conference_open_microphone",
default_value=False,
input_type="boolean",
label="Automatically open the microphone for new conference speakers",
weight=143,
subgroup="Live conference",
)
yield ConfigVariable(
name="general_system_conference_open_video",
default_value=False,
input_type="boolean",
label="Automatically open the web cam for new conference speakers",
weight=144,
subgroup="Live conference",
)
yield ConfigVariable( yield ConfigVariable(
name="general_system_conference_auto_connect_next_speakers", name="general_system_conference_auto_connect_next_speakers",
default_value=0, default_value=0,
input_type="integer", input_type="integer",
label="Number of next speakers automatically connecting to the live conference", label="Number of next speakers automatically connecting to the live conference",
help_text="Live conference has to be active. Choose 0 to disable auto connect.", help_text="Live conference has to be active. Choose 0 to disable auto connect.",
weight=143, weight=145,
subgroup="Live conference", subgroup="Live conference",
) )