UNPKG

@ngx-telly/plugin-wowza-webrtc

Version:
305 lines (299 loc) 12.2 kB
import * as i0 from '@angular/core'; import { EventEmitter, inject, Output, Input, Directive } from '@angular/core'; import { PlayerComponent } from '@ngx-telly/player'; /* * This code and all components (c) Copyright 2019-2020, Wowza Media Systems, LLC. All rights reserved. * This code is licensed pursuant to the BSD 3-Clause License. */ function mungeSDPPlay(sdpStr) { // For greatest playback compatibility, // force H.264 playback to constrained baseline (42e01f). const sdpLines = sdpStr.split(/\r\n/); let sdpStrRet = ''; for (const sdpIndex in sdpLines) { let sdpLine = sdpLines[sdpIndex]; if (sdpLine.length == 0) continue; if (sdpLine.includes('profile-level-id')) { // The profile-level-id string has three parts: XXYYZZ, where // XX: 42 baseline, 4D main, 64 high // YY: constraint // ZZ: level ID // Look for codecs higher than baseline and force downward. const profileLevelId = sdpLine.substr(sdpLine.indexOf('profile-level-id') + 17, 6); let profile = Number('0x' + profileLevelId.substr(0, 2)); let constraint = Number('0x' + profileLevelId.substr(2, 2)); let level = Number('0x' + profileLevelId.substr(4, 2)); if (profile > 0x42) { profile = 0x42; constraint = 0xe0; level = 0x1f; } if (constraint == 0x00) { constraint = 0xe0; } const newProfileLevelId = ('00' + profile.toString(16)).slice(-2).toLowerCase() + ('00' + constraint.toString(16)).slice(-2).toLowerCase() + ('00' + level.toString(16)).slice(-2).toLowerCase(); sdpLine = sdpLine.replace(profileLevelId, newProfileLevelId); } sdpStrRet += sdpLine; sdpStrRet += '\r\n'; } return sdpStrRet; } class WowzaConnector { constructor(config) { this.onIceCandidateRcv = (ev) => { console.log('[ ICE ]', ev.candidate); }; this.onDescriptionRcv = (desc) => { this.peerConnection ?.setLocalDescription(desc) .then(() => { this.wsSend('sendResponse', { sdp: desc }); }) .catch((err) => { console.log('[WEB RTC] Setting local RTCSessionDescriptionInit failed', err); }); }; this.onTrackRcv = (ev) => { try { this.config.videoEl.srcObject = ev.streams[0]; this.config.videoEl.onerror = this.onVideoError; } catch { console.log('[WEB RTC] Cannot bind srcObject to video element. Sources: ', ev.streams); } }; this.onVideoError = (ev) => { if (typeof ev === 'string') return; const videoEl = ev.target; const error = videoEl.error; if (error) { const errorMsg = `[VIDEO PLAYBACK ERROR] Code: ${error.code}, Message: ${error.message}`; console.error(errorMsg, error); this.config?.errorCallback?.(ev); } }; this.onMsgRcv = (ev) => { const msg = JSON.parse(ev.data); const status = +msg['status']; const command = msg['command']; switch (status) { case 200: { console.log('[WOWZA] Success response'); if (msg['streamInfo']) { const streamInfo = msg['streamInfo']; this.config.streamInfo.sessionId = streamInfo.sessionId; } if (msg['sdp']) { msg.sdp.sdp = mungeSDPPlay(msg.sdp.sdp); this.peerConnection?.setRemoteDescription(new RTCSessionDescription(msg.sdp)).then(() => { this.peerConnection?.createAnswer().then((d) => this.onDescriptionRcv(d)); }); } if (msg['iceCandidates'] && msg['iceCandidates'].length) { msg['iceCandidates'].forEach((x) => { this.peerConnection?.addIceCandidate(new RTCIceCandidate(x)); }); } if ('sendResponse'.localeCompare(command) == 0) { this.wsConnection?.close(); this.wsConnection = undefined; } if ('getAvailableStreams'.localeCompare(command) == 0) { this.stop(); } break; } case 403: { const errorMsg = '[WOWZA ERROR] 403 Forbidden - Access denied or authentication failed'; console.error(errorMsg, msg); this.config?.errorCallback?.(ev); this.stop(); break; } case 404: { const errorMsg = '[WOWZA ERROR] 404 Not Found - Stream or resource not found'; console.error(errorMsg, msg); this.config?.errorCallback?.(ev); this.stop(); break; } case 507: { const errorMsg = '[WOWZA ERROR] 507 WebRTC session not found'; console.error(errorMsg, msg); this.config?.errorCallback?.(ev); this.stop(); break; } case 514: { console.warn('[WOWZA] 514 Stream not ready, retrying...'); let retries = 0; if (retries < 10) { setTimeout(() => { this.wsSend('getOffer'); retries++; }, 500); } else { const errorMsg = '[WOWZA ERROR] 514 Max retries exceeded - Stream unavailable'; console.error(errorMsg); this.config?.errorCallback?.(ev); this.stop(); } break; } default: { const errorMsg = `[WOWZA ERROR] Unexpected status code: ${status}`; console.error(errorMsg, msg); this.config?.errorCallback?.(ev); this.stop(); break; } } }; this.onStateChanged = (ev) => { const state = this.peerConnection?.connectionState; console.log('[WEB RTC] Connection state changed to:', state); if (state === 'failed' || state === 'disconnected') { console.error('[WEB RTC] Connection failed/disconnected. ICE state:', this.peerConnection?.iceConnectionState); this.config?.errorCallback?.(ev); } this.config?.stateCallback?.(ev); }; this.onError = (ev) => { this.config?.errorCallback?.(ev); }; this.onClose = (ev) => { this.config?.closeCallback?.(ev); }; this.onWsConnected = () => { this.peerConnection = new RTCPeerConnection(); this.peerConnection.onicecandidate = this.onIceCandidateRcv; this.peerConnection.ontrack = this.onTrackRcv; this.peerConnection.onconnectionstatechange = this.onStateChanged; this.wsSend('getOffer'); }; this.config = config; } start() { if (!this.peerConnection) { this.wsConnect(this.config.sdpUrl); } } stop() { if (this.peerConnection) { this.peerConnection.close(); this.peerConnection = undefined; } if (this.wsConnection) { this.wsConnection.close(); this.wsConnection = undefined; } if (this.config.videoEl) { this.config.videoEl.src = ''; } } wsConnect(url) { this.wsConnection = new WebSocket(url); this.wsConnection.binaryType = 'arraybuffer'; this.wsConnection.onopen = this.onWsConnected; this.wsConnection.onmessage = this.onMsgRcv; this.wsConnection.onclose = this.onClose; this.wsConnection.onerror = this.onError; } wsSend(command, ext = {}) { const cmd = { direction: 'play', command: command, streamInfo: this.config.streamInfo, userData: this.config.userData, ...ext, }; if (this.config.secureToken != null) { cmd.secureToken = this.config.secureToken; } this.wsConnection?.send(JSON.stringify(cmd)); } } class WowzaDirective { constructor() { this.sessionId = '[empty]'; this.wowzaStateCallback = new EventEmitter(); this.wowzaCloseCallback = new EventEmitter(); this.wowzaErrorCallback = new EventEmitter(); this.player = inject(PlayerComponent); } ngOnChanges(changes) { if (changes['tellyWowza']?.currentValue || changes['streamName']?.currentValue) { this.create(); } } create() { if (!this.tellyWowza || !this.streamName || !this.appName) { return; } if (this.wowza) { this.destroy(); } const config = { sdpUrl: this.tellyWowza, videoEl: this.player.video.nativeElement, streamInfo: { applicationName: this.appName, streamName: this.streamName, sessionId: this.sessionId, }, stateCallback: (e) => this.wowzaStateCallback.emit(e), closeCallback: (e) => this.wowzaCloseCallback.emit(e), errorCallback: (e) => this.wowzaErrorCallback.emit(e), }; if (this.token != null) { config.secureToken = this.token; } this.wowza = new WowzaConnector(config); this.wowza.start(); } destroy() { this.player.video.nativeElement.srcObject = null; this.wowza?.stop(); this.wowza = undefined; } ngOnDestroy() { this.destroy(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: WowzaDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.9", type: WowzaDirective, isStandalone: true, selector: "[tellyWowza]", inputs: { tellyWowza: "tellyWowza", streamName: "streamName", appName: "appName", sessionId: "sessionId", token: "token" }, outputs: { wowzaStateCallback: "wowzaStateCallback", wowzaCloseCallback: "wowzaCloseCallback", wowzaErrorCallback: "wowzaErrorCallback" }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: WowzaDirective, decorators: [{ type: Directive, args: [{ selector: '[tellyWowza]', standalone: true, }] }], propDecorators: { tellyWowza: [{ type: Input, args: [{ required: true }] }], streamName: [{ type: Input }], appName: [{ type: Input }], sessionId: [{ type: Input }], token: [{ type: Input }], wowzaStateCallback: [{ type: Output }], wowzaCloseCallback: [{ type: Output }], wowzaErrorCallback: [{ type: Output }] } }); /** * Generated bundle index. Do not edit. */ export { WowzaConnector, WowzaDirective }; //# sourceMappingURL=ngx-telly-plugin-wowza-webrtc.mjs.map