├── src ├── assets │ └── .gitkeep ├── app │ ├── app.component.scss │ ├── core │ │ ├── test │ │ │ ├── test.component.scss │ │ │ ├── test.module.ts │ │ │ ├── test.component.spec.ts │ │ │ ├── test.component.html │ │ │ └── test.component.ts │ │ └── core.module.ts │ ├── app.component.html │ ├── app.component.ts │ ├── wss │ │ ├── wss.module.ts │ │ ├── wss.service.ts │ │ └── wss.mediasoup.ts │ ├── app-routing.module.ts │ ├── app.module.ts │ └── app.component.spec.ts ├── favicon.ico ├── styles.scss ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── main.ts ├── test.ts └── polyfills.ts ├── mediasoup-client ├── index.d.ts ├── CommandQueue.ts ├── EnhancedEventEmitter.ts ├── DataConsumer.ts ├── interfaces.ts ├── DataProducer.ts ├── Consumer.ts ├── Producer.ts ├── Device.ts └── Transport.ts ├── e2e ├── tsconfig.json ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts └── protractor.conf.js ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── browserslist ├── README.md ├── tsconfig.json ├── .gitignore ├── karma.conf.js ├── package.json ├── tslint.json └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/core/test/test.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkosminov/mediasoup-angular-example/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | name: 'production', 4 | wss_url: '' 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /mediasoup-client/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IDevice } from './Device'; 2 | 3 | declare module 'mediasoup-client' { 4 | export const Device: IDevice; 5 | 6 | export const version: string; 7 | 8 | export function parseScalabilityMode(scalabilityMode: any): any; 9 | } 10 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/wss/wss.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { WssService } from './wss.service'; 5 | 6 | @NgModule({ 7 | declarations: [], 8 | providers: [ 9 | WssService, 10 | ], 11 | imports: [ 12 | CommonModule 13 | ] 14 | }) 15 | export class WssModule {} 16 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ], 14 | "angularCompilerOptions": { 15 | "enableIvy": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TestModule } from './test/test.module'; 4 | 5 | @NgModule({ 6 | declarations: [], 7 | imports: [ 8 | CommonModule, 9 | TestModule 10 | ], 11 | exports: [ 12 | TestModule, 13 | ] 14 | }) 15 | export class CoreModule {} 16 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MediasoupAngularExample 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/core/test/test.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TestComponent } from './test.component'; 4 | 5 | 6 | 7 | @NgModule({ 8 | declarations: [TestComponent], 9 | imports: [ 10 | CommonModule 11 | ], 12 | bootstrap: [ 13 | TestComponent 14 | ] 15 | }) 16 | export class TestModule { } 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /mediasoup-client/CommandQueue.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: jsdoc-format 2 | // tslint:disable: no-any 3 | // tslint:disable: no-redundant-jsdoc 4 | 5 | export interface ICommandQueue { 6 | new (); 7 | 8 | close(): void; 9 | 10 | /** 11 | * @param {Function} command - Function that returns a promise. 12 | * 13 | * @async 14 | */ 15 | push(command: () => void): /* CommandQueue.prototype.+Promise */ Promise; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ], 18 | "angularCompilerOptions": { 19 | "enableIvy": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { TestComponent } from './core/test/test.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: TestComponent, 10 | } 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [RouterModule.forRoot(routes)], 15 | exports: [RouterModule] 16 | }) 17 | export class AppRoutingModule { } 18 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /mediasoup-client/EnhancedEventEmitter.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: no-any 2 | // tslint:disable: jsdoc-format 3 | // tslint:disable: no-redundant-jsdoc 4 | 5 | export interface IEnhancedEventEmitter { 6 | /** 7 | * 8 | * @param logger 9 | */ 10 | new (logger: any); 11 | 12 | /** 13 | * 14 | * @param event 15 | * @param ...args 16 | */ 17 | safeEmit(event: any, ...args: any): void; 18 | 19 | /** 20 | * 21 | * @param event 22 | * @param ...args 23 | * @return 24 | */ 25 | safeEmitAsPromise(event: any, ...args: any): Promise; 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MediasoupAngularExample 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.0. 4 | 5 | ## Development server 6 | 7 | Run `npm run start` for a dev server. Navigate to `https://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Build 10 | 11 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 12 | 13 | ## Server 14 | 15 | [Use it](https://github.com/TimurRK/mediasoup-nestjs-example) 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/app/core/test/test.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TestComponent } from './test.component'; 4 | 5 | describe('TestComponent', () => { 6 | let component: TestComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TestComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TestComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to mediasoup-angular-example!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | name: 'development', 8 | wss_url: 'http://localhost:8086' 9 | }; 10 | 11 | /* 12 | * For easier debugging in development mode, you can import the following file 13 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 14 | * 15 | * This import should be commented out in production mode because it will have a negative impact 16 | * on performance if an error is thrown. 17 | */ 18 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | 7 | import { WssModule } from './wss/wss.module'; 8 | 9 | import { LoggerModule, NgxLoggerLevel } from 'ngx-logger'; 10 | import { HttpClientModule } from '@angular/common/http'; 11 | import { CoreModule } from './core/core.module'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | AppComponent 16 | ], 17 | imports: [ 18 | BrowserModule, 19 | AppRoutingModule, 20 | HttpClientModule, 21 | LoggerModule.forRoot({ level: NgxLoggerLevel.DEBUG }), 22 | WssModule, 23 | CoreModule, 24 | ], 25 | providers: [], 26 | bootstrap: [AppComponent] 27 | }) 28 | export class AppModule { } 29 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/mediasoup-angular-example'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'mediasoup-angular-example'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app.title).toEqual('mediasoup-angular-example'); 27 | }); 28 | 29 | it('should render title in a h1 tag', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to mediasoup-angular-example!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/core/test/test.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 |
4 |
Producer Video
5 | 6 |
8 |
Consumer Video
9 | 10 |
12 |
Consumer Audio
13 | 14 |
23 | 24 | 25 |
31 | 32 | 33 |
38 | -------------------------------------------------------------------------------- /src/app/wss/wss.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NGXLogger } from 'ngx-logger'; 3 | 4 | import { connect } from 'socket.io-client'; 5 | 6 | import { environment } from '../../environments/environment'; 7 | import { MediasoupService } from './wss.mediasoup'; 8 | 9 | type IOSocket = SocketIOClient.Socket & { request: (ioEvent: string, data?: any) => Promise }; 10 | 11 | @Injectable() 12 | export class WssService { 13 | private socket: IOSocket; 14 | 15 | public mediasoup: MediasoupService; 16 | 17 | constructor( 18 | private readonly logger: NGXLogger 19 | ) {} 20 | 21 | // tslint:disable-next-line: variable-name 22 | public connect(current_user_id: string) { 23 | this.socket = connect(environment.wss_url, { 24 | query: { 25 | session_id: 1, 26 | user_id: current_user_id, 27 | } 28 | }) as IOSocket; 29 | 30 | this.socket.request = (ioEvent: string, data: object = {}) => { 31 | return new Promise((resolve) => { 32 | this.socket.emit(ioEvent, data, resolve); 33 | }); 34 | }; 35 | 36 | this.mediasoup = new MediasoupService(this.socket); 37 | 38 | this.socket.on('connect', async () => { 39 | await this.mediasoup.load(); 40 | await this.mediasoup.producerVideoStart(); 41 | await this.mediasoup.producerAudioStart(); 42 | }); 43 | 44 | this.socket.on('mediaClientConnected', async (msg: { user_id: string }) => { 45 | // pass 46 | }); 47 | 48 | this.socket.on('mediaClientDisconnect', async (msg: { user_id: string }) => { 49 | // await this.mediasoup.deletePeer(msg.user_id); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediasoup-angular-example", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^8.2.8", 15 | "@angular/common": "^8.2.8", 16 | "@angular/compiler": "^8.2.8", 17 | "@angular/core": "^8.2.8", 18 | "@angular/forms": "^8.2.8", 19 | "@angular/platform-browser": "^8.2.8", 20 | "@angular/platform-browser-dynamic": "^8.2.8", 21 | "@angular/router": "^8.2.8", 22 | "mediasoup-client": "^3.2.6", 23 | "ngx-logger": "^4.0.5", 24 | "rxjs": "~6.4.0", 25 | "socket.io": "^2.3.0", 26 | "socket.io-promise": "^1.1.1", 27 | "tern": "^0.24.1", 28 | "tslib": "^1.10.0", 29 | "zone.js": "~0.9.1" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "^0.802.2", 33 | "@angular/cli": "^8.2.2", 34 | "@angular/compiler-cli": "^8.2.8", 35 | "@angular/language-service": "^8.2.8", 36 | "@types/jasmine": "~3.3.8", 37 | "@types/jasminewd2": "^2.0.7", 38 | "@types/node": "~8.9.4", 39 | "@types/socket.io": "^2.1.3", 40 | "@types/socket.io-client": "^1.4.32", 41 | "codelyzer": "^5.1.2", 42 | "jasmine-core": "~3.4.0", 43 | "jasmine-spec-reporter": "~4.2.1", 44 | "karma": "~4.1.0", 45 | "karma-chrome-launcher": "~2.2.0", 46 | "karma-coverage-istanbul-reporter": "~2.0.1", 47 | "karma-jasmine": "~2.0.1", 48 | "karma-jasmine-html-reporter": "^1.4.0", 49 | "protractor": "~5.4.0", 50 | "ts-node": "~7.0.0", 51 | "tslint": "~5.15.0", 52 | "typescript": "~3.5.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/core/test/test.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; 2 | import { NGXLogger } from 'ngx-logger'; 3 | 4 | import { WssService } from 'src/app/wss/wss.service'; 5 | 6 | @Component({ 7 | selector: 'app-test', 8 | templateUrl: './test.component.html', 9 | styleUrls: ['./test.component.scss'] 10 | }) 11 | export class TestComponent implements OnInit { 12 | 13 | @ViewChild('producerVideo', { static: false }) producerVideo: ElementRef; 14 | @ViewChild('consumerVideo', { static: false }) consumerVideo: ElementRef; 15 | @ViewChild('consumerAudio', { static: false }) consumerAudio: ElementRef; 16 | 17 | // tslint:disable-next-line: variable-name 18 | private readonly user_id: string = 'aaa' + Math.random(); 19 | 20 | constructor( 21 | private readonly logger: NGXLogger, 22 | private wssService: WssService 23 | ) { } 24 | 25 | async ngOnInit() { 26 | this.wssService.connect(this.user_id); 27 | } 28 | 29 | public showProducerVideo() { 30 | this.producerVideo.nativeElement.srcObject = this.wssService.mediasoup.producerVideoStream; 31 | } 32 | 33 | public showConsumerVideo() { 34 | const keys = Array.from(this.wssService.mediasoup.consumersVideoStream.keys()); 35 | this.consumerVideo.nativeElement.srcObject = this.wssService.mediasoup.consumersVideoStream.get(keys[0]); 36 | } 37 | 38 | public showConsumerAudio() { 39 | const keys = Array.from(this.wssService.mediasoup.consumersAudioStream.keys()); 40 | this.consumerAudio.nativeElement.srcObject = this.wssService.mediasoup.consumersAudioStream.get(keys[0]); 41 | } 42 | 43 | public pauseProducerVideo() { 44 | this.wssService.mediasoup.producerVideoPause(); 45 | } 46 | 47 | public resumeProducerVideo() { 48 | this.wssService.mediasoup.producerVideoResume(); 49 | } 50 | 51 | public pauseProducerAudio() { 52 | this.wssService.mediasoup.producerAudioPause(); 53 | } 54 | 55 | public resumeProducerAudio() { 56 | this.wssService.mediasoup.producerAudioResume(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /mediasoup-client/DataConsumer.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: no-any 2 | // tslint:disable: jsdoc-format 3 | // tslint:disable: no-redundant-jsdoc 4 | 5 | export interface IDataConsumer { 6 | /** 7 | * @private 8 | * 9 | * @emits transportclose 10 | * @emits open 11 | * @emits {Object} error 12 | * @emits close 13 | * @emits {Any} message 14 | * @emits @close 15 | */ 16 | new ({ 17 | id, 18 | dataProducerId, 19 | dataChannel, 20 | sctpStreamParameters, 21 | appData, 22 | }: { 23 | id: string; 24 | dataProducerId: string; 25 | dataChannel: RTCDataChannel; 26 | sctpStreamParameters: any; 27 | appData: object; 28 | }); 29 | 30 | /** 31 | * DataConsumer id. 32 | * 33 | * @returns {String} 34 | */ 35 | id: string; 36 | 37 | /** 38 | * Associated DataProducer id. 39 | * 40 | * @returns {String} 41 | */ 42 | dataProducerId: string; 43 | 44 | /** 45 | * Whether the DataConsumer is closed. 46 | * 47 | * @returns {Boolean} 48 | */ 49 | closed: boolean; 50 | 51 | /** 52 | * SCTP stream parameters. 53 | * 54 | * @returns {RTCSctpStreamParameters} 55 | */ 56 | sctpStreamParameters: any; 57 | 58 | /** 59 | * DataChannel readyState. 60 | * 61 | * @returns {String} 62 | */ 63 | readyState: string; 64 | 65 | /** 66 | * DataChannel label. 67 | * 68 | * @returns {String} 69 | */ 70 | label: string; 71 | 72 | /** 73 | * DataChannel protocol. 74 | * 75 | * @returns {String} 76 | */ 77 | protocol: string; 78 | 79 | /** 80 | * DataChannel binaryType. 81 | * 82 | * @returns {String} 83 | */ 84 | binaryType: string; 85 | 86 | /** 87 | * App custom data. 88 | * 89 | * @returns {Object} 90 | */ 91 | appData: object; 92 | 93 | /** 94 | * Closes the DataConsumer. 95 | */ 96 | close(): void; 97 | 98 | // /** 99 | // * Transport was closed. 100 | // * 101 | // * @private 102 | // */ 103 | // transportClosed(): void; 104 | } 105 | -------------------------------------------------------------------------------- /mediasoup-client/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IVideoOrientation { 2 | readonly camera: boolean; // Whether the source is a video camera 3 | readonly flip: boolean; // Whether the video source is flipped 4 | readonly rotation: number; // Rotation degrees (0, 90, 180 or 270) 5 | } 6 | 7 | export interface IIceSelectedTuple { 8 | readonly localIp: string; 9 | readonly localPort: number; 10 | readonly protocol: 'udp' | 'tcp'; 11 | readonly remoteIp: string; 12 | readonly remotePort: number; 13 | } 14 | 15 | export interface ITransportStat { 16 | readonly availableIncomingBitrate: number; 17 | readonly bytesReceived: number; 18 | readonly bytesSent: number; 19 | readonly dtlsState: TState; 20 | readonly iceRole: 'controlled'; 21 | readonly iceSelectedTuple: IIceSelectedTuple; 22 | readonly iceState: TState; 23 | readonly maxIncomingBitrate: number; 24 | readonly recvBitrate: number; 25 | readonly sctpState: TState; 26 | readonly sendBitrate: number; 27 | readonly timestamp: number; 28 | readonly transportId: string; // uuid 29 | readonly type: 'webrtc-transport'; 30 | } 31 | 32 | export interface IPeerStat { 33 | readonly bitrate: number; 34 | readonly byteCount: number; 35 | readonly firCount: number; 36 | readonly fractionLost: number; 37 | readonly kind: TKind; 38 | readonly mimeType: string; 39 | readonly nackCount: number; 40 | readonly nackPacketCount: number; 41 | readonly packetCount: number; 42 | readonly packetsDiscarded: number; 43 | readonly packetsLost: number; 44 | readonly packetsRepaired: number; 45 | readonly packetsRetransmitted: number; 46 | readonly pliCount: number; 47 | readonly roundTripTime: number; 48 | readonly rtxSsrc: number; 49 | readonly score: number; // RTP stream score (from 0 to 10) representing the transmission quality. 50 | readonly ssrc: number; 51 | readonly timestamp: number; 52 | readonly type: 'outbound-rtp' | 'inbound-rtp'; 53 | } 54 | 55 | export type TState = 'new' | 'connecting' | 'connected' | 'failed' | 'closed'; 56 | export type TPeer = 'producer' | 'consumer'; 57 | export type TKind = 'video' | 'audio'; 58 | -------------------------------------------------------------------------------- /mediasoup-client/DataProducer.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: no-any 2 | // tslint:disable: jsdoc-format 3 | // tslint:disable: no-redundant-jsdoc 4 | 5 | export interface IDataProducer { 6 | /** 7 | * @private 8 | * 9 | * @emits transportclose 10 | * @emits open 11 | * @emits {Object} error 12 | * @emits close 13 | * @emits bufferedamountlow 14 | * @emits @close 15 | */ 16 | new ({ 17 | id, 18 | dataChannel, 19 | sctpStreamParameters, 20 | appData, 21 | }: { 22 | id: string; 23 | dataChannel: RTCDataChannel; 24 | sctpStreamParameters: any; 25 | appData: object; 26 | }); 27 | 28 | /** 29 | * DataProducer id. 30 | * 31 | * @returns {String} 32 | */ 33 | id: string; 34 | 35 | /** 36 | * Whether the DataProducer is closed. 37 | * 38 | * @returns {Boolean} 39 | */ 40 | closed: boolean; 41 | 42 | /** 43 | * SCTP stream parameters. 44 | * 45 | * @returns {RTCSctpStreamParameters} 46 | */ 47 | sctpStreamParameters: any; 48 | 49 | /** 50 | * DataChannel readyState. 51 | * 52 | * @returns {String} 53 | */ 54 | readyState: string; 55 | 56 | /** 57 | * DataChannel label. 58 | * 59 | * @returns {String} 60 | */ 61 | label: string; 62 | 63 | /** 64 | * DataChannel protocol. 65 | * 66 | * @returns {String} 67 | */ 68 | protocol: string; 69 | 70 | /** 71 | * DataChannel bufferedAmount. 72 | * 73 | * @returns {String} 74 | */ 75 | bufferedAmount: string; 76 | 77 | /** 78 | * DataChannel bufferedAmountLowThreshold. 79 | * 80 | * @returns {String} 81 | */ 82 | bufferedAmountLowThreshold: string; 83 | 84 | /** 85 | * App custom data. 86 | * 87 | * @returns {Object} 88 | */ 89 | appData: object; 90 | 91 | /** 92 | * Closes the DataProducer. 93 | */ 94 | close(): void; 95 | 96 | /** 97 | * Send a message. 98 | * 99 | * @param {String|Blob|ArrayBuffer|ArrayBufferView} data. 100 | * 101 | * @throws {InvalidStateError} if DataProducer closed. 102 | * @throws {TypeError} if wrong arguments. 103 | * @param data 104 | */ 105 | send(data: string | Blob | ArrayBuffer | ArrayBufferView): void; 106 | 107 | // /** 108 | // * Transport was closed. 109 | // * 110 | // * @private 111 | // */ 112 | // transportClosed(): void; 113 | } 114 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "variable-name": [ 13 | true, 14 | "ban-keywords", 15 | "check-format", 16 | "allow-leading-underscore", 17 | "allow-snake-case" 18 | ], 19 | "directive-selector": [ 20 | true, 21 | "attribute", 22 | "app", 23 | "camelCase" 24 | ], 25 | "component-selector": [ 26 | true, 27 | "element", 28 | "app", 29 | "kebab-case" 30 | ], 31 | "import-blacklist": [ 32 | true, 33 | "rxjs/Rx" 34 | ], 35 | "interface-name": false, 36 | "max-classes-per-file": false, 37 | "max-line-length": [ 38 | true, 39 | 140 40 | ], 41 | "member-access": false, 42 | "member-ordering": [ 43 | true, 44 | { 45 | "order": [ 46 | "static-field", 47 | "instance-field", 48 | "static-method", 49 | "instance-method" 50 | ] 51 | } 52 | ], 53 | "no-consecutive-blank-lines": false, 54 | "no-console": [ 55 | true, 56 | "debug", 57 | "info", 58 | "time", 59 | "timeEnd", 60 | "trace" 61 | ], 62 | "no-empty": false, 63 | "no-inferrable-types": [ 64 | true, 65 | "ignore-params" 66 | ], 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-switch-case-fall-through": true, 70 | "no-use-before-declare": true, 71 | "no-var-requires": false, 72 | "object-literal-key-quotes": [ 73 | true, 74 | "as-needed" 75 | ], 76 | "object-literal-sort-keys": false, 77 | "ordered-imports": false, 78 | "quotemark": [ 79 | true, 80 | "single" 81 | ], 82 | "trailing-comma": false, 83 | "no-conflicting-lifecycle": true, 84 | "no-host-metadata-property": true, 85 | "no-input-rename": true, 86 | "no-inputs-metadata-property": true, 87 | "no-output-native": true, 88 | "no-output-on-prefix": true, 89 | "no-output-rename": true, 90 | "no-outputs-metadata-property": true, 91 | "template-banana-in-box": true, 92 | "template-no-negated-async": true, 93 | "use-lifecycle-interface": true, 94 | "use-pipe-transform-interface": true 95 | }, 96 | "rulesDirectory": [ 97 | "codelyzer" 98 | ] 99 | } -------------------------------------------------------------------------------- /mediasoup-client/Consumer.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: no-any 2 | // tslint:disable: jsdoc-format 3 | // tslint:disable: no-redundant-jsdoc 4 | 5 | export interface IConsumer { 6 | /** 7 | * @private 8 | * 9 | * @emits transportclose 10 | * @emits trackended 11 | * @emits @getstats 12 | * @emits @close 13 | */ 14 | new ({ 15 | id, 16 | localId, 17 | producerId, 18 | track, 19 | rtpParameters, 20 | appData, 21 | }: { 22 | id: string; 23 | localId: string; 24 | producerId: string; 25 | track: MediaStreamTrack; 26 | rtpParameters: RTCRtpParameters; 27 | appData?: object; 28 | }); 29 | 30 | /** 31 | * Consumer id. 32 | * 33 | * @returns {String} 34 | */ 35 | id: string; 36 | 37 | /** 38 | * Local id. 39 | * 40 | * @private 41 | * @returns {String} 42 | */ 43 | localId: string; 44 | 45 | /** 46 | * Associated Producer id. 47 | * 48 | * @returns {String} 49 | */ 50 | producerId: string; 51 | 52 | /** 53 | * Whether the Consumer is closed. 54 | * 55 | * @returns {Boolean} 56 | */ 57 | closed: boolean; 58 | 59 | /** 60 | * Media kind. 61 | * 62 | * @returns {String} 63 | */ 64 | kind: 'audio' | 'video'; 65 | 66 | /** 67 | * The associated track. 68 | * 69 | * @returns {MediaStreamTrack} 70 | */ 71 | track: MediaStreamTrack; 72 | 73 | /** 74 | * RTP parameters. 75 | * 76 | * @returns {RTCRtpParameters} 77 | */ 78 | rtpParameters: RTCRtpParameters; 79 | 80 | /** 81 | * Whether the Consumer is paused. 82 | * 83 | * @returns {Boolean} 84 | */ 85 | paused: boolean; 86 | 87 | /** 88 | * App custom data. 89 | * 90 | * @returns {Object} 91 | */ 92 | appData: object; 93 | 94 | on(type: any, listener: (...params: any) => Promise | void): Promise | void; 95 | 96 | /** 97 | * Closes the Consumer. 98 | */ 99 | close(): void; 100 | 101 | // /** 102 | // * Transport was closed. 103 | // * 104 | // * @private 105 | // */ 106 | // transportClosed(): void; 107 | 108 | /** 109 | * Get associated RTCRtpReceiver stats. 110 | * 111 | * @async 112 | * @returns {RTCStatsReport} 113 | * @throws {InvalidStateError} if Consumer closed. 114 | * @return 115 | */ 116 | getStats(): Promise; 117 | 118 | /** 119 | * Pauses receiving media. 120 | */ 121 | pause(): void; 122 | 123 | /** 124 | * Resumes receiving media. 125 | */ 126 | resume(): void; 127 | } 128 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /mediasoup-client/Producer.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: no-any 2 | // tslint:disable: jsdoc-format 3 | // tslint:disable: no-redundant-jsdoc 4 | 5 | export interface IProducer { 6 | /** 7 | * @private 8 | * 9 | * @emits transportclose 10 | * @emits trackended 11 | * @emits {track: MediaStreamTrack} @replacetrack 12 | * @emits {spatialLayer: String} @setmaxspatiallayer 13 | * @emits @getstats 14 | * @emits @close 15 | */ 16 | new ({ 17 | id, 18 | localId, 19 | track, 20 | rtpParameters, 21 | appData, 22 | }: { 23 | id: string; 24 | localId: string; 25 | track: MediaStreamTrack; 26 | rtpParameters: RTCRtpParameters; 27 | appData?: object; 28 | }); 29 | 30 | /** 31 | * Producer id. 32 | * 33 | * @returns {String} 34 | */ 35 | id: string; 36 | 37 | /** 38 | * Local id. 39 | * 40 | * @private 41 | * @returns {String} 42 | */ 43 | localId: string; 44 | 45 | /** 46 | * Whether the Producer is closed. 47 | * 48 | * @returns {Boolean} 49 | */ 50 | closed: boolean; 51 | 52 | /** 53 | * Media kind. 54 | * 55 | * @returns {String} 56 | */ 57 | kind: 'audio' | 'video'; 58 | 59 | /** 60 | * The associated track. 61 | * 62 | * @returns {MediaStreamTrack} 63 | */ 64 | track: MediaStreamTrack; 65 | 66 | /** 67 | * RTP parameters. 68 | * 69 | * @returns {RTCRtpParameters} 70 | */ 71 | rtpParameters: RTCRtpParameters; 72 | 73 | /** 74 | * Whether the Producer is paused. 75 | * 76 | * @returns {Boolean} 77 | */ 78 | paused: boolean; 79 | 80 | /** 81 | * Max spatial layer. 82 | * 83 | * @type {Number} 84 | */ 85 | maxSpatialLayer: number; 86 | 87 | /** 88 | * App custom data. 89 | * 90 | * @type {Object} 91 | */ 92 | appData: object; 93 | 94 | on(type: any, listener: (...params: any) => Promise | void): Promise | void; 95 | 96 | /** 97 | * Closes the Producer. 98 | */ 99 | close(): void; 100 | 101 | // /** 102 | // * Transport was closed. 103 | // * 104 | // * @private 105 | // */ 106 | // transportClosed(); 107 | 108 | /** 109 | * Get associated RTCRtpSender stats. 110 | * 111 | * @promise 112 | * @returns {RTCStatsReport} 113 | * @throws {InvalidStateError} if Producer closed. 114 | */ 115 | getStats(): Promise; 116 | 117 | /** 118 | * Pauses sending media. 119 | */ 120 | pause(): void; 121 | 122 | /** 123 | * Resumes sending media. 124 | */ 125 | resume(): void; 126 | 127 | /** 128 | * Replaces the current track with a new one. 129 | * 130 | * @param {MediaStreamTrack} track - New track. 131 | * 132 | * @async 133 | * @throws {InvalidStateError} if Producer closed or track ended. 134 | * @throws {TypeError} if wrong arguments. 135 | */ 136 | replaceTrack({ track }: { track: MediaStreamTrack }): Promise; 137 | 138 | /** 139 | * Sets the video max spatial layer to be sent. 140 | * 141 | * @param {Number} spatialLayer 142 | * 143 | * @async 144 | * @throws {InvalidStateError} if Producer closed. 145 | * @throws {UnsupportedError} if not a video Producer. 146 | * @throws {TypeError} if wrong arguments. 147 | */ 148 | setMaxSpatialLayer(spatialLayer: number): Promise; 149 | } 150 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "mediasoup-angular-example": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/mediasoup-angular-example", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "src/styles.scss" 32 | ], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "fileReplacements": [ 38 | { 39 | "replace": "src/environments/environment.ts", 40 | "with": "src/environments/environment.prod.ts" 41 | } 42 | ], 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "extractCss": true, 47 | "namedChunks": false, 48 | "aot": true, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "budgets": [ 53 | { 54 | "type": "initial", 55 | "maximumWarning": "2mb", 56 | "maximumError": "5mb" 57 | }, 58 | { 59 | "type": "anyComponentStyle", 60 | "maximumWarning": "6kb", 61 | "maximumError": "10kb" 62 | } 63 | ] 64 | } 65 | } 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "options": { 70 | "browserTarget": "mediasoup-angular-example:build" 71 | }, 72 | "configurations": { 73 | "production": { 74 | "browserTarget": "mediasoup-angular-example:build:production" 75 | } 76 | } 77 | }, 78 | "extract-i18n": { 79 | "builder": "@angular-devkit/build-angular:extract-i18n", 80 | "options": { 81 | "browserTarget": "mediasoup-angular-example:build" 82 | } 83 | }, 84 | "test": { 85 | "builder": "@angular-devkit/build-angular:karma", 86 | "options": { 87 | "main": "src/test.ts", 88 | "polyfills": "src/polyfills.ts", 89 | "tsConfig": "tsconfig.spec.json", 90 | "karmaConfig": "karma.conf.js", 91 | "assets": [ 92 | "src/favicon.ico", 93 | "src/assets" 94 | ], 95 | "styles": [ 96 | "src/styles.scss" 97 | ], 98 | "scripts": [] 99 | } 100 | }, 101 | "lint": { 102 | "builder": "@angular-devkit/build-angular:tslint", 103 | "options": { 104 | "tsConfig": [ 105 | "tsconfig.app.json", 106 | "tsconfig.spec.json", 107 | "e2e/tsconfig.json" 108 | ], 109 | "exclude": [ 110 | "**/node_modules/**" 111 | ] 112 | } 113 | }, 114 | "e2e": { 115 | "builder": "@angular-devkit/build-angular:protractor", 116 | "options": { 117 | "protractorConfig": "e2e/protractor.conf.js", 118 | "devServerTarget": "mediasoup-angular-example:serve" 119 | }, 120 | "configurations": { 121 | "production": { 122 | "devServerTarget": "mediasoup-angular-example:serve:production" 123 | } 124 | } 125 | } 126 | } 127 | }}, 128 | "defaultProject": "mediasoup-angular-example" 129 | } -------------------------------------------------------------------------------- /mediasoup-client/Device.ts: -------------------------------------------------------------------------------- 1 | import { ITransport } from './Transport'; 2 | 3 | // tslint:disable: no-any 4 | // tslint:disable: jsdoc-format 5 | // tslint:disable: no-redundant-jsdoc 6 | 7 | export interface IDevice { 8 | /** 9 | * Create a new Device to connect to mediasoup server. 10 | * 11 | * @param {Class|String} [Handler] - An optional RTC handler class for unsupported or 12 | * custom devices (not needed when running in a browser). If a String, it will 13 | * force usage of the given built-in handler. 14 | * 15 | * @throws {UnsupportedError} if device is not supported. 16 | */ 17 | new ({ Handler }: { Handler?: any }); 18 | 19 | /** 20 | * Whether the Device is loaded. 21 | * 22 | * @returns {Boolean} 23 | */ 24 | loaded: boolean; 25 | 26 | /** 27 | * The RTC handler class name ('Chrome70', 'Firefox65', etc). 28 | * 29 | * @returns {RTCRtpCapabilities} 30 | */ 31 | handlerName: RTCRtpCapabilities; 32 | 33 | /** 34 | * RTP capabilities of the Device for receiving media. 35 | * 36 | * @returns {RTCRtpCapabilities} 37 | * @throws {InvalidStateError} if not loaded. 38 | */ 39 | rtpCapabilities: string; 40 | 41 | /** 42 | * SCTP capabilities of the Device. 43 | * 44 | * @returns {Object} 45 | * @throws {InvalidStateError} if not loaded. 46 | */ 47 | sctpCapabilities: object; 48 | 49 | /** 50 | * Initialize the Device. 51 | * 52 | * @param {RTCRtpCapabilities} routerRtpCapabilities - Router RTP capabilities. 53 | * 54 | * @async 55 | * @throws {TypeError} if missing/wrong arguments. 56 | * @throws {InvalidStateError} if already loaded. 57 | * @param {routerRtpCapabilities} 58 | */ 59 | load({ routerRtpCapabilities }: { routerRtpCapabilities: RTCRtpCapabilities }): Promise; 60 | 61 | /** 62 | * Whether we can produce audio/video. 63 | * 64 | * @param {String} kind - 'audio' or 'video'. 65 | * 66 | * @returns {Boolean} 67 | * @throws {InvalidStateError} if not loaded. 68 | * @throws {TypeError} if wrong arguments. 69 | */ 70 | canProduce(kind: string): boolean; 71 | 72 | /** 73 | * Creates a Transport for sending media. 74 | * 75 | * @param {String} - Server-side Transport id. 76 | * @param {RTCIceParameters} iceParameters - Server-side Transport ICE parameters. 77 | * @param {Array} [iceCandidates] - Server-side Transport ICE candidates. 78 | * @param {RTCDtlsParameters} dtlsParameters - Server-side Transport DTLS parameters. 79 | * @param {Object} [sctpParameters] - Server-side SCTP parameters. 80 | * @param {Array} [iceServers] - Array of ICE servers. 81 | * @param {RTCIceTransportPolicy} [iceTransportPolicy] - ICE transport 82 | * policy. 83 | * @param {Object} [proprietaryConstraints] - RTCPeerConnection proprietary constraints. 84 | * @param {Object} [appData={}] - Custom app data. 85 | * 86 | * @returns {Transport} 87 | * @throws {InvalidStateError} if not loaded. 88 | * @throws {TypeError} if wrong arguments. 89 | */ 90 | createSendTransport({ 91 | id, 92 | iceParameters, 93 | iceCandidates, 94 | dtlsParameters, 95 | sctpParameters, 96 | iceServers, 97 | iceTransportPolicy, 98 | proprietaryConstraints, 99 | appData, 100 | }: { 101 | id: string; 102 | iceParameters: RTCIceParameters; 103 | iceCandidates: RTCIceCandidate[]; 104 | dtlsParameters: RTCDtlsParameters; 105 | sctpParameters?: object; 106 | iceServers?: RTCIceServer[]; 107 | iceTransportPolicy?: RTCIceTransportPolicy; 108 | proprietaryConstraints?: object; 109 | appData?: object; 110 | }): ITransport; 111 | 112 | /** 113 | * Creates a Transport for receiving media. 114 | * 115 | * @param {String} - Server-side Transport id. 116 | * @param {RTCIceParameters} iceParameters - Server-side Transport ICE parameters. 117 | * @param {Array} [iceCandidates] - Server-side Transport ICE candidates. 118 | * @param {RTCDtlsParameters} dtlsParameters - Server-side Transport DTLS parameters. 119 | * @param {Object} [sctpParameters] - Server-side SCTP parameters. 120 | * @param {Array} [iceServers] - Array of ICE servers. 121 | * @param {RTCIceTransportPolicy} [iceTransportPolicy] - ICE transport 122 | * policy. 123 | * @param {Object} [proprietaryConstraints] - RTCPeerConnection proprietary constraints. 124 | * @param {Object} [appData={}] - Custom app data. 125 | * 126 | * @returns {Transport} 127 | * @throws {InvalidStateError} if not loaded. 128 | * @throws {TypeError} if wrong arguments. 129 | */ 130 | createRecvTransport({ 131 | id, 132 | iceParameters, 133 | iceCandidates, 134 | dtlsParameters, 135 | sctpParameters, 136 | iceServers, 137 | iceTransportPolicy, 138 | proprietaryConstraints, 139 | appData, 140 | }: { 141 | id: string; 142 | iceParameters: RTCIceParameters; 143 | iceCandidates: RTCIceCandidate[]; 144 | dtlsParameters: RTCDtlsParameters; 145 | sctpParameters?: object; 146 | iceServers?: RTCIceServer[]; 147 | iceTransportPolicy?: RTCIceTransportPolicy; 148 | proprietaryConstraints?: object; 149 | appData?: object; 150 | }): ITransport; 151 | } 152 | -------------------------------------------------------------------------------- /mediasoup-client/Transport.ts: -------------------------------------------------------------------------------- 1 | import { IConsumer } from './Consumer'; 2 | import { IDataConsumer } from './DataConsumer'; 3 | import { IDataProducer } from './DataProducer'; 4 | import { IProducer } from './Producer'; 5 | 6 | // tslint:disable: no-any 7 | // tslint:disable: jsdoc-format 8 | // tslint:disable: no-redundant-jsdoc 9 | 10 | export interface ITransport { 11 | /** 12 | * @private 13 | * 14 | * @emits {transportLocalParameters: Object, callback: Function, errback: Function} connect 15 | * @emits {connectionState: String} connectionstatechange 16 | * @emits {producerLocalParameters: Object, callback: Function, errback: Function} produce 17 | * @emits {dataProducerLocalParameters: Object, callback: Function, errback: Function} producedata 18 | */ 19 | new ({ 20 | direction, 21 | id, 22 | iceParameters, 23 | iceCandidates, 24 | dtlsParameters, 25 | sctpParameters, 26 | iceServers, 27 | iceTransportPolicy, 28 | proprietaryConstraints, 29 | appData, 30 | Handler, 31 | extendedRtpCapabilities, 32 | canProduceByKind, 33 | }: { 34 | direction: string; 35 | id: string; 36 | iceParameters: RTCIceParameters; 37 | iceCandidates: RTCIceCandidate[]; 38 | dtlsParameters: RTCDtlsParameters; 39 | sctpParameters: any; 40 | iceServers: RTCIceServer[]; 41 | iceTransportPolicy: RTCIceTransportPolicy; 42 | proprietaryConstraints: object; 43 | appData?: object; 44 | Handler: any; 45 | extendedRtpCapabilities: object; 46 | canProduceByKind: object; 47 | }); 48 | 49 | /** 50 | * Transport id. 51 | * 52 | * @returns {String} 53 | */ 54 | id: string; 55 | 56 | /** 57 | * Whether the Transport is closed. 58 | * 59 | * @returns {Boolean} 60 | */ 61 | closed: boolean; 62 | 63 | /** 64 | * Transport direction. 65 | * 66 | * @returns {String} 67 | */ 68 | direction: string; 69 | 70 | /** 71 | * RTC handler instance. 72 | * 73 | * @returns {Handler} 74 | */ 75 | handler: any; 76 | 77 | /** 78 | * Connection state. 79 | * 80 | * @returns {String} 81 | */ 82 | connectionState: string; 83 | 84 | /** 85 | * App custom data. 86 | * 87 | * @returns {Object} 88 | */ 89 | appData: object; 90 | 91 | on(type: any, listener: (...params: any) => Promise | void): Promise | void; 92 | 93 | /** 94 | * Close the Transport. 95 | */ 96 | close(): void; 97 | 98 | /** 99 | * Get associated Transport (RTCPeerConnection) stats. 100 | * 101 | * @async 102 | * @returns {RTCStatsReport} 103 | * @throws {InvalidStateError} if Transport closed. 104 | */ 105 | getStats(): Promise; 106 | 107 | /** 108 | * Restart ICE connection. 109 | * 110 | * @param {RTCIceParameters} iceParameters - New Server-side Transport ICE parameters. 111 | * 112 | * @async 113 | * @throws {InvalidStateError} if Transport closed. 114 | * @throws {TypeError} if wrong arguments. 115 | */ 116 | restartIce({ iceParameters }: { iceParameters: RTCIceParameters }): Promise; 117 | 118 | /** 119 | * Update ICE servers. 120 | * 121 | * @param {Array} [iceServers] - Array of ICE servers. 122 | * 123 | * @async 124 | * @throws {InvalidStateError} if Transport closed. 125 | * @throws {TypeError} if wrong arguments. 126 | */ 127 | updateIceServers({ iceServers }: { iceServers: RTCIceServer[] }): Promise; 128 | 129 | /** 130 | * Create a Producer. 131 | * 132 | * @param {MediaStreamTrack} track - Track to sent. 133 | * @param {Array} [encodings] - Encodings. 134 | * @param {Object} [codecOptions] - Codec options. 135 | * @param {Object} [appData={}] - Custom app data. 136 | * 137 | * @async 138 | * @returns {Producer} 139 | * @throws {InvalidStateError} if Transport closed or track ended. 140 | * @throws {TypeError} if wrong arguments. 141 | * @throws {UnsupportedError} if Transport direction is incompatible or 142 | * cannot produce the given media kind. 143 | */ 144 | produce({ 145 | track, 146 | encodings, 147 | codecOptions, 148 | appData, 149 | }: { 150 | track: MediaStreamTrack; 151 | encodings?: RTCRtpCodingParameters[]; 152 | codecOptions?: object; 153 | appData?: object; 154 | }): Promise; 155 | 156 | /** 157 | * Create a Consumer to consume a remote Producer. 158 | * 159 | * @param {String} id - Server-side Consumer id. 160 | * @param {String} producerId - Server-side Producer id. 161 | * @param {String} kind - 'audio' or 'video'. 162 | * @param {RTCRtpParameters} rtpParameters - Server-side Consumer RTP parameters. 163 | * @param {Object} [appData={}] - Custom app data. 164 | * 165 | * @async 166 | * @returns {Consumer} 167 | * @throws {InvalidStateError} if Transport closed. 168 | * @throws {TypeError} if wrong arguments. 169 | * @throws {UnsupportedError} if Transport direction is incompatible. 170 | */ 171 | consume({ 172 | id, 173 | producerId, 174 | kind, 175 | rtpParameters, 176 | appData, 177 | }: { 178 | id: string; 179 | producerId: string; 180 | kind: string; 181 | rtpParameters: RTCRtpParameters; 182 | appData?: object; 183 | }): Promise; 184 | 185 | /** 186 | * Create a DataProducer 187 | * 188 | * @param {Boolean} [ordered=true] 189 | * @param {Number} [maxPacketLifeTime] 190 | * @param {Number} [maxRetransmits] 191 | * @param {String} [priority='low'] // 'very-low' / 'low' / 'medium' / 'high' 192 | * @param {String} [label=''] 193 | * @param {String} [protocol=''] 194 | * @param {Object} [appData={}] - Custom app data. 195 | * 196 | * @async 197 | * @returns {DataProducer} 198 | * @throws {InvalidStateError} if Transport closed. 199 | * @throws {TypeError} if wrong arguments. 200 | * @throws {UnsupportedError} if Transport direction is incompatible or remote 201 | * transport does not enable SCTP. 202 | */ 203 | produceData({ 204 | ordered, 205 | maxPacketLifeTime, 206 | maxRetransmits, 207 | priority, 208 | label, 209 | protocol, 210 | appData, 211 | }: { 212 | ordered?: boolean; 213 | maxPacketLifeTime: number; 214 | maxRetransmits: number; 215 | priority?: 'very-low' | 'low' | 'medium' | 'high'; 216 | label?: string; 217 | protocol?: string; 218 | appData?: object; 219 | }): Promise; 220 | 221 | /** 222 | * Create a DataConsumer 223 | * 224 | * @param {String} id - Server-side DataConsumer id. 225 | * @param {String} dataProducerId - Server-side DataProducer id. 226 | * @param {RTCSctpStreamParameters} sctpStreamParameters - Server-side DataConsumer 227 | * SCTP parameters. 228 | * @param {String} [label=''] 229 | * @param {String} [protocol=''] 230 | * @param {Object} [appData={}] - Custom app data. 231 | * 232 | * @async 233 | * @returns {DataConsumer} 234 | * @throws {InvalidStateError} if Transport closed. 235 | * @throws {TypeError} if wrong arguments. 236 | * @throws {UnsupportedError} if Transport direction is incompatible or remote 237 | * transport does not enable SCTP. 238 | */ 239 | consumeData({ 240 | id, 241 | dataProducerId, 242 | sctpStreamParameters, 243 | label, 244 | protocol, 245 | appData, 246 | }: { 247 | id: string; 248 | dataProducerId: string; 249 | sctpStreamParameters: any; 250 | label?: string; 251 | protocol?: string; 252 | appData?: object; 253 | }): Promise; 254 | 255 | /** 256 | * 257 | */ 258 | _handleHandler(): void; 259 | 260 | /** 261 | * 262 | * @param producer 263 | */ 264 | _handleProducer(producer: IProducer): void; 265 | /** 266 | * 267 | * @param consumer 268 | */ 269 | _handleConsumer(consumer: IConsumer): void; 270 | /** 271 | * 272 | * @param dataProducer 273 | */ 274 | _handleDataProducer(dataProducer: IDataProducer): void; 275 | /** 276 | * 277 | * @param dataConsumer 278 | */ 279 | _handleDataConsumer(dataConsumer: IDataConsumer): void; 280 | } 281 | -------------------------------------------------------------------------------- /src/app/wss/wss.mediasoup.ts: -------------------------------------------------------------------------------- 1 | import * as mediasoupClient from 'mediasoup-client'; 2 | import { IDevice } from 'mediasoup-client/Device'; 3 | import { ITransport } from 'mediasoup-client/Transport'; 4 | import { IProducer } from 'mediasoup-client/Producer'; 5 | import { IConsumer } from 'mediasoup-client/Consumer'; 6 | import { TKind, TPeer, TState, IPeerStat, ITransportStat, IVideoOrientation } from 'mediasoup-client/interfaces'; 7 | 8 | type IOSocket = SocketIOClient.Socket & { request: (ioEvent: string, data?: any) => Promise }; 9 | 10 | export class MediasoupService { 11 | private mediasoupDevice: IDevice; 12 | 13 | private producerVideo: IProducer; 14 | private producerAudio: IProducer; 15 | 16 | private producerTransport: ITransport; 17 | private consumerTransport: ITransport; 18 | 19 | public producerVideoStream: MediaStream; 20 | public producerAudioStream: MediaStream; 21 | 22 | public consumersVideo: Map = new Map(); 23 | public consumersAudio: Map = new Map(); 24 | 25 | public consumersVideoStream: Map = new Map(); 26 | public consumersAudioStream: Map = new Map(); 27 | 28 | constructor(private readonly socket: IOSocket) { 29 | this.mediasoupDevice = new mediasoupClient.Device({}); 30 | 31 | /** 32 | * Когда пользователь (не current_user) начинает передавать свой стрим 33 | */ 34 | this.socket.on('mediaProduce', async (data: { user_id: string; kind: TKind }) => { 35 | try { 36 | switch (data.kind) { 37 | case 'video': 38 | await this.consumerVideoStart(data.user_id); 39 | break; 40 | case 'audio': 41 | await this.consumerAudioStart(data.user_id); 42 | break; 43 | } 44 | } catch (error) { 45 | console.error(error.message, error.stack); 46 | } 47 | }); 48 | 49 | /** 50 | * Когда пользователь (любой) поворачивает камеру 51 | */ 52 | this.socket.on('mediaVideoOrientationChange', async (data: { 53 | user_id: string; videoOrientation: IVideoOrientation 54 | }) => { 55 | console.log('mediaVideoOrientationChange', data); 56 | }); 57 | 58 | /** 59 | * Когда пользователю (current_user) необходимо заново переподключить стрим 60 | */ 61 | this.socket.on('mediaReproduce', async (data: { kind: TKind }) => { 62 | try { 63 | switch (data.kind) { 64 | case 'audio': 65 | this.producerAudioStart(); 66 | break; 67 | case 'video': 68 | this.producerVideoStart(); 69 | break; 70 | } 71 | } catch (error) { 72 | console.error(error.message, error.stack); 73 | } 74 | }); 75 | 76 | /** 77 | * Когда пользователь (не current_user) ставит свой стрим на паузу 78 | */ 79 | this.socket.on('mediaProducerPause', async (data: { user_id: string; kind: TKind }) => { 80 | console.log('mediaProducerPause', data); 81 | }); 82 | 83 | /** 84 | * Когда пользователь (не current_user) снимает свой стрим с паузы 85 | */ 86 | this.socket.on('mediaProducerResume', async (data: { user_id: string; kind: TKind }) => { 87 | console.log('mediaProducerResume', data); 88 | }); 89 | 90 | /** 91 | * Когда кто-то разговаривает 92 | */ 93 | this.socket.on('mediaActiveSpeaker', async (data: { user_id: string; volume: number }) => { 94 | console.log('mediaActiveSpeaker', data); 95 | }); 96 | 97 | /** 98 | * Когда в комнате сменился воркер медиасупа и требуется переподключиться. 99 | */ 100 | this.socket.on('mediaReconfigure', async () => { 101 | try { 102 | await this.load(true); 103 | await this.producerAudioStart(); 104 | await this.producerVideoStart(); 105 | } catch (error) { 106 | console.error(error.message, error.stack); 107 | } 108 | }); 109 | } 110 | 111 | /** 112 | * Сменить воркер медиасупа в комнате 113 | */ 114 | public async reConfigureMedia() { 115 | try { 116 | await this.socket.request('mediaReconfigure'); 117 | } catch (error) { 118 | console.error(error.message, error.stack); 119 | } 120 | } 121 | 122 | /** 123 | * Подключиться к медиасупу 124 | * @param skipConsume не принимать стримы от уже подключенных 125 | */ 126 | public async load(skipConsume: boolean = false): Promise { 127 | try { 128 | const data: { routerRtpCapabilities: RTCRtpCapabilities } 129 | = await this.socket.request('media', { action: 'getRouterRtpCapabilities' }); 130 | 131 | if (!this.mediasoupDevice.loaded) { 132 | await this.mediasoupDevice.load(data); 133 | } 134 | 135 | await this.createProducerTransport(); 136 | await this.createConsumerTransport(); 137 | 138 | if (!skipConsume) { 139 | const audioProducerIds: string[] = await this.socket.request('media', { action: 'getAudioProducerIds' }); 140 | 141 | audioProducerIds.forEach(async (id) => { 142 | await this.consumerAudioStart(id); 143 | }); 144 | 145 | const videoProducerIds: string[] = await this.socket.request('media', { action: 'getVideoProducerIds' }); 146 | 147 | videoProducerIds.forEach(async (id) => { 148 | await this.consumerVideoStart(id); 149 | }); 150 | } 151 | } catch (error) { 152 | console.error(error.message, error.stack); 153 | } 154 | } 155 | 156 | /** 157 | * Отключиться от медиасупа 158 | */ 159 | public async close(): Promise { 160 | try { 161 | await this.producerVideoClose(); 162 | await this.producerAudioClose(); 163 | 164 | if (this.producerTransport && !this.producerTransport.closed) { 165 | this.producerTransport.close(); 166 | } 167 | 168 | if (this.consumerTransport && !this.consumerTransport.closed) { 169 | this.consumerTransport.close(); 170 | } 171 | } catch (error) { 172 | console.error(error.message, error.stack); 173 | } 174 | } 175 | 176 | /** 177 | * Создать транспорт для передачи своего стрима 178 | */ 179 | private async createProducerTransport(): Promise { 180 | try { 181 | const data: { 182 | type: TPeer, params: { id: string; iceParameters: RTCIceParameters; iceCandidates: RTCIceCandidate[]; dtlsParameters: object } 183 | } = await this.socket.request('media', { action: 'createWebRtcTransport', data: { type: 'producer' } }); 184 | 185 | this.producerTransport = this.mediasoupDevice.createSendTransport(data.params); 186 | 187 | // 'connect' | 'produce' | 'producedata' | 'connectionstatechange' 188 | this.producerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => { 189 | await this.socket.request('media', { action: 'connectWebRtcTransport', data: { dtlsParameters, type: 'producer' } }) 190 | .then(callback) 191 | .catch(errback); 192 | }); 193 | 194 | this.producerTransport.on('produce', async ({ kind, rtpParameters }, callback, errback) => { 195 | await this.socket.request('media', { 196 | action: 'produce', 197 | data: { 198 | producerTransportId: this.producerTransport.id, 199 | kind, 200 | rtpParameters, 201 | }, 202 | }).then(({ id }) => callback({ id })) 203 | .catch(errback); 204 | }); 205 | 206 | this.producerTransport.on('connectionstatechange', async (state: TState) => { 207 | switch (state) { 208 | case 'connecting': break; 209 | case 'connected': break; 210 | case 'failed': 211 | this.producerTransport.close(); 212 | break; 213 | default: break; 214 | } 215 | }); 216 | } catch (error) { 217 | console.error(error.message, error.stack); 218 | } 219 | } 220 | 221 | /** 222 | * Создать транспорт для приема стримов от других пользователей 223 | */ 224 | private async createConsumerTransport(): Promise { 225 | try { 226 | const data: { 227 | type: TPeer, params: { id: string; iceParameters: RTCIceParameters; iceCandidates: RTCIceCandidate[]; dtlsParameters: object } 228 | } = await this.socket.request('media', { action: 'createWebRtcTransport', data: { type: 'consumer'} }); 229 | 230 | this.consumerTransport = this.mediasoupDevice.createRecvTransport(data.params); 231 | 232 | // 'connect' | 'connectionstatechange' 233 | this.consumerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => { 234 | await this.socket.request('media', { action: 'connectWebRtcTransport', data: { dtlsParameters, type: 'consumer' } }) 235 | .then(callback) 236 | .catch(errback); 237 | }); 238 | 239 | this.consumerTransport.on('connectionstatechange', async (state: TState) => { 240 | switch (state) { 241 | case 'connecting': break; 242 | case 'connected': break; 243 | case 'failed': 244 | this.consumerTransport.close(); 245 | break; 246 | default: break; 247 | } 248 | }); 249 | } catch (error) { 250 | console.error(error.message, error.stack); 251 | } 252 | } 253 | 254 | /** 255 | * Начать передавать свой видео-стрим 256 | */ 257 | public async producerVideoStart(): Promise { 258 | try { 259 | if (this.mediasoupDevice.canProduce('video')) { 260 | const videoStream = await navigator.mediaDevices.getUserMedia({ video: { width: 200, height: 150 } }); 261 | const videoTrack = videoStream.getVideoTracks()[0]; 262 | 263 | if (videoTrack) { 264 | if (this.producerTransport && !this.producerTransport.closed) { 265 | this.producerVideo = await this.producerTransport.produce({ track: videoTrack }); 266 | } 267 | 268 | // 'trackended' | 'transportclose' 269 | // this.producerVideo.on('transportclose', () => {}); 270 | } 271 | this.producerVideoStream = videoStream; 272 | } 273 | } catch (error) { 274 | console.error(error.message, error.stack); 275 | } 276 | } 277 | 278 | /** 279 | * Поставить передачу своего видео-стрима на паузу 280 | */ 281 | public async producerVideoPause(): Promise { 282 | try { 283 | if (this.producerVideo && !this.producerVideo.paused) { 284 | this.producerVideo.pause(); 285 | } 286 | } catch (error) { 287 | console.error(error.message, error.stack); 288 | } 289 | } 290 | 291 | /** 292 | * Снять с паузы передапчу своего видео-стрима 293 | */ 294 | public async producerVideoResume(): Promise { 295 | try { 296 | if (this.producerVideo && this.producerVideo.paused && !this.producerVideo.closed) { 297 | this.producerVideo.resume(); 298 | } else if (this.producerVideo && this.producerVideo.closed) { 299 | await this.producerVideoStart(); 300 | } 301 | } catch (error) { 302 | console.error(error.message, error.stack); 303 | } 304 | } 305 | 306 | /** 307 | * Остановить передачу своего видео-стрима (для повторной передачи требуется пересоздать продюсера) 308 | */ 309 | public async producerVideoClose(): Promise { 310 | try { 311 | if (this.producerVideo && !this.producerVideo.closed) { 312 | this.producerVideo.close(); 313 | } 314 | } catch (error) { 315 | console.error(error.message, error.stack); 316 | } 317 | } 318 | 319 | /** 320 | * Начать передавать свой аудио-стрим 321 | */ 322 | public async producerAudioStart(): Promise { 323 | try { 324 | if (this.mediasoupDevice.canProduce('audio')) { 325 | const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true }); 326 | const audioTrack = audioStream.getAudioTracks()[0]; 327 | 328 | if (audioTrack) { 329 | if (this.producerTransport && !this.producerTransport.closed) { 330 | this.producerAudio = await this.producerTransport.produce({ track: audioTrack }); 331 | } 332 | 333 | // 'trackended' | 'transportclose' 334 | // this.producerAudio.on('transportclose', () => {}); 335 | } 336 | 337 | this.producerAudioStream = audioStream; 338 | } 339 | } catch (error) { 340 | console.error(error.message, error.stack); 341 | } 342 | } 343 | 344 | /** 345 | * Поставить передачу своего аудио-стрима на паузу 346 | */ 347 | public async producerAudioPause(): Promise { 348 | try { 349 | if (this.producerAudio && !this.producerAudio.paused) { 350 | this.producerAudio.pause(); 351 | } 352 | } catch (error) { 353 | console.error(error.message, error.stack); 354 | } 355 | } 356 | 357 | /** 358 | * Снять с паузы передапчу своего аудио-стрима 359 | */ 360 | public async producerAudioResume(): Promise { 361 | try { 362 | if (this.producerAudio && this.producerAudio.paused && !this.producerAudio.closed) { 363 | this.producerAudio.resume(); 364 | } else if (this.producerAudio && this.producerAudio.closed) { 365 | await this.producerAudioStart(); 366 | } 367 | } catch (error) { 368 | console.error(error.message, error.stack); 369 | } 370 | } 371 | 372 | /** 373 | * Остановить передачу своего аудио-стрима (для повторной передачи требуется пересоздать продюсера) 374 | */ 375 | public async producerAudioClose(): Promise { 376 | try { 377 | if (this.producerAudio && !this.producerAudio.closed) { 378 | this.producerAudio.close(); 379 | } 380 | } catch (error) { 381 | console.error(error.message, error.stack); 382 | } 383 | } 384 | 385 | /** 386 | * Поставить на паузу стрим юзера 387 | */ 388 | public async targetProducerPause(data: { user_id: string, kind: TKind }) { 389 | try { 390 | await this.socket.request('media', { action: 'producerPause', data }); 391 | } catch (error) { 392 | console.error(error.message, error.stack); 393 | } 394 | } 395 | 396 | /** 397 | * Снять с паузы стрим юзера 398 | * @param data юзер и тип стрима 399 | */ 400 | public async targetProducerResume(data: { user_id: string, kind: TKind }) { 401 | try { 402 | await this.socket.request('media', { action: 'producerResume', data }); 403 | } catch (error) { 404 | console.error(error.message, error.stack); 405 | } 406 | } 407 | 408 | /** 409 | * Остановить стрим юзера (чтобы возобновить передачу этому пользователю придется пересоздать продюсера) 410 | * @param data юзер и тип стрима 411 | */ 412 | public async targetProducerClose(data: { user_id: string, kind: TKind }) { 413 | try { 414 | await this.socket.request('media', { action: 'producerClose', data }); 415 | } catch (error) { 416 | console.error(error.message, error.stack); 417 | } 418 | } 419 | 420 | /** 421 | * Поставить на паузу стрим всех юзеров 422 | * @param data тип стрима 423 | */ 424 | public async allProducerPause(data: { kind: TKind }) { 425 | try { 426 | await this.socket.request('media', { action: 'allProducerPause', data }); 427 | } catch (error) { 428 | console.error(error.message, error.stack); 429 | } 430 | } 431 | 432 | /** 433 | * Снять с паузы стрим всех юзеров 434 | * @param data тип стрима 435 | */ 436 | public async allProducerResume(data: { kind: TKind }) { 437 | try { 438 | await this.socket.request('media', { action: 'allProducerResume', data }); 439 | } catch (error) { 440 | console.error(error.message, error.stack); 441 | } 442 | } 443 | 444 | /** 445 | * Остановить стрим всех юзеров (чтобы возобновить передачу этим пользователям придется пересоздать продюсера) 446 | * @param data тип стрима 447 | */ 448 | public async allProducerClose(data: { kind: TKind }) { 449 | try { 450 | await this.socket.request('media', { action: 'allProducerClose', data }); 451 | } catch (error) { 452 | console.error(error.message, error.stack); 453 | } 454 | } 455 | 456 | /** 457 | * Приня видео стрим от другого пользователя 458 | * @param user_id юзер, которой передает видео-стрим 459 | */ 460 | private async consumerVideoStart(user_id: string): Promise { 461 | try { 462 | const { rtpCapabilities } = this.mediasoupDevice; 463 | 464 | const consumeData: { 465 | id: string; 466 | producerId: string; 467 | kind: TKind; 468 | rtpParameters: RTCRtpParameters; 469 | } = await this.socket.request('media', { 470 | action: 'consume', 471 | data: { rtpCapabilities, user_id, kind: 'video' }, 472 | }); 473 | 474 | const consumer = await this.consumerTransport.consume(consumeData); 475 | 476 | // 'trackended' | 'transportclose' 477 | consumer.on('transportclose', () => { 478 | this.consumersVideoStream.delete(user_id); 479 | this.consumersVideo.delete(user_id); 480 | }); 481 | 482 | this.consumersVideo.set(user_id, consumer); 483 | 484 | const stream = new MediaStream(); 485 | 486 | stream.addTrack(consumer.track); 487 | 488 | this.consumersVideoStream.set(user_id, stream); 489 | } catch (error) { 490 | console.error(error.message, error.stack); 491 | } 492 | } 493 | 494 | /** 495 | * Принять аудио стрим от другого пользователя 496 | * @param user_id юзер, который передает аудио-стрим 497 | */ 498 | private async consumerAudioStart(user_id: string): Promise { 499 | try { 500 | const { rtpCapabilities } = this.mediasoupDevice; 501 | 502 | const consumeData: { 503 | id: string; 504 | producerId: string; 505 | kind: TKind; 506 | rtpParameters: RTCRtpParameters; 507 | } = await this.socket.request('media', { 508 | action: 'consume', 509 | data: { rtpCapabilities, user_id, kind: 'audio' }, 510 | }); 511 | 512 | const consumer = await this.consumerTransport.consume(consumeData); 513 | 514 | // 'trackended' | 'transportclose' 515 | consumer.on('transportclose', async () => { 516 | this.consumersAudioStream.delete(user_id); 517 | this.consumersAudio.delete(user_id); 518 | }); 519 | 520 | this.consumersAudio.set(user_id, consumer); 521 | 522 | const stream = new MediaStream(); 523 | 524 | stream.addTrack(consumer.track); 525 | 526 | this.consumersAudioStream.set(user_id, stream); 527 | } catch (error) { 528 | console.error(error.message, error.stack); 529 | } 530 | } 531 | 532 | /** 533 | * Перезапустить подключение 534 | * @param type тип транспорта 535 | */ 536 | public async restartIce(type: TPeer): Promise { 537 | try { 538 | const iceParameters: RTCIceParameters = await this.socket.request('media', { 539 | action: 'restartIce', 540 | data: { 541 | type, 542 | }, 543 | }); 544 | 545 | switch (type) { 546 | case 'producer': 547 | await this.producerTransport.restartIce({ iceParameters }); 548 | break; 549 | case 'consumer': 550 | await this.consumerTransport.restartIce({ iceParameters }); 551 | break; 552 | } 553 | } catch (error) { 554 | console.error(error.message, error.stack); 555 | } 556 | } 557 | 558 | /** 559 | * Получить инфу о своем транспорте 560 | * @param type тип транспорта 561 | */ 562 | public async getTransportStats(type: TPeer): Promise<{ type: TPeer, stats: ITransportStat[] }> { 563 | try { 564 | return await this.socket.request('media', { 565 | action: 'getTransportStats', 566 | data: { 567 | type, 568 | }, 569 | }); 570 | } catch (error) { 571 | console.error(error.message, error.stack); 572 | } 573 | } 574 | 575 | /** 576 | * Получить инфу о стриме, который передает пользователь 577 | * @param kind тип стрима 578 | * @param user_id уникальный идентификатор юзера 579 | */ 580 | public async getProducerStats(kind: TKind, user_id: string): Promise<{ kind: TKind, user_id: string; stats: IPeerStat[] }> { 581 | try { 582 | return await this.socket.request('media', { 583 | action: 'getProducerStats', 584 | data: { 585 | kind, 586 | user_id, 587 | }, 588 | }); 589 | } catch (error) { 590 | console.error(error.message, error.stack); 591 | } 592 | } 593 | 594 | /** 595 | * Получить инфу о стриме, который принимает current_user от другого пользователя 596 | * @param kind тип стрима 597 | * @param user_id уникальный идентификатор юзера 598 | */ 599 | public async getConsumerStats(kind: TKind, user_id: string): Promise<{ kind: TKind, user_id: string; stats: IPeerStat[] }> { 600 | try { 601 | return await this.socket.request('media', { 602 | action: 'getConsumerStats', 603 | data: { 604 | kind, 605 | user_id, 606 | }, 607 | }); 608 | } catch (error) { 609 | console.error(error.message, error.stack); 610 | } 611 | } 612 | 613 | /** 614 | * Получить опорный кадр у пользователя, стрим которого принимается. 615 | * Только для видео 616 | * @param user_id уникальный идентификатор юзера 617 | */ 618 | public async requestConsumerKeyFrame(user_id: string): Promise { 619 | try { 620 | return await this.socket.request('media', { 621 | action: 'requestConsumerKeyFrame', 622 | data: { 623 | user_id, 624 | }, 625 | }); 626 | } catch (error) { 627 | console.error(error.message, error.stack); 628 | } 629 | } 630 | } 631 | --------------------------------------------------------------------------------