@ngx-telly/plugin-wowza-webrtc
Version:
Telly - HLS plugin
305 lines (299 loc) • 12.2 kB
JavaScript
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