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 { 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 {
|
||||||
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user