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:
parent
a51103b7b7
commit
26ac618ddf
@ -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();
|
||||
});
|
||||
});
|
110
client/src/app/core/ui-services/user-media-perm.service.ts
Normal file
110
client/src/app/core/ui-services/user-media-perm.service.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { Deferred } from 'app/core/promises/deferred';
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.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 { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||
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;
|
||||
|
||||
private autoconnect: boolean;
|
||||
private startWithMicMuted: boolean;
|
||||
private startWithVideoMuted: boolean;
|
||||
private roomName: string;
|
||||
private roomPassword: string;
|
||||
private jitsiDomain: string;
|
||||
@ -184,8 +187,8 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
||||
startAudioOnly: false,
|
||||
// allows jitsi on mobile devices
|
||||
disableDeepLinking: true,
|
||||
startWithAudioMuted: true,
|
||||
startWithVideoMuted: true,
|
||||
startWithAudioMuted: this.startWithMicMuted,
|
||||
startWithVideoMuted: this.startWithVideoMuted,
|
||||
useNicks: true,
|
||||
enableWelcomePage: false,
|
||||
enableUserRolesBasedOnToken: false,
|
||||
@ -236,7 +239,8 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
||||
private userRepo: UserRepositoryService,
|
||||
private constantsService: ConstantsService,
|
||||
private configService: ConfigService,
|
||||
private closService: CurrentListOfSpeakersService
|
||||
private closService: CurrentListOfSpeakersService,
|
||||
private userMediaPermService: UserMediaPermService
|
||||
) {
|
||||
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.videoStreamUrl = url;
|
||||
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,14 +405,19 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
||||
|
||||
public async enterConversation(): Promise<void> {
|
||||
await this.operator.loaded;
|
||||
this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
|
||||
this.setConferenceState(ConferenceState.jitsi);
|
||||
this.setOptions();
|
||||
this.api = new JitsiMeetExternalAPI(this.jitsiDomain, this.options);
|
||||
try {
|
||||
await this.userMediaPermService.requestMediaAccess();
|
||||
this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
|
||||
this.setConferenceState(ConferenceState.jitsi);
|
||||
this.setOptions();
|
||||
this.api = new JitsiMeetExternalAPI(this.jitsiDomain, this.options);
|
||||
|
||||
const jitsiname = this.userRepo.getShortName(this.operator.user);
|
||||
this.api.executeCommand('displayName', jitsiname);
|
||||
this.loadApiCallbacks();
|
||||
const jitsiname = this.userRepo.getShortName(this.operator.user);
|
||||
this.api.executeCommand('displayName', jitsiname);
|
||||
this.loadApiCallbacks();
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private loadApiCallbacks(): void {
|
||||
|
@ -121,13 +121,31 @@ def get_config_variables():
|
||||
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(
|
||||
name="general_system_conference_auto_connect_next_speakers",
|
||||
default_value=0,
|
||||
input_type="integer",
|
||||
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.",
|
||||
weight=143,
|
||||
weight=145,
|
||||
subgroup="Live conference",
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user