diff --git a/src/app/api/supabase/channel.service.ts b/src/app/api/supabase/channel.service.ts new file mode 100644 index 0000000..0d73e6c --- /dev/null +++ b/src/app/api/supabase/channel.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class ChannelService { + + constructor() { } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c39149a..68d993d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/app/input-manager/input-manager.component.html b/src/app/input-manager/input-manager.component.html new file mode 100644 index 0000000..57c27b3 --- /dev/null +++ b/src/app/input-manager/input-manager.component.html @@ -0,0 +1,15 @@ + + +
+ + +
+ + + +
+ + +
diff --git a/src/app/input-manager/input-manager.component.scss b/src/app/input-manager/input-manager.component.scss new file mode 100644 index 0000000..441227b --- /dev/null +++ b/src/app/input-manager/input-manager.component.scss @@ -0,0 +1,4 @@ +video { + max-width: 100%; + max-height: 100%; + } \ No newline at end of file diff --git a/src/app/input-manager/input-manager.component.spec.ts b/src/app/input-manager/input-manager.component.spec.ts new file mode 100644 index 0000000..10087c0 --- /dev/null +++ b/src/app/input-manager/input-manager.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ InputManagerComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InputManagerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/input-manager/input-manager.component.ts b/src/app/input-manager/input-manager.component.ts new file mode 100644 index 0000000..b9023f5 --- /dev/null +++ b/src/app/input-manager/input-manager.component.ts @@ -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 = 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)); + } +} diff --git a/src/app/profile/profile.component.html b/src/app/profile/profile.component.html index 69bab14..5e4e689 100644 --- a/src/app/profile/profile.component.html +++ b/src/app/profile/profile.component.html @@ -9,4 +9,8 @@ +
+ Media Settings + +
\ No newline at end of file diff --git a/src/app/recorder/recorder.component.html b/src/app/recorder/recorder.component.html index 7177370..f4307dc 100644 --- a/src/app/recorder/recorder.component.html +++ b/src/app/recorder/recorder.component.html @@ -23,8 +23,8 @@ \ No newline at end of file diff --git a/src/app/recorder/recorder.component.ts b/src/app/recorder/recorder.component.ts index 63c8b59..3ff6b6d 100644 --- a/src/app/recorder/recorder.component.ts +++ b/src/app/recorder/recorder.component.ts @@ -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; }