[A] input-manager selection in profile and recorder
This commit is contained in:
parent
6880d9c033
commit
d64dce0680
9 changed files with 212 additions and 17 deletions
9
src/app/api/supabase/channel.service.ts
Normal file
9
src/app/api/supabase/channel.service.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ChannelService {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import { ProfileComponent } from './profile/profile.component';
|
||||||
import { ChannelComponent } from './channel/channel.component';
|
import { ChannelComponent } from './channel/channel.component';
|
||||||
import { SidebarComponent } from './sidebar/sidebar.component';
|
import { SidebarComponent } from './sidebar/sidebar.component';
|
||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
import { InputManagerComponent } from './input-manager/input-manager.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
|
@ -31,7 +32,8 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
ProfileComponent,
|
ProfileComponent,
|
||||||
ChannelComponent,
|
ChannelComponent,
|
||||||
SidebarComponent,
|
SidebarComponent,
|
||||||
DashboardComponent
|
DashboardComponent,
|
||||||
|
InputManagerComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
|
|
||||||
15
src/app/input-manager/input-manager.component.html
Normal file
15
src/app/input-manager/input-manager.component.html
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<video id="localVideo" autoplay muted></video>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="audioSource">Audio input source: </label>
|
||||||
|
<select class="form-control" id="audioSource" (change)="start()"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="select">
|
||||||
|
<label for="audioOutput">Audio output destination: </label><select id="audioOutput" (change)="changeAudioDestination()"></select>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="videoSource">Video source: </label>
|
||||||
|
<select class="form-control" id="videoSource" (change)="start()"></select>
|
||||||
|
</div>
|
||||||
4
src/app/input-manager/input-manager.component.scss
Normal file
4
src/app/input-manager/input-manager.component.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
25
src/app/input-manager/input-manager.component.spec.ts
Normal file
25
src/app/input-manager/input-manager.component.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { InputManagerComponent } from './input-manager.component';
|
||||||
|
|
||||||
|
describe('InputManagerComponent', () => {
|
||||||
|
let component: InputManagerComponent;
|
||||||
|
let fixture: ComponentFixture<InputManagerComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ InputManagerComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(InputManagerComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
142
src/app/input-manager/input-manager.component.ts
Normal file
142
src/app/input-manager/input-manager.component.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-input-manager',
|
||||||
|
templateUrl: './input-manager.component.html',
|
||||||
|
styleUrls: ['./input-manager.component.scss']
|
||||||
|
})
|
||||||
|
export class InputManagerComponent implements AfterViewInit {
|
||||||
|
@Output() streamOutput: EventEmitter<MediaStream> = new EventEmitter();
|
||||||
|
stream: MediaStream;
|
||||||
|
videoElement:any = document.querySelector('#localVideo');
|
||||||
|
audioInputSelect:any = document.querySelector('select#audioSource');
|
||||||
|
// audioOutputSelect:any = document.querySelector('select#audioOutput');
|
||||||
|
videoSelect:any = document.querySelector('select#videoSource');
|
||||||
|
// selectors = [this.audioInputSelect, this.audioOutputSelect, this.videoSelect];
|
||||||
|
selectors = [this.audioInputSelect, this.videoSelect];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.videoElement = document.querySelector('#localVideo');
|
||||||
|
this.videoElement.muted = true;
|
||||||
|
this.audioInputSelect = document.querySelector('select#audioSource');
|
||||||
|
// this.audioOutputSelect = document.querySelector('select#audioOutput');
|
||||||
|
this.videoSelect = document.querySelector('select#videoSource');
|
||||||
|
// this.selectors = [this.audioInputSelect, this.audioOutputSelect, this.videoSelect];
|
||||||
|
this.selectors = [this.audioInputSelect, this.videoSelect];
|
||||||
|
// console.log(this.videoSelect, this.selectors)
|
||||||
|
// this.audioOutputSelect.disabled = !('sinkId' in HTMLMediaElement.prototype);
|
||||||
|
navigator.mediaDevices.enumerateDevices().then(devices => this.gotDevices(devices)).catch(error => this.handleError(error));
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach audio output device to video element using device/sink ID.
|
||||||
|
attachSinkId(element, sinkId) {
|
||||||
|
if (typeof element.sinkId !== 'undefined') {
|
||||||
|
element.setSinkId(sinkId)
|
||||||
|
.then(() => {
|
||||||
|
// console.log(`Success, audio output device attached: ${sinkId}`);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
let errorMessage = error;
|
||||||
|
if (error.name === 'SecurityError') {
|
||||||
|
errorMessage = `You need to use HTTPS for selecting audio output device: ${error}`;
|
||||||
|
}
|
||||||
|
// console.error(errorMessage);
|
||||||
|
// Jump back to first output device in the list as it's the default.
|
||||||
|
// this.audioOutputSelect.selectedIndex = 0;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// console.warn('Browser does not support output device selection.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// changeAudioDestination() {
|
||||||
|
// const audioDestination = this.audioOutputSelect.value;
|
||||||
|
// this.attachSinkId(this.videoElement, audioDestination);
|
||||||
|
// }
|
||||||
|
|
||||||
|
gotStream(stream: MediaStream) {
|
||||||
|
this.stream = stream; // make stream available to // console
|
||||||
|
this.videoElement.srcObject = stream;
|
||||||
|
this.videoElement.muted = true;
|
||||||
|
this.streamOutput.next(stream);
|
||||||
|
// Refresh button list in case labels have become available
|
||||||
|
return navigator.mediaDevices.enumerateDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(error) {
|
||||||
|
console.error('navigator.MediaDevices.getUserMedia error: ', error.message, error.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
gotDevices(deviceInfos) {
|
||||||
|
// Handles being called several times to update labels. Preserve values.
|
||||||
|
const values = this.selectors.map(select => select.value);
|
||||||
|
this.selectors.forEach(select => {
|
||||||
|
while (select.firstChild) {
|
||||||
|
select.removeChild(select.firstChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (let i = 0; i !== deviceInfos.length; ++i) {
|
||||||
|
const deviceInfo = deviceInfos[i];
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = deviceInfo.deviceId;
|
||||||
|
if (deviceInfo.kind === 'audioinput') {
|
||||||
|
option.text = deviceInfo.label || `microphone ${this.audioInputSelect.length + 1}`;
|
||||||
|
this.audioInputSelect.appendChild(option);
|
||||||
|
// } else if (deviceInfo.kind === 'audiooutput') {
|
||||||
|
// option.text = deviceInfo.label || `speaker ${this.audioOutputSelect.length + 1}`;
|
||||||
|
// this.audioOutputSelect.appendChild(option);
|
||||||
|
} else if (deviceInfo.kind === 'videoinput') {
|
||||||
|
option.text = deviceInfo.label || `camera ${this.videoSelect.length + 1}`;
|
||||||
|
this.videoSelect.appendChild(option);
|
||||||
|
} else {
|
||||||
|
// console.log('Some other kind of source/device: ', deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.selectors.forEach((select, selectorIndex) => {
|
||||||
|
if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
|
||||||
|
select.value = values[selectorIndex];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.stream) {
|
||||||
|
this.stream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let audioSource = this.audioInputSelect.value;
|
||||||
|
const savedAudioSource = localStorage.getItem('async-huddle-selected-mic');
|
||||||
|
if (audioSource) {
|
||||||
|
localStorage.setItem('async-huddle-selected-mic', audioSource);
|
||||||
|
} else if (savedAudioSource) {
|
||||||
|
audioSource = savedAudioSource;
|
||||||
|
setTimeout(() => {
|
||||||
|
const e:any = document.querySelector('[value="'+savedAudioSource+'"]')
|
||||||
|
if (e) e.selected = true;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
console.warn('Selecting audio:', audioSource);
|
||||||
|
let videoSource = this.videoSelect.value;
|
||||||
|
const savedVideoSource = localStorage.getItem('async-huddle-selected-video-device');
|
||||||
|
if (videoSource) {
|
||||||
|
localStorage.setItem('async-huddle-selected-video-device', videoSource);
|
||||||
|
} else if (savedVideoSource) {
|
||||||
|
videoSource = savedVideoSource;
|
||||||
|
setTimeout(() => {
|
||||||
|
const e:any = document.querySelector('[value="'+savedVideoSource+'"]')
|
||||||
|
if (e) e.selected = true;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
console.warn('Selecting video:', videoSource);
|
||||||
|
const constraints = {
|
||||||
|
audio: {deviceId: audioSource ? {exact: audioSource} : undefined},
|
||||||
|
video: {deviceId: videoSource ? {exact: videoSource} : undefined}
|
||||||
|
};
|
||||||
|
navigator.mediaDevices.getUserMedia(constraints).then((stream) => this.gotStream(stream)).then(devices => this.gotDevices(devices)).catch(error => this.handleError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,4 +9,8 @@
|
||||||
<button type="submit" class="btn btn-primary" (submit)="updateUser()" (click)="updateUser()">Save</button>
|
<button type="submit" class="btn btn-primary" (submit)="updateUser()" (click)="updateUser()">Save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<legend>Media Settings</legend>
|
||||||
|
<app-input-manager></app-input-manager>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -23,8 +23,8 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body d-flex flex-column">
|
<div class="modal-body d-flex flex-column">
|
||||||
<video *ngIf="stream" [id]="id" [srcObject]="stream" class="video-recorder" autoplay muted volume="0"></video>
|
<app-input-manager (streamOutput)="handleStreamChange($event)"></app-input-manager>
|
||||||
<button *ngIf="!isRecording" class="btn btn-success" (click)="startRecording()">
|
<button *ngIf="!isRecording" class="btn btn-success mt-2" (click)="startRecording()">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
|
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
|
||||||
|
|
@ -35,6 +35,6 @@
|
||||||
</svg>
|
</svg>
|
||||||
Start recording
|
Start recording
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="isRecording" class="btn btn-danger" (click)="stopRecording()">Stop</button>
|
<button *ngIf="isRecording" class="btn btn-danger mt-2" (click)="stopRecording()">Stop</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
@ -27,23 +27,18 @@ export class RecorderComponent implements OnInit {
|
||||||
this.modal = this.modalService.open(this.content, {
|
this.modal = this.modalService.open(this.content, {
|
||||||
centered: true
|
centered: true
|
||||||
});
|
});
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: true,
|
|
||||||
audio: true
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
const video:any = document.getElementById(this.id);
|
|
||||||
video.volume = 0;
|
|
||||||
video.muted = true;
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startRecording() {
|
handleStreamChange(stream: MediaStream) {
|
||||||
if (!this.stream) {
|
this.stream = stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
startRecording(stream: MediaStream = this.stream) {
|
||||||
|
if (!stream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isRecording = true;
|
this.isRecording = true;
|
||||||
this.recorder = new rrtc.RecordRTCPromisesHandler(this.stream, {
|
this.recorder = new rrtc.RecordRTCPromisesHandler(stream, {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
mimeType: 'video/webm;codecs=vp8',
|
mimeType: 'video/webm;codecs=vp8',
|
||||||
});
|
});
|
||||||
|
|
@ -57,7 +52,6 @@ export class RecorderComponent implements OnInit {
|
||||||
let blob = await this.recorder.getDataURL();
|
let blob = await this.recorder.getDataURL();
|
||||||
that.recordingEnded.emit(blob);
|
that.recordingEnded.emit(blob);
|
||||||
that.modal.close();
|
that.modal.close();
|
||||||
// rrtc.invokeSaveAsDialog(blob, 'Recorded-Video.webm');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue