1800 lines
62 KiB
JavaScript
1800 lines
62 KiB
JavaScript
(function (factory) {
|
||
typeof define === 'function' && define.amd ? define(factory) :
|
||
factory();
|
||
})((function () { 'use strict';
|
||
|
||
class Emitter {
|
||
on(name, fn, ctx) {
|
||
const e = this.e || (this.e = {});
|
||
(e[name] || (e[name] = [])).push({
|
||
fn,
|
||
ctx
|
||
});
|
||
return this;
|
||
}
|
||
once(name, fn, ctx) {
|
||
const self = this;
|
||
function listener() {
|
||
self.off(name, listener);
|
||
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
||
args[_key] = arguments[_key];
|
||
}
|
||
fn.apply(ctx, args);
|
||
}
|
||
listener._ = fn;
|
||
return this.on(name, listener, ctx);
|
||
}
|
||
emit(name) {
|
||
const evtArr = ((this.e || (this.e = {}))[name] || []).slice();
|
||
for (var _len2 = arguments.length, data = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
||
data[_key2 - 1] = arguments[_key2];
|
||
}
|
||
for (let i = 0; i < evtArr.length; i += 1) {
|
||
evtArr[i].fn.apply(evtArr[i].ctx, data);
|
||
}
|
||
return this;
|
||
}
|
||
off(name, callback) {
|
||
const e = this.e || (this.e = {});
|
||
if (!name) {
|
||
Object.keys(e).forEach(key => {
|
||
delete e[key];
|
||
});
|
||
delete this.e;
|
||
return;
|
||
}
|
||
const evts = e[name];
|
||
const liveEvents = [];
|
||
if (evts && callback) {
|
||
for (let i = 0, len = evts.length; i < len; i += 1) {
|
||
if (evts[i].fn !== callback && evts[i].fn._ !== callback) liveEvents.push(evts[i]);
|
||
}
|
||
}
|
||
if (liveEvents.length) {
|
||
e[name] = liveEvents;
|
||
} else {
|
||
delete e[name];
|
||
}
|
||
return this;
|
||
}
|
||
}
|
||
|
||
// 播放协议
|
||
const DEBUG_LEVEL = {
|
||
debug: 'debug',
|
||
warn: 'warn'
|
||
};
|
||
|
||
const URL_OBJECT_CLEAR_TIME = 10 * 1000; // url object clear time
|
||
|
||
// inner events
|
||
const EVENTS = {
|
||
fullscreen: 'fullscreen$2',
|
||
webFullscreen: 'webFullscreen',
|
||
decoderWorkerInit: 'decoderWorkerInit',
|
||
play: 'play',
|
||
playing: 'playing',
|
||
pause: 'pause',
|
||
mute: 'mute',
|
||
load: 'load',
|
||
loading: 'loading',
|
||
zooming: 'zooming',
|
||
videoInfo: 'videoInfo',
|
||
timeUpdate: 'timeUpdate',
|
||
audioInfo: "audioInfo",
|
||
log: 'log',
|
||
error: "error",
|
||
kBps: 'kBps',
|
||
timeout: 'timeout',
|
||
delayTimeout: 'delayTimeout',
|
||
delayTimeoutRetryEnd: 'delayTimeoutRetryEnd',
|
||
loadingTimeout: 'loadingTimeout',
|
||
loadingTimeoutRetryEnd: 'loadingTimeoutRetryEnd',
|
||
stats: 'stats',
|
||
performance: "performance",
|
||
videoSmooth: 'videoSmooth',
|
||
faceDetectActive: 'faceDetectActive',
|
||
objectDetectActive: 'objectDetectActive',
|
||
occlusionDetectActive: 'occlusionDetectActive',
|
||
imageDetectActive: 'imageDetectActive',
|
||
// record
|
||
record: 'record',
|
||
recording: 'recording',
|
||
recordingTimestamp: 'recordingTimestamp',
|
||
recordStart: 'recordStart',
|
||
recordEnd: 'recordEnd',
|
||
recordCreateError: 'recordCreateError',
|
||
recordBlob: 'recordBlob',
|
||
buffer: 'buffer',
|
||
videoFrame: 'videoFrame',
|
||
videoSEI: 'videoSEI',
|
||
start: 'start',
|
||
metadata: 'metadata',
|
||
resize: 'resize',
|
||
volumechange: 'volumechange',
|
||
destroy: 'destroy',
|
||
beforeDestroy: 'beforeDestroy',
|
||
// stream
|
||
streamEnd: 'streamEnd',
|
||
streamRate: 'streamRate',
|
||
streamAbps: 'streamAbps',
|
||
streamVbps: 'streamVbps',
|
||
streamDts: 'streamDts',
|
||
streamSuccess: 'streamSuccess',
|
||
streamMessage: 'streamMessage',
|
||
streamError: 'streamError',
|
||
streamStats: 'streamStats',
|
||
// MSE
|
||
mseSourceOpen: 'mseSourceOpen',
|
||
mseSourceClose: 'mseSourceClose',
|
||
mseSourceended: 'mseSourceended',
|
||
mseSourceStartStreaming: 'mseSourceStartStreaming',
|
||
mseSourceEndStreaming: 'mseSourceEndStreaming',
|
||
mseSourceBufferError: 'mseSourceBufferError',
|
||
mseAddSourceBufferError: 'mseAddSourceBufferError',
|
||
mseSourceBufferBusy: 'mseSourceBufferBusy',
|
||
mseSourceBufferFull: 'mseSourceBufferFull',
|
||
// VIDEO
|
||
videoWaiting: 'videoWaiting',
|
||
videoTimeUpdate: 'videoTimeUpdate',
|
||
videoSyncAudio: 'videoSyncAudio',
|
||
//
|
||
playToRenderTimes: 'playToRenderTimes',
|
||
playbackTime: 'playbackTime',
|
||
playbackTimestamp: 'playbackTimestamp',
|
||
playbackTimeScroll: 'playbackTimeScroll',
|
||
playbackPrecision: 'playbackPrecision',
|
||
// inner
|
||
playbackShowPrecisionChange: 'playbackShowPrecisionChange',
|
||
playbackJustTime: 'playbackJustTime',
|
||
playbackStats: 'playbackStats',
|
||
playbackSeek: 'playbackSeek',
|
||
playbackPause: 'playbackPause',
|
||
playbackPauseOrResume: 'playbackPauseOrResume',
|
||
playbackRateChange: 'playbackRateChange',
|
||
playbackPreRateChange: 'playbackPreRateChange',
|
||
ptz: 'ptz',
|
||
streamQualityChange: 'streamQualityChange',
|
||
visibilityChange: "visibilityChange",
|
||
netBuf: 'netBuf',
|
||
close: 'close',
|
||
networkDelayTimeout: 'networkDelayTimeout',
|
||
togglePerformancePanel: 'togglePerformancePanel',
|
||
viewResizeChange: 'viewResizeChange',
|
||
flvDemuxBufferSizeTooLarge: 'flvDemuxBufferSizeTooLarge',
|
||
// talk
|
||
talkGetUserMediaSuccess: 'talkGetUserMediaSuccess',
|
||
talkGetUserMediaFail: 'talkGetUserMediaFail',
|
||
talkGetUserMediaTimeout: 'talkGetUserMediaTimeout',
|
||
talkStreamStart: 'talkStreamStart',
|
||
talkStreamOpen: 'talkStreamOpen',
|
||
talkStreamClose: 'talkStreamClose',
|
||
talkStreamError: 'talkStreamError',
|
||
talkStreamInactive: 'talkStreamInactive',
|
||
webrtcDisconnect: 'webrtcDisconnect',
|
||
webrtcFailed: 'webrtcFailed',
|
||
webrtcClosed: 'webrtcClosed',
|
||
webrtcOnConnectionStateChange: 'webrtcOnConnectionStateChange',
|
||
webrtcOnIceConnectionStateChange: 'webrtcOnIceConnectionStateChange',
|
||
// crash
|
||
crashLog: 'crashLog',
|
||
// dom
|
||
focus: 'focus',
|
||
blur: 'blur',
|
||
visibilityHiddenTimeout: 'visibilityHiddenTimeout',
|
||
// websocket
|
||
websocketOpen: 'websocketOpen',
|
||
websocketClose: 'websocketClose',
|
||
websocketError: 'websocketError',
|
||
websocketMessage: 'websocketMessage',
|
||
// ai
|
||
aiObjectDetectorInfo: 'aiObjectDetectorInfo',
|
||
aiFaceDetectorInfo: 'aiFaceDetectorInfo',
|
||
aiOcclusionDetectResult: 'aiOcclusionDetectResult',
|
||
aiImageDetectResult: 'aiImageDetectResult',
|
||
// 异常暂停
|
||
playFailedAndPaused: 'playFailedAndPaused',
|
||
// audio
|
||
audioResumeState: 'audioResumeState',
|
||
// webrtc
|
||
webrtcStreamH265: 'webrtcStreamH265',
|
||
// flv
|
||
flvMetaData: 'flvMetaData',
|
||
// talk
|
||
talkFailedAndStop: 'talkFailedAndStop',
|
||
removeLoadingBgImage: 'removeLoadingBgImage',
|
||
memoryLog: 'memoryLog',
|
||
downloadMemoryLog: 'downloadMemoryLog',
|
||
pressureObserverCpu: 'pressureObserverCpu',
|
||
currentPts: 'currentPts'
|
||
};
|
||
const TALK_EVENTS = {
|
||
talkStreamClose: EVENTS.talkStreamClose,
|
||
talkStreamError: EVENTS.talkStreamError,
|
||
talkStreamInactive: EVENTS.talkStreamInactive,
|
||
talkGetUserMediaTimeout: EVENTS.talkGetUserMediaTimeout,
|
||
talkFailedAndStop: EVENTS.talkFailedAndStop
|
||
};
|
||
const TALK_EVENTS_ERROR = {
|
||
talkStreamError: EVENTS.talkStreamError,
|
||
talkStreamClose: EVENTS.talkStreamClose
|
||
};
|
||
const EVENTS_ERROR = {
|
||
playError: 'playIsNotPauseOrUrlIsNull',
|
||
fetchError: "fetchError",
|
||
websocketError: 'websocketError',
|
||
webcodecsH265NotSupport: 'webcodecsH265NotSupport',
|
||
webcodecsDecodeError: 'webcodecsDecodeError',
|
||
webcodecsUnsupportedConfigurationError: 'webcodecsUnsupportedConfigurationError',
|
||
webcodecsDecodeConfigureError: 'webcodecsDecodeConfigureError',
|
||
mediaSourceH265NotSupport: 'mediaSourceH265NotSupport',
|
||
mediaSourceAudioG711NotSupport: 'mediaSourceAudioG711NotSupport',
|
||
mediaSourceAudioInitTimeout: 'mediaSourceAudioInitTimeout',
|
||
mediaSourceAudioNoDataTimeout: 'mediaSourceAudioNoDataTimeout',
|
||
mediaSourceDecoderConfigurationError: 'mediaSourceDecoderConfigurationError',
|
||
mediaSourceFull: EVENTS.mseSourceBufferFull,
|
||
mseSourceBufferError: EVENTS.mseSourceBufferError,
|
||
mseAddSourceBufferError: EVENTS.mseAddSourceBufferError,
|
||
mediaSourceAppendBufferError: 'mediaSourceAppendBufferError',
|
||
mediaSourceTsIsMaxDiff: 'mediaSourceTsIsMaxDiff',
|
||
mediaSourceUseCanvasRenderPlayFailed: 'mediaSourceUseCanvasRenderPlayFailed',
|
||
mediaSourceBufferedIsZeroError: 'mediaSourceBufferedIsZeroError',
|
||
wasmDecodeError: 'wasmDecodeError',
|
||
wasmUseVideoRenderError: 'wasmUseVideoRenderError',
|
||
hlsError: 'hlsError',
|
||
webrtcError: 'webrtcError',
|
||
webrtcClosed: EVENTS.webrtcClosed,
|
||
webrtcIceCandidateError: 'webrtcIceCandidateError',
|
||
webglAlignmentError: 'webglAlignmentError',
|
||
wasmWidthOrHeightChange: 'wasmWidthOrHeightChange',
|
||
mseWidthOrHeightChange: 'mseWidthOrHeightChange',
|
||
wcsWidthOrHeightChange: 'wcsWidthOrHeightChange',
|
||
widthOrHeightChange: 'widthOrHeightChange',
|
||
tallWebsocketClosedByError: 'tallWebsocketClosedByError',
|
||
flvDemuxBufferSizeTooLarge: EVENTS.flvDemuxBufferSizeTooLarge,
|
||
wasmDecodeVideoNoResponseError: 'wasmDecodeVideoNoResponseError',
|
||
audioChannelError: 'audioChannelError',
|
||
simdH264DecodeVideoWidthIsTooLarge: 'simdH264DecodeVideoWidthIsTooLarge',
|
||
simdDecodeError: 'simdDecodeError',
|
||
webglContextLostError: 'webglContextLostError',
|
||
videoElementPlayingFailed: 'videoElementPlayingFailed',
|
||
videoElementPlayingFailedForWebrtc: 'videoElementPlayingFailedForWebrtc',
|
||
decoderWorkerInitError: 'decoderWorkerInitError',
|
||
videoInfoError: 'videoInfoError',
|
||
videoCodecIdError: 'videoCodecIdError',
|
||
streamEnd: EVENTS.streamEnd,
|
||
delayTimeout: EVENTS.delayTimeout,
|
||
loadingTimeout: EVENTS.loadingTimeout,
|
||
networkDelayTimeout: EVENTS.networkDelayTimeout,
|
||
aliyunRtcError: 'aliyunRtcError',
|
||
...TALK_EVENTS_ERROR
|
||
};
|
||
const WEBSOCKET_STATUS = {
|
||
notConnect: 'notConnect',
|
||
open: 'open',
|
||
close: 'close',
|
||
error: 'error'
|
||
};
|
||
const TALK_ENC_TYPE = {
|
||
g711a: 'g711a',
|
||
g711u: 'g711u',
|
||
pcm: 'pcm',
|
||
opus: 'opus'
|
||
};
|
||
|
||
// RTP_PAYLOAD_TYPE_PCMU = 0, // g711u
|
||
// RTP_PAYLOAD_TYPE_PCMA = 8, // g711a
|
||
// RTP_PAYLOAD_TYPE_JPEG = 26,
|
||
// RTP_PAYLOAD_TYPE_H264 = 96,
|
||
// RTP_PAYLOAD_TYPE_H265 = 97,
|
||
// RTP_PAYLOAD_TYPE_OPUS = 98,
|
||
// RTP_PAYLOAD_TYPE_AAC = 99,
|
||
// RTP_PAYLOAD_TYPE_G726 = 100,
|
||
// RTP_PAYLOAD_TYPE_G726_16 = 101,
|
||
// RTP_PAYLOAD_TYPE_G726_24 = 102,
|
||
// RTP_PAYLOAD_TYPE_G726_32 = 103,
|
||
// RTP_PAYLOAD_TYPE_G726_40 = 104,
|
||
// RTP_PAYLOAD_TYPE_SPEEX = 105,
|
||
const RTP_PAYLOAD_TYPE = {
|
||
pcma: 8,
|
||
g711a: 8,
|
||
pcmu: 0,
|
||
g711u: 0,
|
||
jpeg: 26,
|
||
h264: 96,
|
||
h265: 97,
|
||
opus: 98,
|
||
aac: 99
|
||
};
|
||
const TALK_PACKET_TYPE = {
|
||
empty: 'empty',
|
||
// 裸的协议
|
||
rtp: 'rtp'
|
||
};
|
||
const TALK_PACKAGE_TCP_SEND_TYPE = {
|
||
tcp: 'tcp',
|
||
udp: 'udp'
|
||
};
|
||
const WEBSOCKET_EVENTS = {
|
||
open: 'open',
|
||
close: 'close',
|
||
error: 'error',
|
||
message: 'message'
|
||
};
|
||
const TALK_ENGINE = {
|
||
worklet: 'worklet',
|
||
script: 'script'
|
||
};
|
||
|
||
// default talk options
|
||
const DEFAULT_TALK_OPTIONS = {
|
||
encType: TALK_ENC_TYPE.g711a,
|
||
packetType: TALK_PACKET_TYPE.rtp,
|
||
// 默认的包个格式化
|
||
packetTcpSendType: TALK_PACKAGE_TCP_SEND_TYPE.tcp,
|
||
// 默认的包个格式化
|
||
rtpSsrc: '0000000000',
|
||
// 10 位
|
||
numberChannels: 1,
|
||
// 采样通道
|
||
sampleRate: 8000,
|
||
// 采样率
|
||
sampleBitsWidth: 16,
|
||
// 采样精度
|
||
sendInterval: 20,
|
||
// 发送间隔(ms)
|
||
debug: false,
|
||
debugLevel: DEBUG_LEVEL.warn,
|
||
// debug level
|
||
testMicrophone: false,
|
||
// 测试麦克风获取
|
||
saveRtpToFile: false,
|
||
// 保存 rtp 到文件
|
||
audioBufferLength: 160,
|
||
// 默认走的是 20ms 8000 采样率 16 位精度
|
||
engine: TALK_ENGINE.worklet,
|
||
//
|
||
checkGetUserMediaTimeout: false,
|
||
// 检测 getUserMedia 超时
|
||
getUserMediaTimeout: 10 * 1000,
|
||
// getUserMedia 超时时间 10s
|
||
audioConstraints: {
|
||
// deviceId: '', // 设备 id
|
||
latency: true,
|
||
//
|
||
noiseSuppression: true,
|
||
// 降噪
|
||
autoGainControl: true,
|
||
echoCancellation: true,
|
||
// 回声消除
|
||
sampleRate: 48000,
|
||
channelCount: 1
|
||
}
|
||
};
|
||
|
||
function createCommonjsModule(fn, module) {
|
||
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
||
}
|
||
|
||
var screenfull = createCommonjsModule(function (module) {
|
||
/*!
|
||
* screenfull
|
||
* v5.1.0 - 2020-12-24
|
||
* (c) Sindre Sorhus; MIT License
|
||
*/
|
||
(function () {
|
||
|
||
var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {};
|
||
var isCommonjs = module.exports;
|
||
|
||
var fn = (function () {
|
||
var val;
|
||
|
||
var fnMap = [
|
||
[
|
||
'requestFullscreen',
|
||
'exitFullscreen',
|
||
'fullscreenElement',
|
||
'fullscreenEnabled',
|
||
'fullscreenchange',
|
||
'fullscreenerror'
|
||
],
|
||
// New WebKit
|
||
[
|
||
'webkitRequestFullscreen',
|
||
'webkitExitFullscreen',
|
||
'webkitFullscreenElement',
|
||
'webkitFullscreenEnabled',
|
||
'webkitfullscreenchange',
|
||
'webkitfullscreenerror'
|
||
|
||
],
|
||
// Old WebKit
|
||
[
|
||
'webkitRequestFullScreen',
|
||
'webkitCancelFullScreen',
|
||
'webkitCurrentFullScreenElement',
|
||
'webkitCancelFullScreen',
|
||
'webkitfullscreenchange',
|
||
'webkitfullscreenerror'
|
||
|
||
],
|
||
[
|
||
'mozRequestFullScreen',
|
||
'mozCancelFullScreen',
|
||
'mozFullScreenElement',
|
||
'mozFullScreenEnabled',
|
||
'mozfullscreenchange',
|
||
'mozfullscreenerror'
|
||
],
|
||
[
|
||
'msRequestFullscreen',
|
||
'msExitFullscreen',
|
||
'msFullscreenElement',
|
||
'msFullscreenEnabled',
|
||
'MSFullscreenChange',
|
||
'MSFullscreenError'
|
||
]
|
||
];
|
||
|
||
var i = 0;
|
||
var l = fnMap.length;
|
||
var ret = {};
|
||
|
||
for (; i < l; i++) {
|
||
val = fnMap[i];
|
||
if (val && val[1] in document) {
|
||
for (i = 0; i < val.length; i++) {
|
||
ret[fnMap[0][i]] = val[i];
|
||
}
|
||
return ret;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
})();
|
||
|
||
var eventNameMap = {
|
||
change: fn.fullscreenchange,
|
||
error: fn.fullscreenerror
|
||
};
|
||
|
||
var screenfull = {
|
||
request: function (element, options) {
|
||
return new Promise(function (resolve, reject) {
|
||
var onFullScreenEntered = function () {
|
||
this.off('change', onFullScreenEntered);
|
||
resolve();
|
||
}.bind(this);
|
||
|
||
this.on('change', onFullScreenEntered);
|
||
|
||
element = element || document.documentElement;
|
||
|
||
var returnPromise = element[fn.requestFullscreen](options);
|
||
|
||
if (returnPromise instanceof Promise) {
|
||
returnPromise.then(onFullScreenEntered).catch(reject);
|
||
}
|
||
}.bind(this));
|
||
},
|
||
exit: function () {
|
||
return new Promise(function (resolve, reject) {
|
||
if (!this.isFullscreen) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
var onFullScreenExit = function () {
|
||
this.off('change', onFullScreenExit);
|
||
resolve();
|
||
}.bind(this);
|
||
|
||
this.on('change', onFullScreenExit);
|
||
|
||
var returnPromise = document[fn.exitFullscreen]();
|
||
|
||
if (returnPromise instanceof Promise) {
|
||
returnPromise.then(onFullScreenExit).catch(reject);
|
||
}
|
||
}.bind(this));
|
||
},
|
||
toggle: function (element, options) {
|
||
return this.isFullscreen ? this.exit() : this.request(element, options);
|
||
},
|
||
onchange: function (callback) {
|
||
this.on('change', callback);
|
||
},
|
||
onerror: function (callback) {
|
||
this.on('error', callback);
|
||
},
|
||
on: function (event, callback) {
|
||
var eventName = eventNameMap[event];
|
||
if (eventName) {
|
||
document.addEventListener(eventName, callback, false);
|
||
}
|
||
},
|
||
off: function (event, callback) {
|
||
var eventName = eventNameMap[event];
|
||
if (eventName) {
|
||
document.removeEventListener(eventName, callback, false);
|
||
}
|
||
},
|
||
raw: fn
|
||
};
|
||
|
||
if (!fn) {
|
||
if (isCommonjs) {
|
||
module.exports = {isEnabled: false};
|
||
} else {
|
||
window.screenfull = {isEnabled: false};
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
Object.defineProperties(screenfull, {
|
||
isFullscreen: {
|
||
get: function () {
|
||
return Boolean(document[fn.fullscreenElement]);
|
||
}
|
||
},
|
||
element: {
|
||
enumerable: true,
|
||
get: function () {
|
||
return document[fn.fullscreenElement];
|
||
}
|
||
},
|
||
isEnabled: {
|
||
enumerable: true,
|
||
get: function () {
|
||
// Coerce to boolean in case of old WebKit
|
||
return Boolean(document[fn.fullscreenEnabled]);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (isCommonjs) {
|
||
module.exports = screenfull;
|
||
} else {
|
||
window.screenfull = screenfull;
|
||
}
|
||
})();
|
||
});
|
||
screenfull.isEnabled;
|
||
|
||
function now() {
|
||
return new Date().getTime();
|
||
}
|
||
(() => {
|
||
try {
|
||
if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") {
|
||
const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
|
||
if (module instanceof WebAssembly.Module) return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
|
||
}
|
||
} catch (e) {}
|
||
return false;
|
||
})();
|
||
function clamp(num, a, b) {
|
||
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
|
||
}
|
||
function isFunction(fn) {
|
||
return typeof fn === "function";
|
||
}
|
||
function isSupportGetUserMedia() {
|
||
let result = false;
|
||
const navigator = window.navigator;
|
||
if (navigator) {
|
||
result = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
|
||
if (!result) {
|
||
result = !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
function createWorkletModuleUrl(func) {
|
||
function functionToString(str) {
|
||
return str.trim().match(/^function\s*\w*\s*\([\w\s,]*\)\s*{([\w\W]*?)}$/)[1];
|
||
}
|
||
const funcStr = functionToString(func.toString());
|
||
const blob = new Blob([funcStr], {
|
||
type: 'application/javascript'
|
||
});
|
||
return URL.createObjectURL(blob);
|
||
}
|
||
function clone(obj) {
|
||
let result = '';
|
||
//
|
||
if (typeof obj === 'object') {
|
||
try {
|
||
result = JSON.stringify(obj);
|
||
result = JSON.parse(result);
|
||
} catch (e) {
|
||
result = obj;
|
||
}
|
||
} else {
|
||
result = obj;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @returns {object:DEFAULT_TALK_OPTIONS}
|
||
*/
|
||
function getDefaultTalkOptions() {
|
||
return clone(DEFAULT_TALK_OPTIONS);
|
||
}
|
||
function isTrue(value) {
|
||
return value === true || value === 'true';
|
||
}
|
||
|
||
class Resampler {
|
||
constructor(options) {
|
||
const {
|
||
fromSampleRate,
|
||
toSampleRate,
|
||
channels,
|
||
inputBufferSize
|
||
} = options;
|
||
if (!fromSampleRate || !toSampleRate || !channels) {
|
||
throw new Error("Invalid settings specified for the resampler.");
|
||
}
|
||
this.resampler = null;
|
||
this.fromSampleRate = fromSampleRate;
|
||
this.toSampleRate = toSampleRate;
|
||
this.channels = channels || 0;
|
||
this.inputBufferSize = inputBufferSize;
|
||
this.initialize();
|
||
}
|
||
initialize() {
|
||
if (this.fromSampleRate == this.toSampleRate) {
|
||
// Setup resampler bypass - Resampler just returns what was passed through
|
||
this.resampler = buffer => {
|
||
return buffer;
|
||
};
|
||
this.ratioWeight = 1;
|
||
} else {
|
||
if (this.fromSampleRate < this.toSampleRate) {
|
||
// Use generic linear interpolation if upsampling,
|
||
// as linear interpolation produces a gradient that we want
|
||
// and works fine with two input sample points per output in this case.
|
||
this.linearInterpolation();
|
||
this.lastWeight = 1;
|
||
} else {
|
||
// Custom resampler I wrote that doesn't skip samples
|
||
// like standard linear interpolation in high downsampling.
|
||
// This is more accurate than linear interpolation on downsampling.
|
||
this.multiTap();
|
||
this.tailExists = false;
|
||
this.lastWeight = 0;
|
||
}
|
||
|
||
// Initialize the internal buffer:
|
||
this.initializeBuffers();
|
||
this.ratioWeight = this.fromSampleRate / this.toSampleRate;
|
||
}
|
||
}
|
||
bufferSlice(sliceAmount) {
|
||
//Typed array and normal array buffer section referencing:
|
||
try {
|
||
return this.outputBuffer.subarray(0, sliceAmount);
|
||
} catch (error) {
|
||
try {
|
||
//Regular array pass:
|
||
this.outputBuffer.length = sliceAmount;
|
||
return this.outputBuffer;
|
||
} catch (error) {
|
||
//Nightly Firefox 4 used to have the subarray function named as slice:
|
||
return this.outputBuffer.slice(0, sliceAmount);
|
||
}
|
||
}
|
||
}
|
||
initializeBuffers() {
|
||
this.outputBufferSize = Math.ceil(this.inputBufferSize * this.toSampleRate / this.fromSampleRate / this.channels * 1.000000476837158203125) + this.channels + this.channels;
|
||
try {
|
||
this.outputBuffer = new Float32Array(this.outputBufferSize);
|
||
this.lastOutput = new Float32Array(this.channels);
|
||
} catch (error) {
|
||
this.outputBuffer = [];
|
||
this.lastOutput = [];
|
||
}
|
||
}
|
||
linearInterpolation() {
|
||
this.resampler = buffer => {
|
||
let bufferLength = buffer.length,
|
||
channels = this.channels,
|
||
outLength,
|
||
ratioWeight,
|
||
weight,
|
||
firstWeight,
|
||
secondWeight,
|
||
sourceOffset,
|
||
outputOffset,
|
||
outputBuffer,
|
||
channel;
|
||
if (bufferLength % channels !== 0) {
|
||
throw new Error("Buffer was of incorrect sample length.");
|
||
}
|
||
if (bufferLength <= 0) {
|
||
return [];
|
||
}
|
||
outLength = this.outputBufferSize;
|
||
ratioWeight = this.ratioWeight;
|
||
weight = this.lastWeight;
|
||
firstWeight = 0;
|
||
secondWeight = 0;
|
||
sourceOffset = 0;
|
||
outputOffset = 0;
|
||
outputBuffer = this.outputBuffer;
|
||
for (; weight < 1; weight += ratioWeight) {
|
||
secondWeight = weight % 1;
|
||
firstWeight = 1 - secondWeight;
|
||
this.lastWeight = weight % 1;
|
||
for (channel = 0; channel < this.channels; ++channel) {
|
||
outputBuffer[outputOffset++] = this.lastOutput[channel] * firstWeight + buffer[channel] * secondWeight;
|
||
}
|
||
}
|
||
weight -= 1;
|
||
for (bufferLength -= channels, sourceOffset = Math.floor(weight) * channels; outputOffset < outLength && sourceOffset < bufferLength;) {
|
||
secondWeight = weight % 1;
|
||
firstWeight = 1 - secondWeight;
|
||
for (channel = 0; channel < this.channels; ++channel) {
|
||
outputBuffer[outputOffset++] = buffer[sourceOffset + (channel > 0 ? channel : 0)] * firstWeight + buffer[sourceOffset + (channels + channel)] * secondWeight;
|
||
}
|
||
weight += ratioWeight;
|
||
sourceOffset = Math.floor(weight) * channels;
|
||
}
|
||
for (channel = 0; channel < channels; ++channel) {
|
||
this.lastOutput[channel] = buffer[sourceOffset++];
|
||
}
|
||
return this.bufferSlice(outputOffset);
|
||
};
|
||
}
|
||
multiTap() {
|
||
this.resampler = buffer => {
|
||
let bufferLength = buffer.length,
|
||
outLength,
|
||
output_variable_list,
|
||
channels = this.channels,
|
||
ratioWeight,
|
||
weight,
|
||
channel,
|
||
actualPosition,
|
||
amountToNext,
|
||
alreadyProcessedTail,
|
||
outputBuffer,
|
||
outputOffset,
|
||
currentPosition;
|
||
if (bufferLength % channels !== 0) {
|
||
throw new Error("Buffer was of incorrect sample length.");
|
||
}
|
||
if (bufferLength <= 0) {
|
||
return [];
|
||
}
|
||
outLength = this.outputBufferSize;
|
||
output_variable_list = [];
|
||
ratioWeight = this.ratioWeight;
|
||
weight = 0;
|
||
actualPosition = 0;
|
||
amountToNext = 0;
|
||
alreadyProcessedTail = !this.tailExists;
|
||
this.tailExists = false;
|
||
outputBuffer = this.outputBuffer;
|
||
outputOffset = 0;
|
||
currentPosition = 0;
|
||
for (channel = 0; channel < channels; ++channel) {
|
||
output_variable_list[channel] = 0;
|
||
}
|
||
do {
|
||
if (alreadyProcessedTail) {
|
||
weight = ratioWeight;
|
||
for (channel = 0; channel < channels; ++channel) {
|
||
output_variable_list[channel] = 0;
|
||
}
|
||
} else {
|
||
weight = this.lastWeight;
|
||
for (channel = 0; channel < channels; ++channel) {
|
||
output_variable_list[channel] = this.lastOutput[channel];
|
||
}
|
||
alreadyProcessedTail = true;
|
||
}
|
||
while (weight > 0 && actualPosition < bufferLength) {
|
||
amountToNext = 1 + actualPosition - currentPosition;
|
||
if (weight >= amountToNext) {
|
||
for (channel = 0; channel < channels; ++channel) {
|
||
output_variable_list[channel] += buffer[actualPosition++] * amountToNext;
|
||
}
|
||
currentPosition = actualPosition;
|
||
weight -= amountToNext;
|
||
} else {
|
||
for (channel = 0; channel < channels; ++channel) {
|
||
output_variable_list[channel] += buffer[actualPosition + (channel > 0 ? channel : 0)] * weight;
|
||
}
|
||
currentPosition += weight;
|
||
weight = 0;
|
||
break;
|
||
}
|
||
}
|
||
if (weight === 0) {
|
||
for (channel = 0; channel < channels; ++channel) {
|
||
outputBuffer[outputOffset++] = output_variable_list[channel] / ratioWeight;
|
||
}
|
||
} else {
|
||
this.lastWeight = weight;
|
||
for (channel = 0; channel < channels; ++channel) {
|
||
this.lastOutput[channel] = output_variable_list[channel];
|
||
}
|
||
this.tailExists = true;
|
||
break;
|
||
}
|
||
} while (actualPosition < bufferLength && outputOffset < outLength);
|
||
return this.bufferSlice(outputOffset);
|
||
};
|
||
}
|
||
resample(buffer) {
|
||
if (this.fromSampleRate == this.toSampleRate) {
|
||
this.ratioWeight = 1;
|
||
} else {
|
||
if (this.fromSampleRate < this.toSampleRate) {
|
||
this.lastWeight = 1;
|
||
} else {
|
||
this.tailExists = false;
|
||
this.lastWeight = 0;
|
||
}
|
||
this.initializeBuffers();
|
||
this.ratioWeight = this.fromSampleRate / this.toSampleRate;
|
||
}
|
||
return this.resampler(buffer);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将无符号Float32Array数组转化成有符号的Int16Array数组
|
||
* @param {Float32Array} input unsinged Float32Array
|
||
* @return {Int16Array} singed int16
|
||
*/
|
||
function floatTo16BitPCM(input) {
|
||
let i = input.length;
|
||
let output = new Int16Array(i);
|
||
while (i--) {
|
||
let s = Math.max(-1, Math.min(1, input[i]));
|
||
output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
||
}
|
||
return output;
|
||
}
|
||
|
||
/**
|
||
* 将无符号Float32Array数组转化成有符号的Int8Array数组
|
||
* @param {Float32Array} input unsinged Float32Array
|
||
* @return {Int8Array} singed int8
|
||
*/
|
||
function floatTo8BitPCM(input) {
|
||
let i = input.length;
|
||
let output = new Int8Array(i);
|
||
while (i--) {
|
||
let s = Math.max(-1, Math.min(1, input[i]));
|
||
const temp = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
||
output[i] = parseInt(255 / (65535 / (32768 + temp)), 10);
|
||
}
|
||
return output;
|
||
}
|
||
|
||
const QUANT_MASK = 0xf;
|
||
const SEG_SHIFT = 4;
|
||
const BIAS = 0x84;
|
||
const segEnd = [0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF];
|
||
function _search(val, table, size) {
|
||
for (let i = 0; i < size; i++) {
|
||
if (val <= table[i]) {
|
||
return i;
|
||
}
|
||
}
|
||
return size;
|
||
}
|
||
|
||
// alaw
|
||
function _linear2alaw(pcmVal) {
|
||
let mask;
|
||
let seg;
|
||
let aval;
|
||
if (pcmVal >= 0) {
|
||
mask = 0xD5;
|
||
} else {
|
||
mask = 0x55;
|
||
|
||
// pcmVal = -pcmVal - 8;
|
||
pcmVal = -pcmVal - 1;
|
||
if (pcmVal < 0) {
|
||
pcmVal = 32767;
|
||
}
|
||
}
|
||
/* Convert the scaled magnitude to segment number. */
|
||
seg = _search(pcmVal, segEnd, 8);
|
||
if (seg >= 8) {
|
||
return 0x7F ^ mask;
|
||
} else {
|
||
aval = seg << SEG_SHIFT;
|
||
if (seg < 2) {
|
||
aval |= pcmVal >> 4 & QUANT_MASK;
|
||
} else {
|
||
aval |= pcmVal >> seg + 3 & QUANT_MASK;
|
||
}
|
||
return aval ^ mask;
|
||
}
|
||
}
|
||
|
||
// ulaw
|
||
function _linear2ulaw(pcmVal) {
|
||
let mask = 0;
|
||
if (pcmVal < 0) {
|
||
pcmVal = BIAS - pcmVal;
|
||
mask = 0x7F;
|
||
} else {
|
||
pcmVal += BIAS;
|
||
mask = 0xFF;
|
||
}
|
||
let seg = _search(pcmVal, segEnd, 8);
|
||
if (seg >= 8) {
|
||
return 0x7F ^ mask;
|
||
} else {
|
||
let uval = seg << 4 | pcmVal >> seg + 3 & 0xF;
|
||
return uval ^ mask;
|
||
}
|
||
}
|
||
|
||
// pcm to g711a
|
||
function g711aEncoder(typedArray) {
|
||
const g711Array = [];
|
||
const tempArray = Array.prototype.slice.call(typedArray);
|
||
tempArray.forEach((i, index) => {
|
||
g711Array[index] = _linear2alaw(i);
|
||
});
|
||
return g711Array;
|
||
}
|
||
|
||
// g711u
|
||
function g711uEncoder(typedArray) {
|
||
const g711Array = [];
|
||
const tempArray = Array.prototype.slice.call(typedArray);
|
||
tempArray.forEach((i, index) => {
|
||
g711Array[index] = _linear2ulaw(i);
|
||
});
|
||
return g711Array;
|
||
}
|
||
|
||
class Debug {
|
||
constructor(master) {
|
||
this.log = function (name) {
|
||
if (master._opt.debug && master._opt.debugLevel == DEBUG_LEVEL.debug) {
|
||
const prefix = master._opt.debugUuid ? `[${master._opt.debugUuid}]` : '';
|
||
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||
args[_key - 1] = arguments[_key];
|
||
}
|
||
console.log(`JbPro${prefix}[\u2705\u2705\u2705][${name}]`, ...args);
|
||
}
|
||
};
|
||
this.warn = function (name) {
|
||
if (master._opt.debug && (master._opt.debugLevel == DEBUG_LEVEL.debug || master._opt.debugLevel == DEBUG_LEVEL.warn)) {
|
||
const prefix = master._opt.debugUuid ? `[${master._opt.debugUuid}]` : '';
|
||
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
||
args[_key2 - 1] = arguments[_key2];
|
||
}
|
||
console.log(`JbPro${prefix}[\u2757\u2757\u2757][${name}]`, ...args);
|
||
}
|
||
};
|
||
this.error = function (name) {
|
||
const prefix = master._opt.debugUuid ? `[${master._opt.debugUuid}]` : '';
|
||
for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
|
||
args[_key3 - 1] = arguments[_key3];
|
||
}
|
||
console.error(`JbPro${prefix}[\u274C\u274C\u274C][${name}]`, ...args);
|
||
|
||
// if (master.addMemoryLog) {
|
||
// master.addMemoryLog(`[${name}]`, ...args);
|
||
// }
|
||
};
|
||
}
|
||
}
|
||
|
||
class Events {
|
||
constructor(master) {
|
||
this.destroys = [];
|
||
this.proxy = this.proxy.bind(this);
|
||
this.master = master;
|
||
}
|
||
proxy(target, name, callback) {
|
||
let option = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
||
if (!target) {
|
||
return;
|
||
}
|
||
if (Array.isArray(name)) {
|
||
return name.map(item => this.proxy(target, item, callback, option));
|
||
}
|
||
target.addEventListener(name, callback, option);
|
||
const destroy = () => {
|
||
if (isFunction(target.removeEventListener)) {
|
||
target.removeEventListener(name, callback, option);
|
||
}
|
||
};
|
||
this.destroys.push(destroy);
|
||
return destroy;
|
||
}
|
||
destroy() {
|
||
this.master.debug && this.master.debug.log(`Events`, 'destroy');
|
||
this.destroys.forEach(event => event());
|
||
this.destroys = [];
|
||
}
|
||
}
|
||
|
||
class Talk extends Emitter {
|
||
constructor(player) {
|
||
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||
super();
|
||
/**@type {import('./constant').DEFAULT_TALK_OPTIONS}*/
|
||
this._opt = {};
|
||
if (player) {
|
||
this.player = player;
|
||
}
|
||
this.tag = 'talk';
|
||
const defaultOptions = getDefaultTalkOptions();
|
||
this._opt = Object.assign({}, defaultOptions, options);
|
||
this._opt.sampleRate = parseInt(this._opt.sampleRate, 10);
|
||
this._opt.sampleBitsWidth = parseInt(this._opt.sampleBitsWidth, 10);
|
||
this.audioContext = null;
|
||
this.gainNode = null;
|
||
this.recorder = null;
|
||
this.workletRecorder = null;
|
||
this.biquadFilter = null;
|
||
this.userMediaStream = null;
|
||
this.clearWorkletUrlTimeout = null;
|
||
// buffersize
|
||
this.bufferSize = 512; //
|
||
this._opt.audioBufferLength = this.calcAudioBufferLength();
|
||
this.audioBufferList = [];
|
||
// socket
|
||
this.socket = null;
|
||
this.socketStatus = WEBSOCKET_STATUS.notConnect;
|
||
|
||
//
|
||
this.mediaStreamSource = null;
|
||
this.heartInterval = null;
|
||
this.checkGetUserMediaTimeout = null;
|
||
this.wsUrl = null;
|
||
this.startTimestamp = 0;
|
||
|
||
// 报文数据
|
||
this.sequenceId = 0;
|
||
this.tempTimestamp = null;
|
||
this.tempRtpBufferList = [];
|
||
this.events = new Events(this);
|
||
this._initTalk();
|
||
//
|
||
if (!this.player) {
|
||
this.debug = new Debug(this);
|
||
}
|
||
if ((this._opt.encType === TALK_ENC_TYPE.g711a || this._opt.encType === TALK_ENC_TYPE.g711u) && !(this._opt.sampleRate === 8000 && this._opt.sampleBitsWidth === 16)) {
|
||
this.warn(this.tag, `
|
||
encType is ${this._opt.encType} and sampleBitsWidth is ${this._opt.sampleBitsWidth}, set sampleBitsWidth to ${this._opt.sampleBitsWidth}。
|
||
${this._opt.encType} only support sampleRate 8000 and sampleBitsWidth 16`);
|
||
}
|
||
this.log(this.tag, 'init', JSON.stringify(this._opt));
|
||
}
|
||
destroy() {
|
||
if (this.clearWorkletUrlTimeout) {
|
||
clearTimeout(this.clearWorkletUrlTimeout);
|
||
this.clearWorkletUrlTimeout = null;
|
||
}
|
||
|
||
//
|
||
if (this.userMediaStream) {
|
||
this.userMediaStream.getTracks && this.userMediaStream.getTracks().forEach(track => {
|
||
track.stop();
|
||
});
|
||
this.userMediaStream = null;
|
||
}
|
||
if (this.mediaStreamSource) {
|
||
this.mediaStreamSource.disconnect();
|
||
this.mediaStreamSource = null;
|
||
}
|
||
if (this.recorder) {
|
||
this.recorder.disconnect();
|
||
this.recorder.onaudioprocess = null;
|
||
this.recorder = null;
|
||
}
|
||
if (this.biquadFilter) {
|
||
this.biquadFilter.disconnect();
|
||
this.biquadFilter = null;
|
||
}
|
||
if (this.gainNode) {
|
||
this.gainNode.disconnect();
|
||
this.gainNode = null;
|
||
}
|
||
if (this.workletRecorder) {
|
||
this.workletRecorder.disconnect();
|
||
this.workletRecorder = null;
|
||
}
|
||
if (this.socket) {
|
||
if (this.socketStatus === WEBSOCKET_STATUS.open) {
|
||
this._sendClose();
|
||
}
|
||
this.socket.close();
|
||
this.socket = null;
|
||
}
|
||
this._stopHeartInterval();
|
||
this._stopCheckGetUserMediaTimeout();
|
||
this.audioContext = null;
|
||
this.gainNode = null;
|
||
this.recorder = null;
|
||
this.audioBufferList = [];
|
||
this.sequenceId = 0;
|
||
this.wsUrl = null;
|
||
this.tempTimestamp = null;
|
||
this.tempRtpBufferList = [];
|
||
this.startTimestamp = 0;
|
||
this.log('talk', 'destroy');
|
||
}
|
||
addRtpToBuffer(rtp) {
|
||
const len = rtp.length + this.tempRtpBufferList.length;
|
||
const buffer = new Uint8Array(len);
|
||
buffer.set(this.tempRtpBufferList, 0);
|
||
buffer.set(rtp, this.tempRtpBufferList.length);
|
||
this.tempRtpBufferList = buffer;
|
||
//console.log('addRtpToBuffer length and byteLength ', this.tempRtpBufferList.length, this.tempRtpBufferList.byteLength)
|
||
}
|
||
|
||
downloadRtpFile() {
|
||
const blob = new Blob([this.tempRtpBufferList]);
|
||
try {
|
||
const oa = document.createElement('a');
|
||
oa.href = window.URL.createObjectURL(blob);
|
||
oa.download = Date.now() + '.rtp';
|
||
oa.click();
|
||
window.URL.revokeObjectURL(oa.href);
|
||
} catch (e) {
|
||
console.error('downloadRtpFile', e);
|
||
}
|
||
}
|
||
calcAudioBufferLength() {
|
||
const {
|
||
sampleRate,
|
||
sampleBitsWidth
|
||
} = this._opt;
|
||
// 默认走的是 20ms 8000 采样率 16 位精度
|
||
return sampleRate * 8 * (20 / 1000) / 8;
|
||
}
|
||
get socketStatusOpen() {
|
||
return this.socketStatus === WEBSOCKET_STATUS.open;
|
||
}
|
||
log() {
|
||
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
||
args[_key] = arguments[_key];
|
||
}
|
||
this._log('log', ...args);
|
||
}
|
||
warn() {
|
||
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
|
||
args[_key2] = arguments[_key2];
|
||
}
|
||
this._log('warn', ...args);
|
||
}
|
||
error() {
|
||
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
|
||
args[_key3] = arguments[_key3];
|
||
}
|
||
this._log('error', ...args);
|
||
}
|
||
_log(type) {
|
||
for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
|
||
args[_key4 - 1] = arguments[_key4];
|
||
}
|
||
if (this.player) {
|
||
this.player.debug[type](...args);
|
||
} else if (this.debug) {
|
||
this.debug[type](...args);
|
||
} else {
|
||
console[type](...args);
|
||
}
|
||
}
|
||
_getSequenceId() {
|
||
return ++this.sequenceId;
|
||
}
|
||
_createWebSocket() {
|
||
return new Promise((resolve, reject) => {
|
||
const proxy = this.events.proxy;
|
||
this.socket = new WebSocket(this.wsUrl);
|
||
this.socket.binaryType = 'arraybuffer';
|
||
this.emit(EVENTS.talkStreamStart);
|
||
proxy(this.socket, WEBSOCKET_EVENTS.open, () => {
|
||
this.socketStatus = WEBSOCKET_STATUS.open;
|
||
this.log(this.tag, 'websocket open -> do talk');
|
||
this.emit(EVENTS.talkStreamOpen);
|
||
resolve();
|
||
this._doTalk();
|
||
});
|
||
proxy(this.socket, WEBSOCKET_EVENTS.message, event => {
|
||
this.log(this.tag, 'websocket message', event.data);
|
||
});
|
||
proxy(this.socket, WEBSOCKET_EVENTS.close, e => {
|
||
this.socketStatus = WEBSOCKET_STATUS.close;
|
||
this.warn(this.tag, 'websocket close -> reject', e);
|
||
this.emit(EVENTS.talkStreamClose);
|
||
reject(e);
|
||
});
|
||
proxy(this.socket, WEBSOCKET_EVENTS.error, error => {
|
||
this.socketStatus = WEBSOCKET_STATUS.error;
|
||
this.error(this.tag, 'websocket error -> reject', error);
|
||
this.emit(EVENTS.talkStreamError, error);
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
_sendClose() {}
|
||
_initTalk() {
|
||
this._initMethods();
|
||
if (this._opt.engine === TALK_ENGINE.worklet) {
|
||
this._initWorklet();
|
||
} else if (this._opt.engine === TALK_ENGINE.script) {
|
||
this._initScriptProcessor();
|
||
}
|
||
this.log(this.tag, 'audioContext samplerate', this.audioContext.sampleRate);
|
||
}
|
||
_initMethods() {
|
||
//
|
||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||
sampleRate: 48000
|
||
});
|
||
this.gainNode = this.audioContext.createGain();
|
||
// default 1
|
||
this.gainNode.gain.value = 1;
|
||
|
||
// 消音器
|
||
this.biquadFilter = this.audioContext.createBiquadFilter();
|
||
this.biquadFilter.type = "lowpass";
|
||
this.biquadFilter.frequency.value = 3000;
|
||
this.resampler = new Resampler({
|
||
fromSampleRate: this.audioContext.sampleRate,
|
||
toSampleRate: this._opt.sampleRate,
|
||
channels: this._opt.numberChannels,
|
||
inputBufferSize: this.bufferSize
|
||
});
|
||
}
|
||
_initScriptProcessor() {
|
||
//
|
||
const createScript = this.audioContext.createScriptProcessor || this.audioContext.createJavaScriptNode;
|
||
this.recorder = createScript.apply(this.audioContext, [this.bufferSize, this._opt.numberChannels, this._opt.numberChannels]);
|
||
this.recorder.onaudioprocess = e => this._onaudioprocess(e);
|
||
}
|
||
_initWorklet() {
|
||
function workletProcess() {
|
||
class TalkProcessor extends AudioWorkletProcessor {
|
||
constructor(options) {
|
||
super();
|
||
this._cursor = 0;
|
||
this._bufferSize = options.processorOptions.bufferSize;
|
||
this._buffer = new Float32Array(this._bufferSize);
|
||
}
|
||
process(inputs, outputs, parameters) {
|
||
if (!inputs.length || !inputs[0].length) {
|
||
return true;
|
||
}
|
||
for (let i = 0; i < inputs[0][0].length; i++) {
|
||
this._cursor += 1;
|
||
if (this._cursor === this._bufferSize) {
|
||
this._cursor = 0;
|
||
this.port.postMessage({
|
||
eventType: 'data',
|
||
buffer: this._buffer
|
||
});
|
||
}
|
||
this._buffer[this._cursor] = inputs[0][0][i];
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
registerProcessor('talk-processor', TalkProcessor);
|
||
}
|
||
const workletUrl = createWorkletModuleUrl(workletProcess);
|
||
this.audioContext.audioWorklet && this.audioContext.audioWorklet.addModule(workletUrl).then(() => {
|
||
const workletNode = new AudioWorkletNode(this.audioContext, 'talk-processor', {
|
||
processorOptions: {
|
||
bufferSize: this.bufferSize
|
||
}
|
||
});
|
||
workletNode.connect(this.gainNode);
|
||
workletNode.port.onmessage = e => {
|
||
if (e.data.eventType === 'data') {
|
||
this._encodeAudioData(e.data.buffer);
|
||
}
|
||
};
|
||
this.workletRecorder = workletNode;
|
||
});
|
||
this.clearWorkletUrlTimeout = setTimeout(() => {
|
||
URL.revokeObjectURL(workletUrl);
|
||
this.clearWorkletUrlTimeout = null;
|
||
}, URL_OBJECT_CLEAR_TIME);
|
||
}
|
||
_onaudioprocess(e) {
|
||
// 数组里的每个数字都是32位的单精度浮点数
|
||
// 默认是单精度
|
||
const float32Array = e.inputBuffer.getChannelData(0);
|
||
// send 出去。
|
||
this._encodeAudioData(new Float32Array(float32Array));
|
||
}
|
||
_encodeAudioData(float32Array) {
|
||
// 没有说话
|
||
if (float32Array[0] === 0 && float32Array[1] === 0) {
|
||
this.log(this.tag, 'empty audio data');
|
||
return;
|
||
}
|
||
const resampleBuffer = this.resampler.resample(float32Array);
|
||
// default 32Bit
|
||
let tempArrayBuffer = resampleBuffer;
|
||
if (this._opt.sampleBitsWidth === 16) {
|
||
tempArrayBuffer = floatTo16BitPCM(resampleBuffer);
|
||
} else if (this._opt.sampleBitsWidth === 8) {
|
||
tempArrayBuffer = floatTo8BitPCM(resampleBuffer);
|
||
}
|
||
if (tempArrayBuffer.buffer !== null) {
|
||
let typedArray = null;
|
||
if (this._opt.encType === TALK_ENC_TYPE.g711a) {
|
||
typedArray = g711aEncoder(tempArrayBuffer);
|
||
} else if (this._opt.encType === TALK_ENC_TYPE.g711u) {
|
||
typedArray = g711uEncoder(tempArrayBuffer);
|
||
} else if (this._opt.encType === TALK_ENC_TYPE.pcm) {
|
||
typedArray = tempArrayBuffer;
|
||
}
|
||
// 变成了8位 array 了
|
||
const unit8Array = new Uint8Array(typedArray);
|
||
for (let i = 0; i < unit8Array.length; i++) {
|
||
let audioBufferLength = this.audioBufferList.length;
|
||
this.audioBufferList[audioBufferLength++] = unit8Array[i];
|
||
if (this.audioBufferList.length === this._opt.audioBufferLength) {
|
||
this._sendTalkMsg(new Uint8Array(this.audioBufferList));
|
||
this.audioBufferList = [];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
_parseAudioMsg(typedArray) {
|
||
let typeArray2 = null;
|
||
// rtp reumx just support g711a or g711u or opus
|
||
if (this._opt.packetType === TALK_PACKET_TYPE.rtp && (this._opt.encType === TALK_ENC_TYPE.g711a || this._opt.encType === TALK_ENC_TYPE.g711u)) {
|
||
typeArray2 = this.rtpPacket(typedArray);
|
||
} else if (this._opt.packetType === TALK_PACKET_TYPE.empty) {
|
||
// 默认
|
||
typeArray2 = typedArray;
|
||
}
|
||
return typeArray2;
|
||
}
|
||
rtpPacket(typedArray) {
|
||
const rtpHeader = [];
|
||
//2 bits RTP的版本,这里统一为2
|
||
const version = 2;
|
||
//1 bit 如果置1,在packet的末尾被填充,填充有时是方便一些针对固定长度的算法的封装
|
||
const padding = 0;
|
||
//1 bit 如果置1,在RTP Header会跟着一个header extension
|
||
const extension = 0;
|
||
//4 bits 表示头部后 特约信源 的个数
|
||
const csrcCount = 0;
|
||
//1 bit 不同的有效载荷有不同的含义,marker=1; 对于视频,标记一帧的结束;对于音频,标记会话的开始。
|
||
const marker = 1;
|
||
//7 bits 表示所传输的多媒体的类型,
|
||
let playloadType = 0;
|
||
//16 bits 每个RTP packet的sequence number会自动加一,以便接收端检测丢包情况
|
||
let sequenceNumber = 0;
|
||
//32 bits 时间戳
|
||
let timestamp = 0;
|
||
//32 bits 同步源的id,每两个同步源的id不能相同
|
||
const ssrc = this._opt.rtpSsrc;
|
||
//
|
||
const frameLen = typedArray.length;
|
||
if (this._opt.encType === TALK_ENC_TYPE.g711a) {
|
||
playloadType = RTP_PAYLOAD_TYPE.g711a;
|
||
} else if (this._opt.encType === TALK_ENC_TYPE.g711u) {
|
||
playloadType = RTP_PAYLOAD_TYPE.g711u;
|
||
} else if (this._opt.encType === TALK_ENC_TYPE.opus) {
|
||
playloadType = RTP_PAYLOAD_TYPE.opus;
|
||
}
|
||
if (!this.startTimestamp) {
|
||
this.startTimestamp = now();
|
||
}
|
||
timestamp = now() - this.startTimestamp;
|
||
sequenceNumber = this._getSequenceId();
|
||
|
||
// frame length
|
||
// 需要在rtp头前面加两个字节,表示数据包长度(整个rtp包长度)
|
||
// 国标流udp不需要两个字节长度
|
||
// 国标流tcp需要两个字节长度
|
||
// websocket目前是按照tcp的做法做的
|
||
let index = 0;
|
||
if (this._opt.packetTcpSendType === TALK_PACKAGE_TCP_SEND_TYPE.tcp) {
|
||
const rtpFrameLen = frameLen + 12;
|
||
// 0
|
||
rtpHeader[index++] = 0xFF & rtpFrameLen >> 8;
|
||
// 1
|
||
rtpHeader[index++] = 0xFF & rtpFrameLen >> 0;
|
||
}
|
||
rtpHeader[index++] = (version << 6) + (padding << 5) + (extension << 4) + csrcCount;
|
||
rtpHeader[index++] = (marker << 7) + playloadType;
|
||
rtpHeader[index++] = sequenceNumber / (0xff + 1);
|
||
rtpHeader[index++] = sequenceNumber % (0xff + 1);
|
||
rtpHeader[index++] = timestamp / (0xffff + 1) / (0xff + 1);
|
||
rtpHeader[index++] = timestamp / (0xffff + 1) % (0xff + 1);
|
||
rtpHeader[index++] = timestamp % (0xffff + 1) / (0xff + 1);
|
||
rtpHeader[index++] = timestamp % (0xffff + 1) % (0xff + 1);
|
||
rtpHeader[index++] = ssrc / (0xffff + 1) / (0xff + 1);
|
||
rtpHeader[index++] = ssrc / (0xffff + 1) % (0xff + 1);
|
||
rtpHeader[index++] = ssrc % (0xffff + 1) / (0xff + 1);
|
||
rtpHeader[index++] = ssrc % (0xffff + 1) % (0xff + 1);
|
||
let typeArray2 = rtpHeader.concat([...typedArray]);
|
||
let binary = new Uint8Array(typeArray2.length);
|
||
for (let ii = 0; ii < typeArray2.length; ii++) {
|
||
binary[ii] = typeArray2[ii];
|
||
}
|
||
return binary;
|
||
}
|
||
opusPacket(typedArray) {
|
||
//TODO:待完成
|
||
|
||
return typedArray;
|
||
}
|
||
_sendTalkMsg(typedArray) {
|
||
if (this.tempTimestamp === null) {
|
||
this.tempTimestamp = now();
|
||
}
|
||
const timestamp = now();
|
||
const diff = timestamp - this.tempTimestamp;
|
||
const typedArray2 = this._parseAudioMsg(typedArray);
|
||
this.log(this.tag, `'send talk msg and diff is ${diff} and byteLength is ${typedArray2.byteLength} and length is ${typedArray2.length}, and g711 length is ${typedArray.length}`);
|
||
if (isTrue(this._opt.saveRtpToFile)) {
|
||
if (this._opt.packetType === TALK_PACKET_TYPE.rtp) {
|
||
this.addRtpToBuffer(typedArray2);
|
||
}
|
||
}
|
||
if (typedArray2) {
|
||
if (this.socketStatusOpen) {
|
||
this.socket.send(typedArray2.buffer);
|
||
} else {
|
||
this.emit(EVENTS_ERROR.tallWebsocketClosedByError);
|
||
}
|
||
}
|
||
this.tempTimestamp = timestamp;
|
||
}
|
||
_doTalk() {
|
||
this._getUserMedia();
|
||
// this._getUserMedia2();
|
||
// this._getUserMedia3();
|
||
}
|
||
|
||
_getUserMedia() {
|
||
this.log(this.tag, 'getUserMedia');
|
||
// 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
|
||
if (window.navigator.mediaDevices === undefined) {
|
||
window.navigator.mediaDevices = {};
|
||
}
|
||
|
||
// 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
|
||
// 因为这样可能会覆盖已有的属性。这里我们只会在没有 getUserMedia 属性的时候添加它。
|
||
if (window.navigator.mediaDevices.getUserMedia === undefined) {
|
||
this.log(this.tag, 'window.navigator.mediaDevices.getUserMedia is undefined and init function');
|
||
window.navigator.mediaDevices.getUserMedia = function (constraints) {
|
||
// 首先,如果有 getUserMedia 的话,就获得它
|
||
var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
|
||
|
||
// 一些浏览器根本没实现它 - 那么就返回一个 error 到 promise 的 reject 来保持一个统一的接口
|
||
// 由于受浏览器的限制,navigator.mediaDevices.getUserMedia在https协议下是可以正常使用的,
|
||
// 而在http协议下只允许localhost/127.0.0.1这两个域名访问,
|
||
// 因此在开发时应做好容灾处理,上线时则需要确认生产环境是否处于https协议下。
|
||
if (!getUserMedia) {
|
||
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
|
||
}
|
||
|
||
// 否则,为老的 navigator.getUserMedia 方法包裹一个 Promise
|
||
return new Promise(function (resolve, reject) {
|
||
getUserMedia.call(navigator, constraints, resolve, reject);
|
||
});
|
||
};
|
||
}
|
||
if (this._opt.checkGetUserMediaTimeout) {
|
||
this._startCheckGetUserMediaTimeout();
|
||
}
|
||
// 最后经过反复测试,只有noiseSuppression+echoCancellation同时生效时,打开录音后再播放音频,系统音量一定会变小,
|
||
// 很惨的是getUserMedia只要你没有配置这两个参数,默认就是同时开启的;只要你给这两参数任意一个设为false,或者都设为false,
|
||
// 就不会影响手机系统音量。
|
||
window.navigator.mediaDevices.getUserMedia({
|
||
audio: this._opt.audioConstraints,
|
||
video: false
|
||
}).then(stream => {
|
||
this.log(this.tag, 'getUserMedia success');
|
||
this.userMediaStream = stream;
|
||
this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);
|
||
this.mediaStreamSource.connect(this.biquadFilter);
|
||
if (this.recorder) {
|
||
this.biquadFilter.connect(this.recorder);
|
||
this.recorder.connect(this.gainNode);
|
||
} else if (this.workletRecorder) {
|
||
this.biquadFilter.connect(this.workletRecorder);
|
||
this.workletRecorder.connect(this.gainNode);
|
||
}
|
||
this.gainNode.connect(this.audioContext.destination);
|
||
this.emit(EVENTS.talkGetUserMediaSuccess);
|
||
|
||
// check stream inactive
|
||
if (stream.oninactive === null) {
|
||
stream.oninactive = e => {
|
||
this._handleStreamInactive(e);
|
||
};
|
||
}
|
||
}).catch(e => {
|
||
this.error(this.tag, 'getUserMedia error', e.toString());
|
||
this.emit(EVENTS.talkGetUserMediaFail, e.toString());
|
||
}).finally(() => {
|
||
this.log(this.tag, 'getUserMedia finally');
|
||
this._stopCheckGetUserMediaTimeout();
|
||
});
|
||
}
|
||
_getUserMedia2() {
|
||
this.log(this.tag, 'getUserMedia');
|
||
navigator.mediaDevices ? navigator.mediaDevices.getUserMedia({
|
||
audio: true
|
||
}).then(stream => {
|
||
this.log(this.tag, 'getUserMedia2 success');
|
||
}) : navigator.getUserMedia({
|
||
audio: true
|
||
}, this.log(this.tag, 'getUserMedia2 success'), this.log(this.tag, 'getUserMedia2 fail'));
|
||
}
|
||
async _getUserMedia3() {
|
||
this.log(this.tag, 'getUserMedia3');
|
||
try {
|
||
const stream = await navigator.mediaDevices.getUserMedia({
|
||
audio: {
|
||
latency: true,
|
||
noiseSuppression: true,
|
||
autoGainControl: true,
|
||
echoCancellation: true,
|
||
sampleRate: 48000,
|
||
channelCount: 1
|
||
},
|
||
video: false
|
||
});
|
||
console.log('getUserMedia() got stream:', stream);
|
||
this.log(this.tag, 'getUserMedia3 success');
|
||
} catch (e) {
|
||
this.log(this.tag, 'getUserMedia3 fail');
|
||
}
|
||
}
|
||
_handleStreamInactive(e) {
|
||
if (this.userMediaStream) {
|
||
this.warn(this.tag, 'stream oninactive', e);
|
||
this.emit(EVENTS.talkStreamInactive);
|
||
}
|
||
}
|
||
_startCheckGetUserMediaTimeout() {
|
||
this._stopCheckGetUserMediaTimeout();
|
||
this.checkGetUserMediaTimeout = setTimeout(() => {
|
||
this.log(this.tag, 'check getUserMedia timeout');
|
||
this.emit(EVENTS.talkGetUserMediaTimeout);
|
||
}, this._opt.getUserMediaTimeout);
|
||
}
|
||
_stopCheckGetUserMediaTimeout() {
|
||
if (this.checkGetUserMediaTimeout) {
|
||
this.log(this.tag, 'stop checkGetUserMediaTimeout');
|
||
clearTimeout(this.checkGetUserMediaTimeout);
|
||
this.checkGetUserMediaTimeout = null;
|
||
}
|
||
}
|
||
_startHeartInterval() {
|
||
// 定时发送心跳,
|
||
this.heartInterval = setInterval(() => {
|
||
this.log(this.tag, 'heart interval');
|
||
let data = [0x23, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||
data = new Uint8Array(data);
|
||
this.socket.send(data.buffer);
|
||
}, 15 * 1000);
|
||
}
|
||
_stopHeartInterval() {
|
||
if (this.heartInterval) {
|
||
this.log(this.tag, 'stop heart interval');
|
||
clearInterval(this.heartInterval);
|
||
this.heartInterval = null;
|
||
}
|
||
}
|
||
startTalk(wsUrl) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!isSupportGetUserMedia()) {
|
||
return reject('not support getUserMedia');
|
||
}
|
||
this.wsUrl = wsUrl;
|
||
if (this._opt.testMicrophone) {
|
||
this._doTalk();
|
||
} else {
|
||
if (!this.wsUrl) {
|
||
return reject('wsUrl is null');
|
||
}
|
||
this._createWebSocket().catch(e => {
|
||
reject(e);
|
||
});
|
||
}
|
||
// reject
|
||
this.once(EVENTS.talkGetUserMediaFail, () => {
|
||
reject('getUserMedia fail');
|
||
});
|
||
// only get user media success and resolve
|
||
this.once(EVENTS.talkGetUserMediaSuccess, () => {
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
setVolume(volume) {
|
||
volume = parseFloat(volume).toFixed(2);
|
||
if (isNaN(volume)) {
|
||
return;
|
||
}
|
||
volume = clamp(volume, 0, 1);
|
||
this.gainNode.gain.value = volume;
|
||
}
|
||
getOption() {
|
||
return this._opt;
|
||
}
|
||
get volume() {
|
||
return this.gainNode ? parseFloat(this.gainNode.gain.value * 100).toFixed(0) : null;
|
||
}
|
||
}
|
||
|
||
class JessibucaProTalk extends Emitter {
|
||
constructor() {
|
||
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
||
super();
|
||
this.talk = null;
|
||
/**@type {import('./constant').DEFAULT_TALK_OPTIONS}*/
|
||
this._opt = options;
|
||
this.LOG_TAG = 'JbProTalk';
|
||
this.debug = new Debug(this);
|
||
this.debug.log(this.LOG_TAG, 'init', JSON.stringify(options));
|
||
}
|
||
destroy() {
|
||
this.debug.log(this.LOG_TAG, 'destroy()');
|
||
this.off();
|
||
if (this.talk) {
|
||
this.talk.destroy();
|
||
this.talk = null;
|
||
}
|
||
this.debug.log(this.LOG_TAG, 'destroy');
|
||
}
|
||
_initTalk() {
|
||
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
||
if (this.talk) {
|
||
this.debug.log(this.LOG_TAG, '_initTalk this.talk is not null and destroy');
|
||
this.talk.destroy();
|
||
this.talk = null;
|
||
}
|
||
const opt = Object.assign({}, clone(this._opt), options);
|
||
this.talk = new Talk(null, opt);
|
||
this.debug.log(this.LOG_TAG, '_initTalk', this.talk.getOption());
|
||
this._bindTalkEvents();
|
||
}
|
||
_bindTalkEvents() {
|
||
// 对外的事件
|
||
Object.keys(TALK_EVENTS).forEach(key => {
|
||
this.talk.on(TALK_EVENTS[key], value => {
|
||
this.emit(key, value);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param wsUrl
|
||
* @param options
|
||
* @returns {Promise<unknown>}
|
||
*/
|
||
startTalk(wsUrl) {
|
||
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||
return new Promise((resolve, reject) => {
|
||
this.debug.log(this.LOG_TAG, 'startTalk', wsUrl, JSON.stringify(options));
|
||
this._initTalk(options);
|
||
this.talk.startTalk(wsUrl).then(() => {
|
||
resolve();
|
||
this.talk.once(EVENTS.talkStreamClose, () => {
|
||
this.debug.warn(this.LOG_TAG, 'talkStreamClose -> stopTalk');
|
||
this.stopTalk().catch(e => {
|
||
this.debug.warn(this.LOG_TAG, 'talkStreamClose stopTalk', e);
|
||
}).finally(() => {
|
||
this.emit(EVENTS.talkFailedAndStop, EVENTS.talkStreamClose);
|
||
});
|
||
});
|
||
this.talk.once(EVENTS.talkStreamError, e => {
|
||
this.debug.error(this.LOG_TAG, 'talkStreamError -> stopTalk');
|
||
this.stopTalk().catch(e => {
|
||
this.debug.warn(this.LOG_TAG, 'talkStreamError stopTalk', e);
|
||
}).finally(() => {
|
||
this.emit(EVENTS.talkFailedAndStop, EVENTS.talkStreamError);
|
||
});
|
||
});
|
||
this.talk.once(EVENTS.talkStreamInactive, () => {
|
||
this.debug.warn(this.LOG_TAG, 'talkStreamInactive -> stopTalk');
|
||
this.stopTalk().catch(e => {
|
||
this.debug.warn(this.LOG_TAG, 'talkStreamInactive stopTalk', e);
|
||
}).finally(() => {
|
||
this.emit(EVENTS.talkFailedAndStop, EVENTS.talkStreamInactive);
|
||
});
|
||
});
|
||
this.talk.once(EVENTS.talkGetUserMediaTimeout, () => {
|
||
this.debug.warn(this.LOG_TAG, 'talkGetUserMediaTimeout -> stopTalk');
|
||
this.stopTalk().catch(e => {
|
||
this.debug.warn(this.LOG_TAG, 'talkGetUserMediaTimeout stopTalk', e);
|
||
}).finally(() => {
|
||
this.emit(EVENTS.talkFailedAndStop, EVENTS.talkGetUserMediaTimeout);
|
||
});
|
||
});
|
||
}).catch(e => {
|
||
reject(e);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @returns {Promise<unknown>}
|
||
*/
|
||
stopTalk() {
|
||
return new Promise((resolve, reject) => {
|
||
this.debug.log(this.LOG_TAG, 'stopTalk()');
|
||
if (!this.talk) {
|
||
reject('talk is not init');
|
||
}
|
||
this.talk.destroy();
|
||
this.talk = null;
|
||
resolve();
|
||
});
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @returns {Promise<unknown>}
|
||
*/
|
||
getTalkVolume() {
|
||
return new Promise((resolve, reject) => {
|
||
if (!this.talk) {
|
||
reject('talk is not init');
|
||
}
|
||
let result = this.talk.volume;
|
||
resolve(result);
|
||
});
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param volume
|
||
* @returns {Promise<unknown>}
|
||
*/
|
||
setTalkVolume(volume) {
|
||
return new Promise((resolve, reject) => {
|
||
this.debug.log(this.LOG_TAG, 'setTalkVolume', volume);
|
||
if (!this.talk) {
|
||
reject('talk is not init');
|
||
}
|
||
this.talk.setVolume(volume / 100);
|
||
resolve();
|
||
});
|
||
}
|
||
downloadTempRtpFile() {
|
||
return new Promise((resolve, reject) => {
|
||
if (this.talk) {
|
||
this.talk.downloadRtpFile();
|
||
resolve();
|
||
} else {
|
||
reject('talk is not init');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
JessibucaProTalk.EVENTS = TALK_EVENTS;
|
||
window.JessibucaProTalk = JessibucaProTalk;
|
||
window.JbProTalk = JessibucaProTalk;
|
||
window.WebPlayerProTalk = JessibucaProTalk;
|
||
|
||
}));
|
||
//# sourceMappingURL=web-player-pro-talk.js.map
|