[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 { SidebarComponent } from './sidebar/sidebar.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { InputManagerComponent } from './input-manager/input-manager.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
|
@ -31,7 +32,8 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
|||
ProfileComponent,
|
||||
ChannelComponent,
|
||||
SidebarComponent,
|
||||
DashboardComponent
|
||||
DashboardComponent,
|
||||
InputManagerComponent
|
||||
],
|
||||
imports: [
|
||||
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>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6">
|
||||
<legend>Media Settings</legend>
|
||||
<app-input-manager></app-input-manager>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -23,8 +23,8 @@
|
|||
</span>
|
||||
</div>
|
||||
<div class="modal-body d-flex flex-column">
|
||||
<video *ngIf="stream" [id]="id" [srcObject]="stream" class="video-recorder" autoplay muted volume="0"></video>
|
||||
<button *ngIf="!isRecording" class="btn btn-success" (click)="startRecording()">
|
||||
<app-input-manager (streamOutput)="handleStreamChange($event)"></app-input-manager>
|
||||
<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">
|
||||
<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"
|
||||
|
|
@ -35,6 +35,6 @@
|
|||
</svg>
|
||||
Start recording
|
||||
</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>
|
||||
</ng-template>
|
||||
|
|
@ -27,23 +27,18 @@ export class RecorderComponent implements OnInit {
|
|||
this.modal = this.modalService.open(this.content, {
|
||||
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() {
|
||||
if (!this.stream) {
|
||||
handleStreamChange(stream: MediaStream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
startRecording(stream: MediaStream = this.stream) {
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
this.isRecording = true;
|
||||
this.recorder = new rrtc.RecordRTCPromisesHandler(this.stream, {
|
||||
this.recorder = new rrtc.RecordRTCPromisesHandler(stream, {
|
||||
type: 'video',
|
||||
mimeType: 'video/webm;codecs=vp8',
|
||||
});
|
||||
|
|
@ -57,7 +52,6 @@ export class RecorderComponent implements OnInit {
|
|||
let blob = await this.recorder.getDataURL();
|
||||
that.recordingEnded.emit(blob);
|
||||
that.modal.close();
|
||||
// rrtc.invokeSaveAsDialog(blob, 'Recorded-Video.webm');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue