UNPKG

rtc-phone

Version:
896 lines (820 loc) 27.8 kB
import EventEmitter from "eventemitter3"; import utils from "./utils/utils"; import Log from "./utils/Log"; class RtcPhone extends EventEmitter { /** * * @param {String} webRtcType Sets the default webrtc type. Supported values: native, w4a and erisson. * @param {Integer} pfs Sets the video fps. Requires webrt4all plugin. * @param {String} maxVideoSize maxVideoSize value. Supported values: "sqcif", "qcif" "qvga" "cif" "hvga", "vga", "4cif", "svga", "480p", "720p", "16cif", "1080p", "2160p". * @param {Integer} maxBandwidthUp Sets the maximum bandwidth (upload). Requires webrt4all plugin. value (kbps). * @param {Integer} maxBandwidthDown Sets the maximum bandwidth (down). Requires webrt4all plugin. value (kbps). * @param {Boolean} zeroArtifacts Defines whether to enable "zero-artifacts" features. Requires webrt4all plugin. * @param {Boolean} startNativeDebug Starts debugging the native (C/C++) code. Requires webrt4all plugin. On Windows, the output file should be at C:\Users\<YOUR LOGIN>\AppData\Local\Temp\Low\webrtc4all.log. Starting the native debug isn't recommended and must be done to track issues only. */ constructor({ debugLevel = "info", webRtcType = "native", pfs, maxVideoSize, maxBandwidthUp, maxBandwidthDown, zeroArtifacts, startNativeDebug = false, ringToneDom = null, ringbackToneDom = null, audioRemoteDom, videoLocalDom = null, videoRemoteDom = null, onInitialized, onStackStartFailed, onConnected, onDisconnected, onRinging, onDialing, onTalking, onReleased, }) { super(); if (document.readyState !== "complete") { throw "Dom not ready !"; } Log.info(`RtcPhone Constructor:location=${window.location}`); // set debug level this.debugLevel = debugLevel; SIPml.setDebugLevel(this.debugLevel); // this.webRtcType = webRtcType; // this.pfs = pfs; this.maxVideoSize = maxVideoSize; this.maxBandwidthUp = maxBandwidthUp; this.maxBandwidthDown = maxBandwidthDown; this.zeroArtifacts = zeroArtifacts; this.startNativeDebug = startNativeDebug; // check dom if (ringToneDom && !(ringToneDom instanceof HTMLAudioElement)) { throw new Error(`Parameter 'ringToneDom' not found or not an audio element.`); } else { this.ringTone = ringToneDom; } if (ringbackToneDom && !(ringbackToneDom instanceof HTMLAudioElement)) { throw new Error(`Parameter 'ringbackToneDom' not found or not an audio element.`); } else { this.ringbackTone = ringbackToneDom; } if (!(audioRemoteDom instanceof HTMLAudioElement)) { throw new Error(`Parameter 'audioRemoteDom' not found or not an audio element.`); } else { this.audioRemote = audioRemoteDom; } if (videoLocalDom && !(videoLocalDom instanceof HTMLVideoElement)) { throw new Error(`Parameter 'videoLocalDom' not found or not an video element.`); } else { this.videoLocal = videoLocalDom; } if (videoRemoteDom && !(videoRemoteDom instanceof HTMLVideoElement)) { throw new Error(`Parameter 'videoRemoteDom' not found or not an video element.`); } else { this.videoRemote = videoRemoteDom; } this.configCall = { audio_remote: this.audioRemote, video_local: this.videoLocal, video_remote: this.videoRemote, screencast_window_id: 0x00000000, // entire desktop bandwidth: { audio: undefined, video: undefined }, video_size: { minWidth: undefined, minHeight: undefined, maxWidth: undefined, maxHeight: undefined, }, events_listener: { events: "*", listener: this.onSipEventSession.bind(this), }, sip_caps: [ { name: "+g.oma.sip-im" }, { name: "language", value: '"en,fr"' }, ], }; // sip协议栈对象 this.sipStack = null; // 注册对象 this.sipSessionRegister = null; // 主会话 this.sipSessionCall = null; // 转接呼叫会话 this.sipSessionTransferCallByCti = null; this.status = "uninitialized"; // add event listener utils.isFunction(onInitialized) && this.on("initialized", onInitialized); utils.isFunction(onStackStartFailed) && this.on("stackStartFailed", onStackStartFailed); utils.isFunction(onConnected) && this.on("connected", onConnected); utils.isFunction(onDisconnected) && this.on("disconnected", onDisconnected); utils.isFunction(onRinging) && this.on("ringing", onRinging); utils.isFunction(onDialing) && this.on("dialing", onDialing); utils.isFunction(onTalking) && this.on("talking", onTalking); utils.isFunction(onReleased) && this.on("released", onReleased); } /** * initialize SIPML5 */ sipInit() { if (SIPml.isInitialized()) { this.postInit(); } else { // set default webrtc type (before initialization) SIPml.setWebRtcType(this.webRtcType); // initialize SIPML5 SIPml.init(this.postInit.bind(this)); // set other options after initialization if (this.pfs) SIPml.setFps(this.pfs); if (this.maxVideoSize) SIPml.setMaxVideoSize(this.maxVideoSize); if (this.maxBandwidthUp) SIPml.setMaxBandwidthUp(this.maxBandwidthUp); if (this.maxBandwidthDown) SIPml.setMaxBandwidthDown(this.maxBandwidthDown); if (this.zeroArtifacts) SIPml.setZeroArtifacts(this.zeroArtifacts === true); if (this.startNativeDebug === true) { SIPml.startNativeDebug(); } } } /** * 初始化完成 * @returns */ postInit() { // check for WebRTC support if (!SIPml.isWebRtcSupported()) { // is it chrome? if (SIPml.getNavigatorFriendlyName() == "chrome") { if ( confirm( "You're using an old Chrome version or WebRTC is not enabled.\nDo you want to see how to enable WebRTC?" ) ) { window.location = "http://www.webrtc.org/running-the-demos"; } else { window.location = "index.html"; } return; } else { if ( confirm( "webrtc-everywhere extension is not installed. Do you want to install it?\nIMPORTANT: You must restart your browser after the installation." ) ) { window.location = "https://github.com/sarandogou/webrtc-everywhere"; } else { // Must do nothing: give the user the chance to accept the extension // window.location = "index.html"; } } } // checks for WebSocket support if (!SIPml.isWebSocketSupported()) { if ( confirm( "Your browser don't support WebSockets.\nDo you want to download a WebSocket-capable browser?" ) ) { window.location = "https://www.google.com/intl/en/chrome/browser/"; } else { window.location = "index.html"; } return; } if (!SIPml.isWebRtcSupported()) { if ( confirm( "Your browser don't support WebRTC.\naudio/video calls will be disabled.\nDo you want to download a WebRTC-capable browser?" ) ) { window.location = "https://www.google.com/intl/en/chrome/browser/"; } } this.status = "initialized"; this.emit("initialized"); } /** * sends SIP REGISTER request to login * @param {*} displayName * @returns */ sipRegister({ displayName = "", privateIdentity = "", publicIdentity = "", password = "", realm = "", websocketProxyUrl = "wss://rtc.95ykf.com:4443", enableRtcwebBreaker = false, }) { if (this.status === "uninitialized") { Log.error("RtcPhone uninitialized!"); return false; } if (this.sipSessionRegister) { Log.error("RtcPhone Registered!"); return false; } // catch exception for IE (DOM not ready) try { let _impu = tsip_uri.prototype.Parse(publicIdentity); if (!_impu || !_impu.s_user_name || !_impu.s_host) { throw "Public Identity format error !"; } this.realm = realm; this.displayName = displayName; this.privateIdentity = privateIdentity; this.publicIdentity = publicIdentity; this.websocketProxyUrl = websocketProxyUrl; this.enableRtcwebBreaker = enableRtcwebBreaker; // enable notifications if not already done if ( window.webkitNotifications && window.webkitNotifications.checkPermission() != 0 ) { window.webkitNotifications.requestPermission(); } // update debug level to be sure new values will be used if the user haven't updated the page // TODO: 对外提供函数,不在单独设置 SIPml.setDebugLevel( window.localStorage && window.localStorage.getItem("org.doubango.expert.disable_debug") == "true" ? "error" : "info" ); // create SIP stack this.sipStack = new SIPml.Stack({ realm: this.realm, impi: this.privateIdentity, impu: this.publicIdentity, password: password, display_name: this.displayName, websocket_proxy_url: this.websocketProxyUrl, outbound_proxy_url: window.localStorage ? window.localStorage.getItem( "org.doubango.expert.sip_outboundproxy_url" ) : null, ice_servers: [], enable_rtcweb_breaker: this.enableRtcwebBreaker, events_listener: { events: "*", listener: this.onSipEventStack.bind(this), }, enable_early_ims: window.localStorage ? window.localStorage.getItem( "org.doubango.expert.disable_early_ims" ) != "true" : true, // Must be true unless you're using a real IMS network enable_media_stream_cache: window.localStorage ? window.localStorage.getItem( "org.doubango.expert.enable_media_caching" ) == "true" : false, bandwidth: window.localStorage ? tsk_string_to_object( window.localStorage.getItem("org.doubango.expert.bandwidth") ) : null, // could be redefined a session-level video_size: window.localStorage ? tsk_string_to_object( window.localStorage.getItem("org.doubango.expert.video_size") ) : null, // could be redefined a session-level sip_headers: [ { name: "User-Agent", value: "HL-WebRTC" }, { name: "Organization", value: "HL95" }, ], }); // Failed to start the SIP stack if (this.sipStack.start() != 0) { this.emit("stackStartFailed"); } } catch (e) { this.emit("stackStartFailed", e); Log.error(e) } } // sends SIP REGISTER (expires=0) to logout sipUnRegister() { if (this.sipStack) { this.sipStack.stop(); // shutdown all sessions } } /** * makes a call (SIP INVITE) * @param {String} type 'call-audio', 'call-audiovideo', 'call-video', 'call-screenshare'. * @param {*} phoneNumber * @returns */ sipCall(type, phoneNumber) { if (!this.sipStack) { utils.showMessage("UnRegister!"); return false; } if (!this.sipSessionCall) { if (!utils.checkPhoneNumber(phoneNumber)) { utils.showMessage("Phone number format error !"); return false; } if (type == "call-screenshare") { if (!SIPml.isScreenShareSupported()) { utils.showMessage( "Screen sharing not supported. Are you using chrome 26+?" ); return false; } if (!location.protocol.match("https")) { utils.showMessage("Screen sharing requires https://."); this.sipUnRegister(); return false; } } if (window.localStorage) { this.configCall.bandwidth = tsk_string_to_object( window.localStorage.getItem("org.doubango.expert.bandwidth") ); // already defined at stack-level but redifined to use latest values this.configCall.video_size = tsk_string_to_object( window.localStorage.getItem("org.doubango.expert.video_size") ); // already defined at stack-level but redifined to use latest values } // create call session this.sipSessionCall = this.sipStack.newSession(type, this.configCall); // make call if (this.sipSessionCall.call(phoneNumber) != 0) { this.sipSessionCall = null; this.emit("callFailed"); return; } } else { // this.callStatus = 'Connecting'; this.sipSessionCall.accept(this.configCall); } } sipAnswer() { if (!this.sipStack) { utils.showMessage("UnRegister!"); return false; } if (!this.sipSessionCall) { utils.showMessage("no call!"); return false; } else { this.sipSessionCall.accept(this.configCall); } } // Share entire desktop aor application using BFCP or WebRTC native implementation sipShareScreen() { if (SIPml.getWebRtcType() === "w4a") { // Sharing using BFCP -> requires an active session if (!this.sipSessionCall) { utils.showMessage("No active session"); return false; } if (this.sipSessionCall.bfcpSharing) { if (this.sipSessionCall.stopBfcpShare(this.configCall) != 0) { txtCallStatus.value = "Failed to stop BFCP share"; } else { this.sipSessionCall.bfcpSharing = false; } } else { this.configCall.screencast_window_id = 0x00000000; if (this.sipSessionCall.startBfcpShare(this.configCall) != 0) { txtCallStatus.value = "Failed to start BFCP share"; } else { this.sipSessionCall.bfcpSharing = true; } } } else { sipCall("call-screenshare"); } } /** * transfers the call * @param {String} phoneNumber * @returns */ sipTransfer(phoneNumber) { if (!this.sipSessionCall) { return false; } if (!utils.checkPhoneNumber(phoneNumber)) { return false; } let ret = this.sipSessionCall.transfer(phoneNumber); if (ret != 0) { Log.error('Call transfer failed'); return false; } Log.info('Transfering the call...'); return true; } // holds or resumes the call sipToggleHoldResume() { if (!this.sipSessionCall) { return false; } let ret = this.sipSessionCall.bHeld ? this.sipSessionCall.resume() : this.sipSessionCall.hold(); if (ret != 0) { return false; } return true; } // Mute or Unmute the call sipToggleMute() { if (!this.sipSessionCall) { return false; } let bMute = !this.sipSessionCall.bMute; let ret = this.sipSessionCall.mute("audio" /*could be 'video'*/, bMute); if (ret != 0) { return false; } this.sipSessionCall.bMute = bMute; return true; } // terminates the call (SIP BYE or CANCEL) sipHangup() { if (this.sipSessionCall) { this.sipSessionCall.hangup({ events_listener: { events: "*", listener: this.onSipEventSession.bind(this), }, }); } } /** * * @param {String} c 按键 */ sipSendDTMF(c) { if (this.sipSessionCall && c) { if (this.sipSessionCall.dtmf(c) == 0) { Log.info(`sipSendDTMF ${c}`); } } } /** * 播放铃声 */ startRingTone() { try { if (this.ringTone) { this.ringTone.play(); } } catch (e) { Log.error("铃声播放失败", e) } } stopRingTone() { try { if (this.ringTone) { this.ringTone.pause(); } } catch (e) { Log.error("停止铃声播放失败", e) } } /** * 播放回铃音 */ startRingbackTone() { try { if (this.ringbackTone) { this.ringbackTone.play(); } } catch (e) { Log.error("播放回铃音失败", e) } } stopRingbackTone() { try { if (this.ringbackTone) { this.ringbackTone.pause(); } } catch (e) { Log.error("停止播放回铃音失败", e) } } // Callback function for SIP Stacks onSipEventStack(e /*SIPml.Stack.Event*/) { Log.info("==stack event = " + e.type, e); switch (e.type) { case "started": { // catch exception for IE (DOM not ready) try { // LogIn (REGISTER) as soon as the stack finish starting this.sipSessionRegister = this.sipStack.newSession("register", { expires: 200, events_listener: { events: "*", listener: this.onSipEventSession.bind(this), }, sip_caps: [ { name: "+g.oma.sip-im", value: null }, //{ name: '+sip.ice' }, // rfc5768: FIXME doesn't work with Polycom TelePresence { name: "+audio", value: null }, { name: "language", value: '"en,fr"' }, ], }); this.sipSessionRegister.register(); } catch (e) { Log.error("注册失败", e); } break; } case "stopping": case "stopped": case "failed_to_start": case "failed_to_stop": { let bFailure = e.type == "failed_to_start" || e.type == "failed_to_stop"; this.sipStack = null; this.sipSessionRegister = null; this.sipSessionCall = null; this.sipSessionTransferCallByCti = null; this.emit("disconnected"); this.stopRingbackTone(); this.stopRingTone(); // uiVideoDisplayShowHide(false); TODO: 是否加时间考虑 Log.info(bFailure ? "Disconnected: " + e.description : "Disconnected"); break; } case "i_new_call": { Log.info("==i_new_call_log = ", this.sipSessionCall); let ao_headers = e.o_event.get_message().ao_headers; let callInfo = ao_headers.find((h) => h.s_name === "Call-Info"); if (this.sipSessionCall) { // 如果已经存在通话,新增通话时为转接振铃,需自动接听 if (callInfo && callInfo.s_value === "answer-after=0") { this.sipSessionTransferCallByCti = e.newSession; Log.info("CTI转接呼叫自动接听"); this.sipSessionTransferCallByCti.accept(this.configCall); } else { // do not accept the incoming call if we're already 'in call' e.newSession.hangup(); // comment this line for multi-line support } } else { this.sipSessionCall = e.newSession; // 自动接听 if (callInfo && callInfo.s_value === "answer-after=0") { Log.info("CTI发起外呼自动接听"); this.sipSessionCall.accept(this.configCall); } else { // start listening for events this.sipSessionCall.setConfiguration(this.configCall); this.callStatus = "ringing"; this.startRingTone(); let remoteNumber = this.sipSessionCall.getRemoteFriendlyName() || "unknown"; Log.info("Incoming call from " + remoteNumber, this.sipSessionCall); this.emit("ringing", remoteNumber); } } break; } case "m_permission_requested": { Log.log("=======m_permission_requested=====" + e); break; } case "m_permission_accepted": case "m_permission_refused": { Log.log("=======m_permission_accepted=m_permission_refused11=====" + e); break; } case "starting": default: break; } } /** * * @param {SIPml.Session.Event} e SIPml.Session.Event */ onSipEventSession(e) { Log.info("==session event = " + e.type, e); switch (e.type) { case "connecting": { if (e.session == this.sipSessionRegister) { this.status = e.type; this.emit(e.type, e); } else if (e.session == this.sipSessionCall) { this.emit("session", e); } break; } // 'connecting' case "connected": { if (e.session == this.sipSessionRegister) { this.status = e.type; this.emit(e.type, e); } else if (e.session == this.sipSessionCall) { this.stopRingbackTone(); this.stopRingTone(); this.callStatus = "talking"; this.emit("talking", e); } break; } // 'connected' case "terminating": case "terminated": { if (e.session == this.sipSessionRegister) { this.status = e.type; this.sipSessionCall = null; this.sipSessionTransferCallByCti = null; this.sipSessionRegister = null; this.emit("disconnected"); } else if (e.session == this.sipSessionCall) { this.sipSessionCall = null; this.stopRingbackTone(); this.stopRingTone(); this.emit("released", e); } else if (e.session == this.sipSessionTransferCallByCti) { this.sipSessionTransferCallByCti = null; } break; } // 'terminating' | 'terminated' case "m_stream_video_local_added": { if (e.session == this.sipSessionCall) { this.emit("localVideoAdded"); } break; } case "m_stream_video_local_removed": { if (e.session == this.sipSessionCall) { this.emit("localVideoRemoved"); } break; } case "m_stream_video_remote_added": { if (e.session == this.sipSessionCall) { //uiVideoDisplayEvent(false, true); } break; } case "m_stream_video_remote_removed": { if (e.session == this.sipSessionCall) { //uiVideoDisplayEvent(false, false); } break; } case "m_stream_audio_local_added": case "m_stream_audio_local_removed": case "m_stream_audio_remote_added": case "m_stream_audio_remote_removed": { break; } case "i_ect_new_call": { oSipSessionTransferCall = e.session; break; } case "i_ao_request": { if (e.session == this.sipSessionCall) { let iSipResponseCode = e.getSipResponseCode(); if (iSipResponseCode == 180 || iSipResponseCode == 183) { this.callStatus = "dialing"; // FIXME: 使用对方铃声,呼叫开始阶段会有一段没有铃声 // this.startRingbackTone(); this.emit("dialing", e); Log.info("Remote ringing..."); } } break; } case "m_early_media": { if (e.session == this.sipSessionCall) { this.stopRingbackTone(); this.stopRingTone(); Log.info("Early media started"); } break; } case "m_local_hold_ok": { if (e.session == this.sipSessionCall) { if (this.sipSessionCall.bTransfering) { this.sipSessionCall.bTransfering = false; // this.AVSession.TransferCall(this.transferUri); } this.emit("holdCompleted", e); Log.info("Call placed on hold"); this.sipSessionCall.bHeld = true; } break; } case "m_local_hold_nok": { if (e.session == this.sipSessionCall) { this.sipSessionCall.bTransfering = false; this.emit("holdFailed", e); Log.info("Failed to place remote party on hold"); } break; } case "m_local_resume_ok": { if (e.session == this.sipSessionCall) { this.sipSessionCall.bTransfering = false; this.emit("resumeCompleted", e); Log.info("Call taken off hold"); this.sipSessionCall.bHeld = false; if (SIPml.isWebRtc4AllSupported()) { // IE don't provide stream callback yet //uiVideoDisplayEvent(false, true); //uiVideoDisplayEvent(true, true); } } break; } case "m_local_resume_nok": { if (e.session == this.sipSessionCall) { this.sipSessionCall.bTransfering = false; this.emit("resumeFailed", e); Log.info("Failed to unhold call"); } break; } case "m_remote_hold": { if (e.session == this.sipSessionCall) { Log.info("Placed on hold by remote party"); } break; } case "m_remote_resume": { if (e.session == this.sipSessionCall) { Log.info("Taken off hold by remote party"); } break; } case "m_bfcp_info": { if (e.session == this.sipSessionCall) { txtCallStatus.innerHTML = "BFCP Info: <i>" + e.description + "</i>"; } break; } case "o_ect_trying": { if (e.session == this.sipSessionCall) { Log.info("Call transfer in progress..."); } break; } case "o_ect_accepted": { if (e.session == this.sipSessionCall) { Log.info("Call transfer accepted"); } break; } case "o_ect_completed": case "i_ect_completed": { if (e.session == this.sipSessionCall) { Log.info("Call transfer completed"); if (oSipSessionTransferCall) { this.sipSessionCall = oSipSessionTransferCall; } oSipSessionTransferCall = null; this.emit("transferCompleted", e); } break; } case "o_ect_failed": case "i_ect_failed": { if (e.session == this.sipSessionCall) { Log.info("Call transfer failed"); this.emit("transferFailed", e); } break; } case "o_ect_notify": case "i_ect_notify": { if (e.session == this.sipSessionCall) { txtCallStatus.innerHTML = "<i>Call Transfer: <b>" + e.getSipResponseCode() + " " + e.description + "</b></i>"; if (e.getSipResponseCode() >= 300) { if (this.sipSessionCall.bHeld) { this.sipSessionCall.resume(); } btnTransfer.disabled = false; } } break; } case "i_ect_requested": { if (e.session == this.sipSessionCall) { let s_message = "Do you accept call transfer to [" + e.getTransferDestinationFriendlyName() + "]?"; //FIXME: if (confirm(s_message)) { txtCallStatus.innerHTML = "<i>Call transfer in progress...</i>"; this.sipSessionCall.acceptTransfer(); break; } this.sipSessionCall.rejectTransfer(); } break; } } } } RtcPhone.utils = utils; RtcPhone.Log = Log; export default RtcPhone;