# JsSIP 從入門到實戰:構建你的第一個 Web 電話

前言

歡迎來到實時通信(Real-Time Communication, RTC)的世界!如果你是一名 JavaScript 開發者,渴望讓你的 Web 應用擁有語音通話、視頻聊天甚至即時消息的能力,那么你來對地方了。這本書是為你量身打造的指南,它將帶領你從零開始,一步步掌握強大的 JsSIP 庫,最終構建出一個功能完善的網頁電話(Web Softphone)。

我們假設你只熟悉 JavaScript,對 SIP、WebRTC 這些復雜的通信協議一無所知。這完全沒問題!本書的設計初衷就是“由淺入深”。我們將首先為你揭開通信協議的神秘面紗,用通俗易懂的語言和生動的比喻,讓你理解電話是如何在互聯網上“打通”的。然后,我們將深入 JsSIP 的世界,從第一個 “Hello, World!” 程序開始,到處理復雜的通話控制,再到詳盡的 API 解析和排錯技巧。

本書不僅僅是 API 的羅列,更是一本實踐手冊。每一章都建立在前一章的基礎上,理論與代碼緊密結合。在本書的最后,我們將整合所有知識,從界面設計到核心邏輯,手把手帶你構建一個完整的、可以實際運行的 Web 電話項目。

準備好了嗎?讓我們一起開啟這段激動人心的旅程,讓瀏覽器真正地“開口說話”!


第一部分:理論基石

在編寫任何代碼之前,理解其背后的原理至關重要。這一部分將為你鋪設堅實的理論基礎。不理解 JsSIP 正在為你處理什么,你就無法真正地駕馭它。我們將一起揭開實時通信背后的“魔法”,讓你不僅知其然,更知其所以然。

第一章:通信世界的基石——SIP 協議入門

想象一下,你想給朋友打個電話。你需要先撥號,等待對方接聽,通話結束后再掛斷。在互聯網世界里,完成這一系列“握手”和“告別”動作的規則,就是 SIP 協議。你可以把它理解為通信世界的 “HTTP”,它不是用來傳輸通話內容的,而是用來建立、管理和結束通話這個“會話”的信令語言。

什么是 SIP?

SIP 的全稱是 會話發起協議(Session Initiation Protocol)。它是一個在應用層工作的信令協議,其核心任務是發起、維持和終止實時的通信會話 1。這些會話可以包含語音、視頻、即時消息等多種媒體形式。

與你可能熟悉的 HTTP 或 SMTP 協議類似,SIP 也是一個基于文本的協議 1。這意味著它的消息是人類可讀的,這在調試時非常方便。SIP 由互聯網工程任務組(IETF)在著名的

RFC 3261 文檔中進行了標準化,并已成為現代網絡電話(VoIP)和實時通信領域的行業標準,廣泛取代了像 H.323 這樣的早期協議 3。

SIP 架構的核心組件

一個典型的 SIP 網絡由多個組件協同工作,就像一個電話系統需要有電話機、接線員和電話簿一樣。

  • 用戶代理 (User Agents - UA)

    這是 SIP 世界的終端設備,比如你的電腦上運行的軟電話,或者一個 IP 電話機。一個用戶代理具有雙重身份 1:

    • 用戶代理客戶端 (User Agent Client - UAC): 當你發起一個呼叫時,你的設備就扮演了 UAC 的角色,它會發送 SIP 請求。

    • 用戶代理服務器 (User Agent Server - UAS): 當你接收一個來電時,你的設備則扮演 UAS 的角色,它會響應這個 SIP 請求。

      在 JsSIP 中,你將要創建和操作的核心對象 JsSIP.UA,就是這個概念的軟件實現。

  • 服務器 (Servers)

    服務器在 SIP 網絡中扮演著關鍵的中間人角色。

    • 代理服務器 (Proxy Server): 這是最核心的服務器,就像一個智能的電話接線員。它接收來自 UAC 的請求,然后根據一定的規則將請求路由(轉發)到下一個目的地 2。
    • 注冊服務器 (Registrar Server): 這個服務器像一本“地址簿”。當你啟動軟電話并登錄時,它會發送一個 REGISTER 請求到注冊服務器。注冊服務器會記錄下你的 SIP 地址(例如 sip:alice@example.com)和你當前的實際網絡位置(IP 地址和端口)之間的映射關系。這樣,當別人呼叫你時,代理服務器就能通過查詢注冊服務器找到你 2。
    • 重定向服務器 (Redirect Server): 這個服務器比較特殊,它不轉發請求,而是直接告訴 UAC:“你應該去聯系這個地址”,讓 UAC 自己去發起新的請求 2。
SIP 消息與方法

SIP 遵循一個簡單的請求/響應模型,與 HTTP 非常相似 1。UAC 發送一個請求,UAS 回復一個或多個響應。以下是一些最核心的 SIP 方法(可以理解為命令),以及它們的通俗解釋 3:

  • INVITE: “我想和你通話。” 這是發起一個呼叫的請求。
  • ACK: “我收到你的確認了,連接正式建立。” 這是對成功響應(2xx)的確認。
  • BYE: “再見,掛斷電話。” 這是用來終止一個已建立的通話。
  • CANCEL: “算了,我不想打了。” 在對方還未接聽時,用來取消之前的 INVITE 請求。
  • REGISTER: “嗨,服務器,我上線了,我的地址是這個。” 這是向注冊服務器登記自己的位置。
一個基本的 SIP 呼叫流程

理解一個完整的呼叫流程至關重要,因為它直接映射到你之后將要處理的 JsSIP 事件。讓我們看看從 Alice 呼叫 Bob 的過程中,SIP 消息是如何流轉的 3。

!(https://i.imgur.com/qg9bYyH.png)

  1. 發起呼叫 (INVITE): Alice 的軟電話(UAC)向代理服務器發送一個 INVITE 請求,請求呼叫 Bob (sip:bob@example.com)。
  2. 嘗試連接 (100 Trying): 代理服務器收到 INVITE 后,會立即回復一個 100 Trying 響應,告訴 Alice:“我收到了,正在處理,請不要重復發送 INVITE。”
  3. 路由與振鈴 (INVITE & 180 Ringing): 代理服務器查詢注冊服務器,找到了 Bob 的當前位置,并將 INVITE 請求轉發給 Bob 的軟電話。Bob 的電話收到后開始響鈴,并回復一個 180 Ringing 響應,這個響應會通過代理服務器傳回給 Alice。Alice 的軟電話收到后,就會播放“嘟…嘟…”的回鈴音。
  4. 接聽通話 (200 OK): Bob 點擊接聽。他的軟電話(現在是 UAS)發送一個 200 OK 響應,表示呼叫被成功接受。這個響應也會傳回給 Alice。
  5. 確認連接 (ACK): Alice 的軟電話收到 200 OK 后,知道對方已經接聽,于是發送一個 ACK 消息作為最終確認。這個 ACK 可能直接發送給 Bob,也可能通過代理。當 Bob 收到 ACK 后,一個完整的 SIP 會話(也稱為 Dialog)就建立成功了。
  6. 媒體傳輸 (RTP): 此時,SIP 的主要任務已經完成。雙方的音視頻數據開始通過另一個獨立的協議——實時傳輸協議 (Real-time Transport Protocol, RTP)——直接在 Alice 和 Bob 之間傳輸。這是一個非常關鍵的概念:SIP 只負責信令(建立和控制),不負責傳輸媒體本身 3。SIP 就像是安排兩位貴賓見面的禮賓司,而 RTP 則是運送貴賓的專車。
  7. 結束通話 (BYE): 通話結束后,任何一方(比如 Alice)都可以發送一個 BYE 請求來終止會話。
  8. 確認掛斷 (200 OK): 另一方(Bob)收到 BYE 后,回復一個 200 OK,確認通話結束。至此,整個會話生命周期完成。

理解了這個流程,你就能明白為什么在 JsSIP 中會有 progress(對應 180 Ringing)、accepted(對應 200 OK)、confirmed(對應 ACK)和 ended(對應 BYE)這些事件了。它們正是這個底層協議流程在 JavaScript 世界的映射。

第二章:讓瀏覽器開口說話——WebRTC 與信令

上一章我們了解了 SIP,這個負責建立和控制通信會話的“大腦”。但我們也提到了,SIP 本身不傳輸聲音和圖像。那么,在瀏覽器中,誰來扮演這個“運輸卡車”的角色呢?答案就是 WebRTC

WebRTC 簡介

WebRTC,全稱 Web Real-Time Communication,是一項革命性的技術。它是一套開放標準和 API,允許 Web 應用程序在不需要安裝任何額外插件(如 Flash 或 Java Applets)的情況下,直接在瀏覽器之間捕獲和流式傳輸音頻、視頻媒體,以及交換任意數據 9。

WebRTC 主要由以下幾個核心的 JavaScript API 組成 10:

  • getUserMedia: 這個 API 用于獲取用戶的媒體設備權限,比如請求訪問攝像頭和麥克風。這是所有音視頻通話的第一步。
  • RTCPeerConnection: 這是 WebRTC 的心臟。它負責在兩個瀏覽器(稱為“對等端”或 Peer)之間建立和管理一個高效、穩定的點對點(Peer-to-Peer, P2P)連接。它處理所有復雜的工作,如信號處理、編解碼器協商、安全加密和帶寬管理 9。
  • RTCDataChannel: 除了音視頻,WebRTC 還允許通過 RTCDataChannel 在對等端之間建立一個低延遲的雙向數據通道,可以用來傳輸聊天消息、游戲狀態、文件等任意數據 9。
WebRTC 的“缺失環節”:信令

WebRTC 非常強大,但它有一個“故意”的設計留白:它本身不包含**信令(Signaling)**機制 10。

想象一下,RTCPeerConnection 就像一部功能強大的對講機,但它沒有撥號盤,也不知道其他對講機的頻率。它不知道:

  • 要和誰通話?
  • 對方是否愿意通話?
  • 如何找到對方的網絡地址?
  • 雙方支持哪些音視頻格式(編解碼器)?

這些信息必須通過一個獨立于 WebRTC 的“帶外”機制來交換,這個過程就叫做信令。開發者可以使用任何技術來實現信令,比如 WebSocket、HTTP 請求,甚至是信鴿 13。信令服務器就像一個中間人或郵局,負責在兩個希望通話的瀏覽器之間傳遞“信件”。

信令過程主要交換三類信息 14:

  1. 會話控制消息: 用于初始化、關閉和修改通信會話,比如“我想打給你”或“我掛了”。
  2. 網絡配置信息: 比如對方的 IP 地址和端口,這樣瀏覽器才知道把媒體數據包發到哪里去。
  3. 媒體能力信息: 比如雙方各自支持哪些視頻編碼格式(H.264, VP8?)和音頻編碼格式(Opus, G.711?)。

這個交換過程通常遵循一個 Offer/Answer 模型。一方(呼叫方)創建一個包含其網絡和媒體信息的“Offer”(提議),通過信令服務器發送給另一方。另一方(被叫方)收到后,生成一個包含自己信息的“Answer”(應答),再通過信令服務器回傳給呼叫方。一旦交換完成,雙方就有了建立 P2P 連接所需的所有信息 13。這些 Offer 和 Answer 的格式,遵循一種叫做

SDP (Session Description Protocol) 的規范。

穿越網絡迷霧:ICE, STUN, 和 TURN

理論上,一旦雙方通過信令交換了 IP 地址,就可以直接建立 P2P 連接了。但現實網絡環境遠比這復雜。我們大多數人的設備都位于家庭或公司路由器后面,使用著一個叫做 NAT(網絡地址轉換) 的技術。這意味著我們的設備沒有一個公網 IP 地址,而是只有一個內網 IP 地址(比如 192.168.1.100),這在公網上是無法被直接訪問的。這就好比你住在一個大公寓樓里,你的地址是“XX 公寓 1802 房”,但郵遞員只知道“XX 公寓”這個大樓地址,不知道如何把信直接送到你的房門口。

為了解決這個問題,WebRTC 使用了一個名為 ICE (Interactive Connectivity Establishment) 的框架 14。ICE 的工作就是想盡一切辦法,為兩個處于 NAT 后面的設備找到一條可以通信的路徑。它主要使用兩種工具:STUN 和 TURN。

  • STUN (Session Traversal Utilities for NAT)

    STUN 服務器非常簡單,它就像一面放在公網上的鏡子。當你的設備向 STUN 服務器發送一個請求時,STUN 服務器會看到這個請求來自哪個公網 IP 地址和端口,然后把這個“公網地址”信息告訴你的設備。你的設備拿到這個地址后,就可以通過信令告訴對方:“嘿,你在公網上可以從這個地址找到我。” 17。在很多情況下,只要 NAT 類型不是太嚴格,STUN 就足以幫助雙方建立直接的 P2P 連接。

  • TURN (Traversal Using Relays around NAT)

    然而,在某些復雜的網絡環境下(比如“對稱型 NAT”或嚴格的企業防火墻),即使知道了對方的公網地址,也無法直接打通連接。這時,就需要 TURN 服務器作為最后的手段。

    TURN 服務器不再是“鏡子”,而是變成了一個“中繼站”或“郵政中轉中心”。當 P2P 直連失敗時,雙方都會把自己的媒體數據包發送給 TURN 服務器,然后由 TURN 服務器負責將數據包轉發給另一方 17。這種方式保證了連接的成功率,但缺點也很明顯:所有數據都要經過服務器中轉,這會增加延遲,并且極大地消耗服務器的帶寬和成本。因此,TURN 通常只在 P2P 直連失敗時作為備用方案。

!(https://i.imgur.com/k6t789c.png)

現在,我們可以將所有概念串聯起來了。JsSIP 的核心價值,正是為強大的 WebRTC 媒體引擎提供了一套成熟、標準化的 SIP 信令機制。它通過 SIP over WebSocket 的方式在瀏覽器和 SIP 服務器之間傳遞信令,交換 Offer/Answer 和 ICE 候選地址,最終配置好 RTCPeerConnection,讓音視頻數據在對等端之間奔跑起來。

當你使用 JsSIP 開發應用時,遇到最常見的問題之一可能就是“通話接通了,但聽不到對方聲音”。十有八九,這并不是 JsSIP 的 API 調用錯了,而是底層的 WebRTC 媒體路徑沒有成功建立。這通常是因為 ICE 過程失敗,特別是當通話雙方都處于復雜的 NAT 網絡后,而你又沒有在配置中提供一個可用的 TURN 服務器。因此,深刻理解本章介紹的信令、ICE、STUN 和 TURN 的概念,將為你后續的開發和排錯之路掃清最大的障礙。

第三章:連接 SIP 與 WebRTC 的橋梁——JsSIP 登場

前面兩章,我們分別了解了通信世界的兩大主角:負責信令的 SIP 和負責媒體傳輸的 WebRTC。現在,是時候請出我們的主角——JsSIP 了。JsSIP 正是那座精心搭建的橋梁,它將經典、強大的 SIP 協議引入現代瀏覽器,使其成為 WebRTC 的完美信令搭檔。

JsSIP 是什么?

JsSIP 是一個輕量級、功能強大且 100% 純 JavaScript 編寫的庫。它能讓你在任何網站中,僅用幾行代碼,就能構建出一個功能齊全的 SIP 終端(用戶代理),從而實現音視頻通話、即時消息等實時通信功能 20。

它的核心特性包括 20:

  • SIP over WebSocket: 這是 JsSIP 的基石,我們稍后會詳細解釋。
  • 音視頻通話與即時消息: 全面支持 WebRTC 的媒體能力和 SIP 的消息能力。
  • 輕量級: 庫文件大小經過優化(約 140KB),對頁面性能影響小。
  • 純 JavaScript: 無需任何瀏覽器插件,易于集成到現代前端開發流程中。
  • 易用且強大的 API: 提供了對開發者友好的高層 API,同時也保留了足夠的靈活性進行深度定制。
  • 專業背景: 該庫由 RFC 7118(“The WebSocket Protocol as a Transport for SIP” 標準文檔)的作者親自編寫,保證了其對 SIP 標準的深刻理解和精準實現。

值得一提的是,在 JavaScript 的 RTC 領域,除了 JsSIP,還有另一個流行的庫叫做 SIP.js 24。雖然它們的目標相似,都是為了在瀏覽器中實現 SIP 通信,但它們的 API 設計、社區和發展路線有所不同。本書將完全專注于

JsSIP,在學習和查閱資料時,請注意區分,避免將兩個庫的示例代碼和文檔混淆。

JsSIP 的架構:SIP over WebSocket

我們知道,傳統的 SIP 協議主要運行在 UDP 或 TCP 之上。但是,出于安全考慮,瀏覽器中的 JavaScript 無法直接創建和操作底層的 TCP/UDP 套接字。那么,JsSIP 是如何在瀏覽器里發送和接收 SIP 消息的呢?答案是 WebSocket

WebSocket 協議提供了一個在單個 TCP 連接上進行全雙工通信的通道。它就像一條在瀏覽器和服務器之間建立的持久化、雙向的“高速公路”。JsSIP 正是利用這條高速公路來傳輸 SIP 消息,這個技術被稱為 SIP over WebSocket 20。

其工作流程如下圖所示:

!(https://i.imgur.com/G3Cq35f.png)

  1. 你的 Web 應用(客戶端)使用 JsSIP 庫。
  2. JsSIP 通過瀏覽器內置的 WebSocket API,與一臺支持 SIP over WebSocket 的服務器建立連接。
  3. 所有的 SIP 信令(如 INVITE, REGISTER, BYE 等)都被打包成文本消息,通過這條 WebSocket 隧道發送到服務器。
  4. SIP 服務器(如 Kamailio, Asterisk)解開消息,像處理普通 SIP 請求一樣進行路由、認證等操作,并與其他 SIP 網絡(例如另一個 JsSIP 客戶端、一個物理 IP 電話,甚至是傳統的電話網絡 PSTN)進行交互。
  5. 來自其他 SIP 網絡的響應或新請求,也通過 SIP 服務器打包,沿著 WebSocket 隧道發回給你的 JsSIP客戶端。

這種架構的最大優勢在于,JsSIP 在瀏覽器中說的是“真正的 SIP” 20。它沒有對 SIP 協議進行任何刪減或轉換,這意味著你的 Web 應用可以無縫地融入龐大而成熟的現有 SIP 生態系統,與各種 SIP 設備和平臺進行互聯互通。

運行 JsSIP 的先決條件

理解 JsSIP 的架構后,一個至關重要的前提就浮出水面了:JsSIP 是一個純客戶端庫,它無法獨立工作,必須連接到一個支持 SIP over WebSocket 的后端服務器 21。

這個服務器是 JsSIP 應用的大腦和網關,負責處理用戶注冊、呼叫路由等所有信令邏輯。目前,許多主流的開源 SIP 服務器都已經支持 WebSocket,例如 20:

  • Kamailio
  • Asterisk
  • OverSIP
  • FreeSWITCH

如果你已經有了一個 SIP 服務提供商(比如公司的電話系統),你需要向他們咨詢 WebSocket 連接地址(通常以 ws://wss:// 開頭)以及你的 SIP 賬戶信息。如果你的現有 SIP 服務器不支持 WebSocket,也可以在其前端部署一個像 OverSIP 這樣的 WebSocket 代理服務器 21。

本書的目標是教會你如何使用 JsSIP 這個客戶端庫。我們不會深入講解如何從零開始搭建和配置一個 SIP 服務器,因為那本身就是另一個龐大而復雜的領域。在后續的示例中,我們將假設你已經擁有了必要的服務器連接信息。你可以使用一些公開的測試服務,或者從你的 VoIP 提供商處獲取。

現在,理論基礎已經牢固。從下一部分開始,我們將卷起袖子,真正開始編寫代碼,讓 JsSIP 在你的項目中運行起來!


第二部分:JsSIP 核心實踐

理論學習告一段落,現在是時候將知識轉化為代碼了。在這一部分,我們將從零開始,一步步構建你的第一個 JsSIP 應用。你將學會如何配置和啟動客戶端,如何發起和接聽電話,以及如何實現通話中常見的各種高級功能。讓我們一起進入 JsSIP 的核心實踐環節。

第四章:第一個 JsSIP 應用:Hello, World!

任何編程學習之旅都始于一個經典的 “Hello, World!”。在 JsSIP 的世界里,我們的 “Hello, World!” 就是成功地讓一個客戶端連接并注冊到 SIP 服務器。這標志著你的 Web 應用已經正式踏入了 SIP 通信網絡。

安裝 JsSIP

在開始編碼之前,首先需要將 JsSIP 庫引入到你的項目中。推薦使用 npm 進行安裝,這能更好地與現代前端工程化流程(如 Webpack, Vite)集成 23。

在你的項目目錄下,打開終端并運行:

Bash

npm install jssip

安裝完成后,你就可以在你的 JavaScript 文件中通過 importrequire 來使用它。如果你的項目沒有使用構建工具,也可以通過在 HTML 中直接引入 JsSIP 的發行版文件 23。

核心對象:JsSIP.UA

JsSIP 的一切操作都圍繞著它的核心對象——JsSIP.UA (User Agent) 展開。這個對象在代碼中代表了一個 SIP 客戶端,它與一個唯一的 SIP 賬戶相關聯 27。你可以把它想象成你的軟電話實例。

配置你的用戶代理

要創建一個 UA 實例,你必須提供一個配置對象。這個對象告訴 JsSIP 如何連接服務器以及使用哪個身份。以下是一個最基礎的配置示例 27:

JavaScript

// 首先,導入 JsSIP 庫
import * as JsSIP from 'jssip';// 1. 定義 WebSocket 連接接口
const socket = new JsSIP.WebSocketInterface('wss://sip.myhost.com');// 2. 創建配置對象
const configuration = {sockets: [socket],uri: 'sip:alice@example.com',password: 'superpassword'
};

讓我們來逐一解析這三個必填的配置參數 27:

  • sockets: 這是一個數組,用于定義一個或多個 WebSocket 連接。數組中的每一項都必須是一個 JsSIP.WebSocketInterface 的實例。你需要在實例化 WebSocketInterface 時傳入你的 SIP 服務器的 WebSocket 地址(以 wss://ws:// 開頭)。之所以設計成數組,是為了實現連接的高可用性。你可以提供多個服務器地址,當第一個連接失敗時,JsSIP 會自動嘗試下一個,從而實現故障轉移。
  • uri: 這是一個字符串,代表你的 SIP 地址(也稱為 SIP URI)。它通常由你的 SIP 服務提供商分配,格式類似于一個電子郵件地址。
  • password: 這是一個字符串,即你的 SIP 賬戶的認證密碼。
啟動與停止

有了配置對象,我們就可以實例化并啟動 UA 了:

JavaScript

// 3. 實例化 UA
const ua = new JsSIP.UA(configuration);// 4. 啟動 UA
ua.start();
  • new JsSIP.UA(configuration) 創建了一個用戶代理實例。
  • ua.start() 是一個至關重要的方法。調用它之后,JsSIP 會開始嘗試連接到你在 sockets 中指定的 WebSocket 服務器。連接成功后,如果配置中沒有禁用自動注冊,它還會自動發送 REGISTER 請求到 SIP 服務器,以宣告自己的在線狀態 27。

start() 對應的是 ua.stop() 方法。調用 ua.stop() 會讓 JsSIP 優雅地關閉:它會先向服務器發送注銷請求,然后終止所有活動會話,最后斷開 WebSocket 連接 29。

監聽生命周期事件

UA 在其生命周期中會經歷多種狀態變化(連接中、已連接、注冊成功、注冊失敗等)。JsSIP 通過一個事件系統來通知我們這些變化。我們可以使用 ua.on('eventName', callback) 的方式來監聽這些事件 30。

讓我們為 UA 的關鍵生命周期事件添加監聽器,以便實時了解其狀態 27:

JavaScript

ua.on('connecting', () => {console.log('UA 正在連接...');
});ua.on('connected', () => {console.log('UA 已連接!');
});ua.on('disconnected', () => {console.log('UA 已斷開連接。');
});ua.on('registered', () => {console.log('UA 注冊成功!');// 在這里,你的軟電話已經準備好撥打和接聽電話了
});ua.on('unregistered', () => {console.log('UA 已注銷。');
});ua.on('registrationFailed', (e) => {console.error('UA 注冊失敗!原因:', e.cause);// `e.cause` 提供了失敗的具體原因,例如 'Authentication Error'
});// 最后,別忘了啟動 UA
ua.start();

在上面的代碼中,我們為 UA 的主要狀態轉換都注冊了回調函數。當 UA 啟動后,你將在瀏覽器的控制臺中看到它狀態變化的實時日志。特別是 registered 事件,它的觸發標志著你的軟電話已經完全就緒。而如果 registrationFailed 事件被觸發,你可以通過檢查事件對象 e 中的 cause 屬性來診斷問題,這對于排錯至關重要 31。

至此,你已經完成了 JsSIP 的 “Hello, World!”。你的代碼已經能夠與 SIP 服務器建立連接并完成身份認證。這是構建更復雜功能的第一步,也是最重要的一步。

第五章:撥打與接聽:實現音視頻通話

成功注冊到 SIP 網絡后,我們的軟電話就擁有了“身份證”,現在是時候讓它發揮核心作用——進行音視頻通話了。本章將深入探討 JsSIP 中最激動人心的部分:如何發起呼叫、如何處理來電,以及如何將真實的音視頻流渲染到網頁上。

發起呼叫 (ua.call())

要發起一個呼叫,我們使用 ua.call(target, options) 方法 29。

  • target: 字符串類型,代表你希望呼叫的對象。它可以是一個簡單的用戶名(如 'bob'),JsSIP 會根據你的 uri 自動補全域名;也可以是一個完整的 SIP URI(如 'sip:bob@example.com')。

  • options: 這是一個非常重要的配置對象,它能讓你精細地控制這次呼叫。以下是幾個關鍵的選項:

    • mediaConstraints: 一個對象,用于指定你希望使用的媒體類型。例如,{ audio: true, video: true } 表示你想發起一個音視頻通話。如果只想進行音頻通話,可以設置為 { audio: true, video: false } 29。

    • pcConfig: 這個對象用于直接配置底層的 RTCPeerConnection。最重要的用途就是在這里提供 STUN 和 TURN 服務器的地址,以幫助 WebRTC 進行 NAT 穿透 34。例如:

      JavaScript

      const pcConfig = {iceServers: [{ urls: 'stun:stun.l.google.com:19302' },{ urls: 'turn:my.turn.server.com:3478',username: 'turn_user',credential: 'turn_password'}]
      };
      
    • eventHandlers: 一個事件處理器對象。你可以為這次呼叫的會話(RTCSession)預先注冊好事件監聽器,而無需等待會話創建后再綁定。這是一種非常便捷的編碼方式 29。

下面是一個發起視頻通話的完整示例:

JavaScript

const target = 'sip:bob@example.com';const options = {mediaConstraints: { audio: true, video: true },pcConfig: {iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]},eventHandlers: {progress: (e) => { console.log('呼叫進行中...'); },failed: (e) => { console.log(`呼叫失敗: ${e.cause}`); },ended: (e) => { console.log('呼叫結束。'); },confirmed: (e) => { console.log('呼叫已接通!'); }}
};// 發起呼叫
ua.call(target, options);
處理來電 (newRTCSession 事件)

無論是你發起的呼叫(去電),還是別人打給你的呼叫(來電),都會觸發 UA 實例上的 newRTCSession 事件。這個事件是所有通話的統一入口點 27。

JavaScript

let currentSession = null;ua.on('newRTCSession', (data) => {console.log('新的 RTC 會話已創建');// 保存會話對象currentSession = data.session;// 監聽會話的各種事件setupSessionEventHandlers(currentSession);if (currentSession.direction === 'incoming') {console.log('這是一個來電,來自:', currentSession.remote_identity.uri.toString());// 需要在這里處理UI,比如彈出一個接聽/掛斷的窗口} else if (currentSession.direction === 'outgoing') {console.log('這是一個去電,目標是:', currentSession.remote_identity.uri.toString());}
});

newRTCSession 事件的回調函數接收一個 data 對象,其中包含三個關鍵屬性 36:

  • originator: 字符串 'local''remote',指明會話是由本地發起還是由遠端發起。
  • session: 核心的 JsSIP.RTCSession 對象實例。這是我們管理這次特定通話的句柄。
  • request: 原始的 SIP INVITE 請求對象。

通過檢查 session.direction 屬性(值為 'incoming''outgoing'),我們可以輕松地區分來電和去電,并執行不同的邏輯 34。

管理通話會話 (JsSIP.RTCSession)

RTCSession 對象代表了一個獨立的通話會話。它擁有一系列方法和事件,用于管理通話的全過程。

  • 接聽來電: 對于一個來電會話(direction === 'incoming'),你需要調用 session.answer(options) 方法來接聽電話。options 參數與 ua.call() 中的類似,你可以在這里指定媒體約束等 34。

    JavaScript

    // 假設用戶點擊了“接聽”按鈕
    function answerCall() {if (currentSession && currentSession.direction === 'incoming') {const answerOptions = {mediaConstraints: { audio: true, video: true }};currentSession.answer(answerOptions);}
    }
    
  • 掛斷通話: 無論通話處于何種狀態(正在呼叫、已接通、來電振鈴中),你都可以調用 session.terminate() 來結束或拒絕這次通話 38。

    JavaScript

    // 假設用戶點擊了“掛斷”按鈕
    function terminateCall() {if (currentSession) {currentSession.terminate();}
    }
    
  • 獲取通話信息: 你可以從 session 對象上獲取對方的身份信息,這對于在 UI 上顯示非常有用 38。

    • session.remote_identity.display_name: 對方的顯示名(昵稱)。
    • session.remote_identity.uri: 對方的完整 SIP URI。
渲染音視頻流

這是將通話“可視化”的關鍵一步。我們需要從 RTCPeerConnection 中捕獲到遠程的媒體流,并將其附加到 HTML 的 <video> 元素上。

一個常見的誤區是試圖在 newRTCSession 事件觸發后立即獲取媒體流。此時,對于來電而言,RTCPeerConnection 可能還未建立。正確的做法是監聽 RTCSession 上的 peerconnection 事件,這個事件在底層的 RTCPeerConnection 實例被創建后觸發。然后,我們再在該 peerconnection 對象上監聽 track 事件 41。

JavaScript

function setupSessionEventHandlers(session) {//... 其他事件監聽器,如 ended, failed...session.on('peerconnection', (data) => {console.log('RTCPeerConnection 已創建');const peerconnection = data.peerconnection;peerconnection.addEventListener('track', (event) => {console.log('接收到遠程媒體軌道');const remoteStream = event.streams;const remoteVideo = document.getElementById('remoteVideo');// 將遠程視頻流附加到 video 元素if (remoteVideo) {remoteVideo.srcObject = remoteStream;}});});// 同時,我們也需要獲取并顯示本地視頻流session.on('accepted', () => {console.log('通話被接受,顯示本地視頻');const localStream = session.connection.getLocalStreams();const localVideo = document.getElementById('localVideo');if (localVideo && localStream) {localVideo.srcObject = localStream;}});
}
  • peerconnection 事件: 在這個事件的回調中,我們可以安全地訪問 data.peerconnection 對象。
  • track 事件: 當遠程對等端添加媒體軌道時,此事件被觸發。event.streams 就是我們需要的遠程 MediaStream 對象。
  • 附加到 <video> 元素: 我們通過 videoElement.srcObject = stream; 的方式將媒體流賦給視頻標簽。確保你的 HTML 中有 <video id="remoteVideo" autoplay></video> 這樣的元素,autoplay 屬性可以讓視頻自動播放 34。
  • 本地視頻流: 你可以通過 session.connection.getLocalStreams() 獲取本地的媒體流,并將其顯示在另一個 <video> 元素中,作為本地預覽 39。

通過本章的學習,你已經掌握了 JsSIP 最核心的通話功能。但一個優秀的軟電話還需要更多交互能力,下一章我們將學習如何在通話中實現靜音、保持等高級操作。

第六章:通話中的高級操作

一個基本的通話功能已經實現,但要構建一個用戶體驗良好的軟電話,還需要提供一些通話中常用的控制功能。本章將教你如何使用 JsSIP 來實現通話的靜音、保持以及發送電話按鍵音(DTMF)。

靜音與取消靜音 (Mute and Unmute)

在通話中讓對方聽不到自己的聲音,是一個非常基礎且必要的功能。JsSIP 提供了簡潔的 API 來實現這一點。

  • 方法:
    • session.mute(options): 將通話靜音。
    • session.unmute(options): 取消靜音。
    • session.isMuted(): 返回一個布爾值,告訴你當前是否處于靜音狀態 43。

options 參數可以指定只靜音音頻或視頻,例如 session.mute({ audio: true, video: false })。如果不傳,則默認同時靜音音視頻。

  • 事件:
    • muted: 當通話被靜音時觸發。
    • unmuted: 當通話取消靜音時觸發 44。

你應該監聽這些事件來更新你的 UI,例如改變靜音按鈕的圖標或狀態。

  • 實現原理:

    調用 mute() 或 unmute() 并非只是一個簡單的本地操作。它實際上是通過操作本地 MediaStreamTrack 的 enabled 屬性來停止或恢復發送媒體數據。在某些配置下,JsSIP 還可能會通過發送一個更新后的 SIP 請求(如 re-INVITE 或 UPDATE)來通知對方媒體流的狀態發生了變化。理解這一點很重要,因為它解釋了為什么這些操作是異步的。

示例代碼:

JavaScript

// HTML 中有一個 id 為 'muteButton' 的按鈕
const muteButton = document.getElementById('muteButton');muteButton.addEventListener('click', () => {if (currentSession && currentSession.isEstablished()) {if (currentSession.isMuted().audio) {currentSession.unmute({ audio: true });} else {currentSession.mute({ audio: true });}}
});// 在會話事件處理器中更新 UI
function setupSessionEventHandlers(session) {//...session.on('muted', () => {console.log('通話已靜音');muteButton.textContent = '取消靜音';});session.on('unmuted', () => {console.log('通話已取消靜音');muteButton.textContent = '靜音';});//...
}
通話保持與恢復 (Hold and Unhold)

通話保持功能允許用戶暫時中斷與一個人的通話(例如,去接聽另一個電話),而不會掛斷當前通話。

  • 方法:

    • session.hold(): 將通話置于保持狀態。
    • session.unhold(): 從保持狀態中恢復通話。
    • session.isOnHold(): 返回一個對象,告訴你本地和遠程的保持狀態 43。
  • 事件:

    • hold: 當通話被任一方置于保持狀態時觸發。
    • unhold: 當通話從保持狀態恢復時觸發 43。
  • 實現原理:

    通話保持是一個純粹的信令層操作。當調用 session.hold() 時,JsSIP 會構造一個新的 SDP(會話描述協議)內容,在其中將媒體流的方向屬性標記為 sendonly(只發送,不接收)或 inactive(不發送也不接收)。然后,它會通過一個 re-INVITE 或 UPDATE 請求將這個新的 SDP 發送給對方。對方收到并同意后,雙方的客戶端就會停止處理媒體流,從而實現“保持”的效果 47。這個過程被稱為“會話重新協商”(re-negotiation)。

示例代碼:

JavaScript

// HTML 中有一個 id 為 'holdButton' 的按鈕
const holdButton = document.getElementById('holdButton');holdButton.addEventListener('click', () => {if (currentSession && currentSession.isEstablished()) {if (currentSession.isOnHold().local) {currentSession.unhold();} else {currentSession.hold();}}
});// 在會話事件處理器中更新 UI
function setupSessionEventHandlers(session) {//...session.on('hold', () => {console.log('通話已保持');holdButton.textContent = '恢復通話';});session.on('unhold', () => {console.log('通話已恢復');holdButton.textContent = '保持';});//...
}

當你理解了 hold 是一個異步的重新協商過程后,就能編寫出更健壯的 UI 邏輯。例如,在調用 hold() 后,你可以將按鈕狀態設置為“正在保持…”,直到接收到 hold 事件,再將其更新為“恢復通話”。

發送 DTMF (Sending DTMF Tones)

DTMF(雙音多頻)就是我們平時在電話鍵盤上按鍵時聽到的聲音。在 VoIP 通話中,發送 DTMF 信號通常用于與自動語音應答系統(IVR)進行交互,例如在致電銀行時根據語音提示按“1”選擇服務,按“2”輸入密碼等 48。

  • 方法:

    • session.sendDTMF(tone, options): 在當前通話中發送一個或多個 DTMF 音 38。
      • tone: 一個字符串或數字,代表要發送的按鍵。可以是單個字符如 '1', '#',也可以是連續的字符串如 '1234#'
      • options: 一個可選對象,可以設置 duration(每個音的持續時間,單位毫秒)和 interToneGap(多個音之間的間隔時間)49。
  • 事件:

    • newDTMF: 當收到來自對方的 DTMF 信號時觸發 49。
  • 實現原理:

    DTMF 信號主要有兩種發送方式:帶內(in-band)和帶外(out-of-band)。帶內方式是將 DTMF 音作為特殊的 RTP 包(RFC 2833)在媒體流中傳輸。帶外方式則是通過信令協議(如 SIP INFO 或 MESSAGE 請求)來發送。JsSIP 默認使用 SIP INFO 請求這種帶外方式來發送 DTMF,這種方式通常更可靠 49。

示例代碼:

JavaScript

// 假設我們有一個撥號盤,點擊數字按鈕時調用此函數
function sendDTMFDigit(digit) {if (currentSession && currentSession.isEstablished()) {const options = {duration: 160,interToneGap: 120};currentSession.sendDTMF(digit, options);console.log(`已發送 DTMF: ${digit}`);}
}// 監聽收到的 DTMF
function setupSessionEventHandlers(session) {//...session.on('newDTMF', (data) => {if (data.originator === 'remote') {console.log(`收到遠程 DTMF: ${data.dtmf.tone}`);// 可以在這里播放一個本地聲音提示用戶}});//...
}

通過本章的學習,你的軟電話已經從一個只能“打”和“接”的簡單工具,變成了一個具備靜音、保持、按鍵交互等實用功能的通信終端。接下來,我們將進入更廣闊的領域,探索 JsSIP 在即時消息方面的能力,并為你提供一份詳盡的配置與排錯寶典。


第三部分:深入探索與 API 寶典

你已經掌握了 JsSIP 的核心通話功能。現在,讓我們更進一步,探索 JsSIP 的其他能力,并將之前零散的知識點系統化。這一部分將作為你的“瑞士軍刀”和參考手冊,內容涵蓋即時消息、詳盡的配置參數解析、一套行之有效的排錯方法論,以及一份完整的 API 速查表。掌握了這部分內容,你將有能力獨立解決大部分開發中遇到的問題。

第七章:不止于通話:即時消息 (IM)

除了強大的音視頻通話能力,JsSIP 還支持通過 SIP MESSAGE 方法實現簡單的即時消息(Instant Messaging)功能。這讓你可以在不建立通話的情況下,向另一個 SIP 用戶發送和接收文本消息。

需要澄清的是,這里所說的即時消息,是基于 SIP 協議本身的 MESSAGE 方法實現的,它是一個獨立的、無會話的(session-less)消息傳遞機制。這與在 WebRTC 通話中利用 RTCDataChannel 實現的“通話內聊天”是兩種不同的技術。JsSIPsendMessage 功能讓你擁有了獨立于通話的、類似短信的通信能力。

發送消息 (ua.sendMessage())

要發送一條即時消息,你只需調用 UA 實例上的 ua.sendMessage(target, body, options) 方法 27。

  • target: 字符串,消息接收方的 SIP URI,例如 'sip:bob@example.com'
  • body: 字符串,你想要發送的消息內容。
  • options: 一個可選的配置對象,其中常用的屬性是 eventHandlers,用于監聽該條消息的發送狀態 36。

示例代碼:

JavaScript

const target = 'sip:bob@example.com';
const body = '你好,Bob!這是一條測試消息。';const options = {eventHandlers: {succeeded: (e) => {console.log('消息發送成功!');// 可以在這里更新 UI,顯示消息已送達},failed: (e) => {console.error(`消息發送失敗,原因: ${e.cause}`);// 可以在這里更新 UI,標記消息發送失敗}}
};ua.sendMessage(target, body, options);

eventHandlers 中監聽 succeededfailed 事件,可以讓你獲得關于消息投遞狀態的即時反饋,這對于構建一個可靠的聊天應用至關重要 27。

接收消息 (newMessage 事件)

當有其他人給你發送 SIP MESSAGE 時,UA 實例會觸發 newMessage 事件 27。你需要監聽這個事件來處理收到的消息。

JavaScript

ua.on('newMessage', (data) => {// 判斷是否為收到的消息if (data.originator === 'remote') {console.log('收到一條新消息!');// 獲取發送方信息const sender = data.message.remote_identity.uri.toString();// 獲取消息內容// 注意:消息內容在原始請求的 body 中const content = data.request.body;console.log(`來自 ${sender} 的消息: ${content}`);// 在 UI 上顯示收到的消息displayNewMessage(sender, content);// (可選)向發送方回復一個確認接收的響應// 這在協議層面不是強制的,但有助于實現“已讀”等功能// data.message.accept(); }
});

newMessage 事件的回調函數參數 data 對象中包含了所有你需要的信息 36:

  • originator: 值為 'remote' 表示是收到的消息。
  • message: 一個 JsSIP.Message 實例,你可以從中獲取發送方的身份信息 message.remote_identity
  • request: 原始的 SIP MESSAGE 請求對象,消息的正文存儲在 request.body 中。

對于收到的消息,JsSIP.Message 實例還提供了 accept()reject() 方法,用于向發送方回復一個 2xx 成功響應或一個非 2xx 失敗響應。雖然在許多場景下這不是必需的,但它可以被用來實現更復雜的信令邏輯,例如消息的已達回執 52。

第八章:配置與排錯

在開發過程中,遇到問題在所難免。本章旨在為你提供最強大的“武器”——詳盡的配置知識和清晰的排錯思路。掌握了它們,你就能從容應對 JsSIP 開發中的各種挑戰。

JsSIP.UA 配置參數大全

UA 的配置是 JsSIP 應用的起點,也是最容易出錯的地方。下面這張表格匯總了 JsSIP 中最重要的一些配置參數,并附有中文說明,供你隨時查閱 28。

參數名 (Parameter)類型 (Type)是否必須 (Mandatory)默認值 (Default)中文說明
uriString-你的完整 SIP URI,如 sip:alice@example.com
socketsArray-JsSIP.WebSocketInterface 實例的數組,定義 WebSocket 服務器連接。
passwordString-你的 SIP 賬戶密碼。如果服務器需要認證,則為必須。
ha1String-預計算的 HA1 摘要,用于 Digest 認證,可替代 password
realmString-SIP 認證域。與 ha1 配合使用。Asterisk 服務器通常為 asterisk
authorization_userStringuri 的用戶名用于認證的用戶名,如果與 uri 中的用戶名不同。
display_nameString-你的顯示名稱,會顯示在對方的來電提示中。
registerBooleantrue是否在 ua.start() 后自動注冊。
register_expiresNumber600注冊有效期(秒)。UA 會在此時間到期前自動續期。
registrar_serverStringuri 的域SIP 注冊服務器的地址,如果與 uri 中的域不同。
no_answer_timeoutNumber60來電無應答的超時時間(秒),超時后會自動拒絕。
session_timersBooleantrue是否啟用會話定時器(RFC 4028),用于檢測僵死會話。
connection_recovery_min_intervalNumber2WebSocket 斷線重連的最小間隔(秒)。
connection_recovery_max_intervalNumber30WebSocket 斷線重連的最大間隔(秒)。
調試你的 JsSIP 應用

當應用行為不符合預期時,第一步是獲取更多的信息。

  1. 開啟 JsSIP 調試日志:

    這是最重要、最有效的調試手段。在你的代碼初始化階段加入下面這行代碼,JsSIP 就會在瀏覽器的開發者工具控制臺中打印出所有收發的 SIP 消息和內部狀態變化 42。

    JavaScript

    JsSIP.debug.enable('JsSIP:*');
    

    通過閱讀這些日志,你可以清晰地看到 INVITE200 OKACK 等消息的流轉過程,以及 SDP 的具體內容,這對于定位問題非常有幫助。

  2. 使用瀏覽器開發者工具:

    • 網絡 (Network) 面板: 篩選 WS (WebSocket) 流量,你可以看到 JsSIP 與服務器之間的實時通信數據。
    • 控制臺 (Console) 面板: 除了 JsSIP 的調試日志,這里還會顯示任何 JavaScript 運行時錯誤。
常見錯誤與解決方案

排錯的本質是分層定位問題。一個 JsSIP 應用的故障,可能發生在網絡連接層、SIP 信令層,或是 WebRTC 媒體層。下面我們提供一個清晰的排錯流程和常見問題的解決方案。

排錯流程圖:

  1. 檢查連接層 -> ua.on('connected') 觸發了嗎?
    • : 問題出在 WebSocket 連接。檢查 sockets 配置中的服務器地址是否正確、網絡是否通暢、服務器是否正在運行。
  2. 檢查注冊層 -> ua.on('registered') 觸發了嗎?
    • : 問題出在 SIP 注冊。監聽 registrationFailed 事件,查看 e.cause 31。
      • Authentication Error: 密碼 (passwordha1) 或用戶名 (authorization_user) 錯誤 58。
      • Connection Error: 無法連接到服務器。
  3. 檢查呼叫信令層 -> 對方接聽后,session.on('confirmed') 觸發了嗎?
    • : 問題出在呼叫建立過程。監聽 session.on('failed') 事件,查看 e.cause 58。
      • Busy: 對方正忙。
      • Rejected: 對方拒絕接聽。
      • Not Found: 對方不在線或號碼錯誤。
      • Incompatible SDP: 雙方媒體能力不兼容,例如沒有共同支持的編解碼器。
  4. 檢查媒體層 -> 通話已接通 (confirmed),但聽不到/看不到對方?
    • : 這是最常見的問題,幾乎總是 NAT 穿透失敗 導致的 60。
    • 解決方案:
      • 確認 STUN/TURN 配置: 檢查 ua.call()ua 構造函數的 pcConfig.iceServers 中是否正確配置了 STUN 和 TURN 服務器。
      • TURN 服務器是關鍵: STUN 只能解決部分 NAT 問題。在復雜的網絡環境中(如對稱型 NAT),必須使用 TURN 服務器進行媒體中繼。
      • 檢查 TURN 憑證: 確認 TURN 服務器的地址、用戶名 (username) 和密碼 (credential) 是否正確無誤。
      • 查看 SDP: 在 JsSIP:* 日志中找到 INVITE200 OK 消息里的 SDP 內容,檢查其中的 a=candidate 行,確認是否有 relay 類型的候選地址(這表示 TURN 服務器已生效)。如果只有 hostsrflx 類型的候選地址,說明 TURN 服務器可能未生效或無法訪問。
      • RTP Timeout: 如果媒體流中斷一段時間,JsSIP 會因為收不到 RTP 包而觸發 failed 事件,cause 通常為 'RTP Timeout' 32。這同樣指向了 NAT 穿透問題。

常見失敗原因 (JsSIP.C.causes) 表 32

原因常量 (Constant)字符串值 (Value)描述
CONNECTION_ERROR‘Connection Error’WebSocket 連接錯誤。
AUTHENTICATION_ERROR‘Authentication Error’認證失敗(用戶名或密碼錯誤)。
BUSY‘Busy’對方正忙(收到 486 或 600 響應)。
REJECTED‘Rejected’對方拒絕(收到 403 或 603 響應)。
NOT_FOUND‘Not Found’找不到目標用戶(收到 404 或 604 響應)。
UNAVAILABLE‘Unavailable’對方當前不可用(收到 480, 410 等響應)。
INCOMPATIBLE_SDP‘Incompatible SDP’媒體能力不兼容(收到 488 或 606 響應)。
NO_ANSWER‘No Answer’來電在 no_answer_timeout 內未被接聽。
CANCELED‘Canceled’呼叫在接聽前被主叫或被叫取消。
RTP_TIMEOUT‘RTP Timeout’因長時間未收到 RTP 媒體包導致會話終止。
USER_DENIED_MEDIA_ACCESS‘User Denied Media Access’用戶在瀏覽器彈窗中拒絕了攝像頭/麥克風權限。

第九章:JsSIP API 參考大全

本章是你的 JsSIP API 速查手冊。我們將以表格的形式,清晰、完整地列出 JsSIP 核心類的主要方法和事件,方便你在開發過程中隨時查閱。

JsSIP.UA 事件 (Events)

UA 的事件是驅動應用狀態變化的核心,此表讓開發者對所有可能的狀態變化一目了然 27。

事件名 (Event)觸發時機回調參數 data 結構
connecting每次嘗試連接 WebSocket 時{ socket, attempts }
connectedWebSocket 連接成功建立時{ socket }
disconnectedWebSocket 連接斷開時{ socket, code, reason }
registeredSIP 注冊成功時{ response }
unregisteredSIP 注銷成功或注冊過期時{ response, cause }
registrationFailedSIP 注冊失敗時{ response, cause }
newRTCSession收到或發起新的音視頻通話時{ originator, session, request }
newMessage收到或發起新的即時消息時{ originator, message, request }
JsSIP.RTCSession 方法 (Methods)

這是控制通話的“遙控器”,此表是實現所有通話功能的速查手冊 38。

方法名 (Method)功能描述
answer(options)接聽來電。
terminate(options)終止通話(掛斷或拒絕)。
hold(options)將通話置于保持狀態。
unhold(options)從保持狀態恢復通話。
mute(options)將通話靜音。
unmute(options)取消通話靜音。
sendDTMF(tone, options)發送 DTMF 按鍵音。
refer(target, options)將通話轉移給第三方(呼叫轉移)。
isOnHold()檢查通話的保持狀態。
isMuted()檢查通話的靜音狀態。
isEstablished()檢查通話是否已建立。
isEnded()檢查通話是否已結束。
getLocalStreams()獲取本地媒體流數組。
getRemoteStreams()獲取遠程媒體流數組。
JsSIP.RTCSession 事件 (Events)

RTCSession 的事件反映了通話的完整生命周期,掌握它們是編寫健壯通話界面的關鍵 38。

事件名 (Event)觸發時機回調參數 data 結構
progress呼叫進行中(對方正在響鈴)。{ originator, response }
accepted對方已接聽通話。{ originator, response }
confirmed通話雙方確認,媒體通道建立。{ originator, ack }
ended通話正常結束。{ originator, message, cause }
failed通話建立失敗或中途異常終止。{ originator, message, cause }
peerconnection底層 RTCPeerConnection 創建時。{ peerconnection }
hold通話被任一方置于保持狀態。{ originator }
unhold通話從保持狀態恢復。{ originator }
muted本地媒體被靜音。-
unmuted本地媒體被取消靜音。-
newDTMF收到或發送了 DTMF 信號。{ originator, dtmf, request }
sdp本地或遠程 SDP 發生變化時。{ originator, sdp }

第四部分:實戰項目:從零構建一個功能完善的 Web 軟電話

理論與實踐相結合,方能真正掌握一門技術。在最后這一部分,我們將把前面所有章節的知識融會貫通,從零開始,一步步構建一個界面美觀、功能完善的 Web 軟電話。這個項目不僅是對你學習成果的檢驗,更可以作為你未來開發自己 RTC 應用的堅實模板。

第十章:項目設計與界面

一個好的產品,始于一個好的設計。在編寫核心邏輯之前,我們首先要規劃軟電話的用戶界面(UI)和用戶體驗(UX),并完成 HTML 和 CSS 的編寫。

UI/UX 設計最佳實踐

對于一個軟電話應用,清晰、直觀、高效是設計的核心原則。我們可以借鑒一些通用的 UI/UX 最佳實踐 62:

  • 簡潔性與一致性: 界面元素(按鈕、輸入框、狀態顯示)的風格、顏色、字體應保持統一。避免不必要的裝飾,讓用戶專注于核心的通話功能。
  • 清晰的視覺層級: 重要的信息,如通話狀態(“已連接”、“通話中”)、對方號碼、通話時長,應該在視覺上最突出。次要信息則應弱化。
  • 明確的行動號召 (CTA): “呼叫”、“接聽”、“掛斷”等核心操作按鈕,應該使用高對比度的顏色、合適的尺寸和清晰的圖標,讓用戶能毫不猶豫地找到并點擊。
  • 即時反饋: 用戶的每一個操作都應得到視覺反饋。例如,點擊按鈕時按鈕有按下的效果;發起呼叫后,界面狀態應立即變為“正在呼叫…”。這能消除用戶的不確定感。
  • 合理的布局: 將相關功能組織在一起。例如,登錄配置區、撥號區、通話中控制區應有明確的劃分。
界面布局設計

根據上述原則,我們將軟電話界面劃分為以下幾個區域:

  1. 配置/登錄區 (Configuration/Login Area): 位于頁面頂部,用于輸入 SIP 服務器地址、SIP URI 和密碼。旁邊有一個“連接/斷開”按鈕和狀態指示燈。
  2. 撥號區 (Dialpad Area): 一個標準的電話撥號盤,包含數字 0-9、*、#,一個用于顯示輸入號碼的文本框,以及一個“呼叫”按鈕。
  3. 通話區 (Call Area):
    • 視頻窗口: 包含兩個 <video> 元素,一個用于顯示遠程視頻流 (remoteView),一個用于顯示本地視頻預覽 (selfView)。
    • 通話信息: 顯示對方的號碼/名稱和通話計時器。
    • 通話控制欄: 在通話建立后顯示,包含“靜音”、“保持”、“鍵盤”、“掛斷”等按鈕。
HTML 結構

下面是我們軟電話的完整 HTML 骨架。我們為所有需要通過 JavaScript 操作的元素都賦予了清晰的 id。這份代碼基于我們研究過的示例 34,并進行了重構和功能擴展。

HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>JsSIP Web Softphone</title><link rel="stylesheet" href="style.css">
</head>
<body><div class="phone"><div class="config-area"><h3>配置</h3><div class="input-group"><label for="sip-uri">SIP URI:</label><input type="text" id="sip-uri" placeholder="sip:user@domain.com"></div><div class="input-group"><label for="sip-password">密碼:</label><input type="password" id="sip-password"></div><div class="input-group"><label for="ws-server">WebSocket 服務器:</label><input type="text" id="ws-server" placeholder="wss://sip.myhost.com"></div><div class="config-controls"><button id="connect-button">連接</button><span id="connection-status" class="status-light red"></span></div></div><div class="video-area"><video id="remote-video" autoplay></video><video id="local-video" autoplay muted></video></div><div class="dial-area"><div id="call-info" class="call-info-display"><span id="call-status">未連接</span><span id="call-timer">00:00</span></div><input type="text" id="dial-input" placeholder="輸入 SIP URI 或號碼"><div id="dialpad" class="dialpad-grid"></div><button id="call-button" class="action-button call" disabled>呼叫</button></div><div id="in-call-controls" class="in-call-controls-grid hidden"><button id="mute-button" class="control-button">靜音</button><button id="hold-button" class="control-button">保持</button><button id="dtmf-button" class="control-button">鍵盤</button><button id="hangup-button" class="action-button hangup">掛斷</button></div><div id="incoming-call-toast" class="incoming-toast hidden"><p>來電來自: <span id="incoming-caller"></span></p><button id="answer-button" class="action-button call">接聽</button><button id="reject-button" class="action-button hangup">拒絕</button></div></div><script src="jssip.min.js"></script><script src="main.js"></script>
</body>
</html>
CSS 樣式

為了讓界面美觀易用,我們編寫以下 style.css 文件。樣式代碼注重響應式設計和視覺清晰度。

CSS

/* style.css */
body {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;display: flex;justify-content: center;align-items: center;min-height: 100vh;background-color: #f0f2f5;margin: 0;
}.phone {width: 360px;background: #fff;border-radius: 20px;box-shadow: 0 10px 30px rgba(0,0,0,0.1);overflow: hidden;display: flex;flex-direction: column;
}/* 配置區域 */
.config-area { padding: 20px; background-color: #f8f9fa; }
.config-area h3 { margin-top: 0; text-align: center; color: #333; }
.input-group { margin-bottom: 10px; }
.input-group label { display: block; margin-bottom: 5px; font-size: 14px; color: #555; }
.input-group input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 5px; box-sizing: border-box; }
.config-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }
#connect-button { padding: 8px 15px; border: none; border-radius: 5px; background-color: #007bff; color: white; cursor: pointer; }
.status-light { width: 12px; height: 12px; border-radius: 50%; }
.status-light.red { background-color: #dc3545; }
.status-light.yellow { background-color: #ffc107; }
.status-light.green { background-color: #28a745; }/* 視頻區域 */
.video-area { position: relative; width: 100%; background-color: #000; }
#remote-video { width: 100%; display: block; }
#local-video { position: absolute; width: 25%; bottom: 10px; right: 10px; border: 2px solid white; border-radius: 5px; }/* 撥號區域 */
.dial-area { padding: 20px; }
.call-info-display { text-align: center; margin-bottom: 15px; height: 40px; }
#call-status { display: block; font-size: 18px; color: #333; }
#call-timer { font-size: 14px; color: #888; }
#dial-input { width: 100%; padding: 10px; font-size: 20px; text-align: center; border: none; border-bottom: 2px solid #007bff; margin-bottom: 15px; box-sizing: border-box; }
.dialpad-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px; }
.dialpad-grid button { padding: 15px; font-size: 20px; border: 1px solid #ddd; border-radius: 50%; background-color: #f8f9fa; cursor: pointer; }/* 控制按鈕 */
.action-button { width: 100%; padding: 15px; font-size: 18px; border: none; border-radius: 10px; color: white; cursor: pointer; }
.action-button.call { background-color: #28a745; }
.action-button.hangup { background-color: #dc3545; }
.action-button:disabled { background-color: #ccc; cursor: not-allowed; }/* 通話中控制 */
.in-call-controls-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; padding: 0 20px 20px; }
.control-button { padding: 10px; font-size: 14px; border: 1px solid #ddd; border-radius: 8px; background-color: #f8f9fa; cursor: pointer; }
.control-button.active { background-color: #007bff; color: white; }/* 來電提示 */
.incoming-toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(0,0,0,0.8); color: white; padding: 15px 20px; border-radius: 10px; z-index: 1000; display: flex; align-items: center; gap: 15px; }
.incoming-toast p { margin: 0; }
.incoming-toast button { padding: 8px 12px; }.hidden { display: none!important; }

現在,我們已經準備好了軟電話的“外殼”。下一章,我們將為它注入“靈魂”——編寫 main.js 文件,實現所有核心的交互邏輯。

第十一章:核心功能實現

界面已經就緒,現在是時候編寫 JavaScript 代碼,將 UI 元素與 JsSIP 的強大功能連接起來,讓我們的軟電話真正“活”起來。本章將遵循模塊化的思想,一步步實現所有核心邏輯。

代碼結構規劃

為了讓代碼清晰、可維護,我們將其劃分為幾個邏輯部分:

  1. 全局變量與常量: 存放 UA 實例、當前會話、DOM 元素引用等。
  2. UI 元素獲取: 在腳本開始時,一次性獲取所有需要操作的 DOM 元素的引用。
  3. UI 更新函數: 編寫獨立的、職責單一的函數來更新界面,例如 updateConnectionStatus()showInCallControls()
  4. 事件處理器: 集中處理 JsSIP.UAJsSIP.RTCSession 的所有事件。
  5. 動作綁定: 為 HTML 按鈕(如連接、呼叫、掛斷)綁定點擊事件。
  6. 初始化: 腳本的入口點,負責綁定初始事件和生成撥號盤。
分步實現 (main.js)

下面是 main.js 的完整實現,包含了詳細的注釋來解釋每一步。

JavaScript

// main.js// 1. 全局變量與常量
let ua;
let currentSession;
let callTimerInterval;const DIALPAD_BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];// 2. UI 元素獲取
const ui = {sipUriInput: document.getElementById('sip-uri'),sipPasswordInput: document.getElementById('sip-password'),wsServerInput: document.getElementById('ws-server'),connectButton: document.getElementById('connect-button'),connectionStatus: document.getElementById('connection-status'),dialInput: document.getElementById('dial-input'),dialpad: document.getElementById('dialpad'),callButton: document.getElementById('call-button'),callInfo: document.getElementById('call-info'),callStatus: document.getElementById('call-status'),callTimer: document.getElementById('call-timer'),inCallControls: document.getElementById('in-call-controls'),hangupButton: document.getElementById('hangup-button'),muteButton: document.getElementById('mute-button'),holdButton: document.getElementById('hold-button'),dtmfButton: document.getElementById('dtmf-button'),incomingToast: document.getElementById('incoming-call-toast'),incomingCaller: document.getElementById('incoming-caller'),answerButton: document.getElementById('answer-button'),rejectButton: document.getElementById('reject-button'),localVideo: document.getElementById('local-video'),remoteVideo: document.getElementById('remote-video')
};// 3. UI 更新函數
function updateConnectionStatus(status) { // 'disconnected', 'connecting', 'connected', 'registered'ui.connectionStatus.className = 'status-light';switch (status) {case 'disconnected':ui.connectionStatus.classList.add('red');ui.connectButton.textContent = '連接';ui.callButton.disabled = true;break;case 'connecting':ui.connectionStatus.classList.add('yellow');ui.connectButton.textContent = '連接中...';break;case 'connected': // Connected to WebSocket, but not yet registeredui.connectionStatus.classList.add('yellow');ui.connectButton.textContent = '斷開';break;case 'registered':ui.connectionStatus.classList.add('green');ui.connectButton.textContent = '斷開';ui.callButton.disabled = false;break;}
}function updateCallStatus(status, remoteIdentity = '') {ui.callStatus.textContent = status;if (remoteIdentity) {ui.callStatus.textContent += ` - ${remoteIdentity}`;}
}function showInCallControls(show) {if (show) {ui.dialpad.classList.add('hidden');ui.callButton.classList.add('hidden');ui.inCallControls.classList.remove('hidden');ui.dialInput.disabled = true;} else {ui.dialpad.classList.remove('hidden');ui.callButton.classList.remove('hidden');ui.inCallControls.classList.add('hidden');ui.dialInput.disabled = false;ui.dialInput.value = '';updateCallStatus('已注冊');stopCallTimer();// 重置控制按鈕狀態ui.muteButton.classList.remove('active');ui.muteButton.textContent = '靜音';ui.holdButton.classList.remove('active');ui.holdButton.textContent = '保持';}
}function showIncomingCallToast(show, remoteIdentity = '') {if (show) {ui.incomingCaller.textContent = remoteIdentity;ui.incomingToast.classList.remove('hidden');} else {ui.incomingToast.classList.add('hidden');}
}function startCallTimer() {let startTime = Date.now();ui.callTimer.textContent = '00:00';callTimerInterval = setInterval(() => {let seconds = Math.floor((Date.now() - startTime) / 1000);let mins = Math.floor(seconds / 60).toString().padStart(2, '0');let secs = (seconds % 60).toString().padStart(2, '0');ui.callTimer.textContent = `${mins}:${secs}`;}, 1000);
}function stopCallTimer() {clearInterval(callTimerInterval);ui.callTimer.textContent = '00:00';
}// 4. 事件處理器
function setupUaEventHandlers() {ua.on('connecting', () => {updateConnectionStatus('connecting');updateCallStatus('正在連接服務器...');});ua.on('connected', () => {updateConnectionStatus('connected');updateCallStatus('服務器已連接,正在注冊...');});ua.on('disconnected', () => {updateConnectionStatus('disconnected');updateCallStatus('未連接');alert('WebSocket 連接已斷開。');});ua.on('registered', () => {updateConnectionStatus('registered');updateCallStatus('已注冊');});ua.on('registrationFailed', (e) => {updateConnectionStatus('connected'); // Still connected to WSupdateCallStatus('注冊失敗');alert(`注冊失敗: ${e.cause}`);});ua.on('unregistered', () => {updateConnectionStatus('connected');updateCallStatus('已注銷');});ua.on('newRTCSession', (data) => {if (currentSession) { // 如果已有通話,自動拒絕新來電data.session.terminate({ status_code: 486, reason_phrase: 'Busy Here' });return;}currentSession = data.session;setupSessionEventHandlers();if (currentSession.direction === 'incoming') {const remoteIdentity = currentSession.remote_identity.uri.toString();showIncomingCallToast(true, remoteIdentity);}});
}function setupSessionEventHandlers() {currentSession.on('progress', () => {updateCallStatus('正在呼叫...', currentSession.remote_identity.uri.user);});currentSession.on('failed', (e) => {updateCallStatus(`呼叫失敗: ${e.cause}`);showInCallControls(false);currentSession = null;});currentSession.on('ended', (e) => {updateCallStatus(`通話結束: ${e.cause}`);showInCallControls(false);currentSession = null;});currentSession.on('accepted', () => {updateCallStatus('通話已接通', currentSession.remote_identity.uri.user);showInCallControls(true);startCallTimer();});// 媒體流處理currentSession.on('peerconnection', (data) => {data.peerconnection.addEventListener('track', (e) => {ui.remoteVideo.srcObject = e.streams;});});// 靜音/保持事件currentSession.on('muted', () => {ui.muteButton.classList.add('active');ui.muteButton.textContent = '取消靜音';});currentSession.on('unmuted', () => {ui.muteButton.classList.remove('active');ui.muteButton.textContent = '靜音';});currentSession.on('hold', () => {ui.holdButton.classList.add('active');ui.holdButton.textContent = '恢復通話';});currentSession.on('unhold', () => {ui.holdButton.classList.remove('active');ui.holdButton.textContent = '保持';});
}// 5. 動作綁定
ui.connectButton.addEventListener('click', () => {if (ua && ua.isRegistered()) {ua.unregister();} else if (ua && ua.isConnected()) {ua.stop();} else {try {const socket = new JsSIP.WebSocketInterface(ui.wsServerInput.value);const configuration = {sockets: [socket],uri: ui.sipUriInput.value,password: ui.sipPasswordInput.value,register: true};ua = new JsSIP.UA(configuration);setupUaEventHandlers();ua.start();} catch (e) {alert(`配置錯誤: ${e.message}`);}}
});ui.callButton.addEventListener('click', () => {const target = ui.dialInput.value;if (!target) {alert('請輸入要呼叫的 SIP URI 或號碼');return;}const options = {mediaConstraints: { audio: true, video: true },pcConfig: {iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]}};ua.call(target, options);
});ui.hangupButton.addEventListener('click', () => {if (currentSession) {currentSession.terminate();}
});ui.answerButton.addEventListener('click', () => {if (currentSession) {const options = {mediaConstraints: { audio: true, video: true }};currentSession.answer(options);showIncomingCallToast(false);}
});ui.rejectButton.addEventListener('click', () => {if (currentSession) {currentSession.terminate({ status_code: 486, reason_phrase: 'Busy Here' });showIncomingCallToast(false);}
});ui.muteButton.addEventListener('click', () => {if (currentSession && currentSession.isMuted().audio) {currentSession.unmute({ audio: true });} else if (currentSession) {currentSession.mute({ audio: true });}
});ui.holdButton.addEventListener('click', () => {if (currentSession && currentSession.isOnHold().local) {currentSession.unhold();} else if (currentSession) {currentSession.hold();}
});ui.dtmfButton.addEventListener('click', () => {ui.dialpad.classList.toggle('hidden');
});// 6. 初始化
function initialize() {// 生成撥號盤DIALPAD_BUTTONS.forEach(btn => {const button = document.createElement('button');button.textContent = btn;button.addEventListener('click', () => {if (currentSession && currentSession.isEstablished()) {currentSession.sendDTMF(btn);} else {ui.dialInput.value += btn;}});ui.dialpad.appendChild(button);});// 默認狀態updateConnectionStatus('disconnected');showInCallControls(false);
}// 啟動應用
initialize();

至此,我們的軟電話已經具備了所有核心功能。它能夠連接、注冊、撥打、接聽、掛斷、靜音、保持和發送 DTMF。代碼結構清晰,UI 和邏輯分離,方便后續的擴展和維護。

第十二章:完整代碼與展望

恭喜你!你已經跟隨本書的腳步,從一個對 SIP 和 WebRTC 一無所知的 JavaScript 開發者,成長為能夠獨立構建一個功能完善的 Web 軟電話的實踐者。本章將為你提供最終的、完整的項目代碼,并為你未來的學習和探索之路指明方向。

最終項目代碼

我們已經將所有功能整合到了三個文件中:index.html(結構),style.css(樣式),以及 main.js(邏輯)。上一章已經展示了 index.htmlmain.js 的完整代碼,style.css 也已提供。這三個文件共同構成了一個可以獨立運行的 Web 軟電話項目。

如何運行 Demo

要運行這個項目,你需要:

  1. 獲取 jssip.min.js: 從 JsSIP 官網下載頁面或 npm 包的 dist 目錄中找到最新版本的 jssip.min.js 文件,并將其與 index.html, style.css, main.js 放在同一個文件夾下。

  2. 獲取 SIP 賬戶信息: 你需要一個可用的 SIP 賬戶,包括:

    • WebSocket 服務器地址 (如 wss://sip.example.com)
    • 你的 SIP URI (如 sip:1001@example.com)
    • 你的密碼
  3. 運行本地 Web 服務器: 由于瀏覽器安全策略的限制(特別是 getUserMedia API 需要在安全上下文 httpslocalhost 中運行),你不能直接通過 file:// 協議打開 index.html。你需要在項目文件夾中啟動一個簡單的本地 Web 服務器。如果你安裝了 Node.js,可以使用 http-server 包:

    Bash

    # 安裝 http-server (如果尚未安裝)
    npm install -g http-server# 在你的項目文件夾中運行
    http-server
    

    然后,在瀏覽器中打開它提供的地址(通常是 http://localhost:8080)。

  4. 配置并連接: 在打開的網頁中,填入你的 SIP 賬戶信息,點擊“連接”。如果一切順利,狀態指示燈將變為綠色,你就可以開始撥打電話了!

未來展望

你已經構建了一個堅實的基礎,但實時通信的世界遠不止于此。以下是一些你可以繼續探索的方向:

  • 通話轉移 (Call Transfer): JsSIP 支持通過 session.refer() 方法實現通話轉移(包括盲轉和咨詢轉)44。你可以研究這個 API,為你的軟電話添加“轉移”按鈕。
  • 多方通話 (Conference Calls): JsSIP 本身是點對點通信的庫。要實現三人或更多人的通話,通常需要一個中心化的媒體服務器,如 MCU(多點控制單元)或 SFU(選擇性轉發單元)。你可以研究如何將 JsSIP 客戶端連接到像 Janus, Jitsi, 或 Medooze 這樣的開源媒體服務器。
  • 在線狀態 (Presence): SIP 協議包含了一套基于 SUBSCRIBENOTIFY 方法的在線狀態(Presence)和訂閱機制。你可以利用它來訂閱一個聯系人列表的狀態,并在你的軟電話界面上顯示他們是“在線”、“離線”還是“通話中”。
  • 構建生產級應用: 本書的 Demo 是一個學習工具。在構建真正的生產級應用時,你還需要考慮更多:
    • 安全性: 永遠不要在客戶端代碼中硬編碼密碼。認證信息應通過安全的后端服務獲取。
    • 可靠的 TURN 服務: 依賴公共 STUN 服務器是不夠的。生產應用必須部署自己的、地理分布的、高可用的 TURN 服務器,以保證在各種網絡環境下的通話成功率。
    • 完善的 UI/UX: 進行更深入的用戶研究,設計更友好的交互流程,處理各種邊緣情況(如網絡斷開重連、設備切換等)。
    • 質量監控: 集成 WebRTC 的 getStats() API,監控通話質量指標(如丟包率、延遲、抖動),以便分析和優化通話體驗。

結語

實時通信是一個充滿挑戰但又極具價值的領域。通過本書的學習,你不僅掌握了 JsSIP 這個優秀的工具,更重要的是,你理解了其背后 SIP 和 WebRTC 的核心原理。這份知識將成為你未來探索更廣闊 RTC 世界的通行證。

希望這本書能成為你 RTC 開發之旅的起點。不斷實踐,不斷探索,你將能夠創造出更多連接人與人的精彩應用。祝你編碼愉快!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/90786.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/90786.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/90786.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【RHCSA 問答題】第 12 章 安裝和更新軟件包

目錄什么是 RPM&#xff1f;dnf 是什么&#xff0c;它和 rpm 有什么聯系和區別&#xff1f;如何設置禁止直接遠程登錄 root 賬戶&#xff1f;RHEL 中如何做才能啟用對第三方存儲庫的支持&#xff1f;怎么理解 RHEL9 中的應用流(Application Streams)和模塊(Modules)&#xff1f…

GEO優化實戰:如何在DeepSeek、豆包等AI平臺搶占推薦位?

在當今競爭激烈的 AI 領域&#xff0c;GEO 優化在搶占 AI 平臺推薦位上的重要性日益凸顯。各大平臺都在為優質內容和企業爭取更好的展示機會&#xff0c;與此同時&#xff0c;一個現象引發了眾人關注&#xff1a;眾多企業大力推薦天津誠智未來公司&#xff0c;這背后究竟隱藏著…

機器學習——隨機森林算法分類問題案例解析(sklearn)

1. 集成學習&#xff1a;三個臭皮匠&#xff0c;如何賽過諸葛亮&#xff1f;我們之前學習的線性回歸、決策樹等算法&#xff0c;就像是團隊里的某一位“專家”。這位專家可能在某個領域很擅長&#xff0c;但單憑他一人&#xff0c;要解決復雜多變的問題&#xff0c;總會遇到瓶頸…

Mermaid流程圖

手動畫流程圖太復雜了&#xff0c;用極少的字符生成圖表是人生的夢想。 Mermaid Chart - Create complex, visual diagrams with text. A smarter way of creating diagrams. Linux開始菜單流程圖 flowchartA(["StartMenu"]) --> B["/usr/share/applicati…

Compose筆記(三十八)--CompositionLocal

這一節主要了解一下CompositionLocal&#xff0c;CompositionLocal是Jetpack Compose中用于組件樹內隱式數據傳遞的核心機制&#xff0c;其設計初衷是解決跨多層組件的數據共享問題&#xff0c;避免通過函數參數逐層傳遞數據。簡單總結:API: (1)compositionLocalOf<T>創建…

解決uniapp 使用uview生成小程序包太大無法上傳的問題

直接打包的插件內容優化后完美上傳&#xff0c; 相信眼尖的小伙伴已經發現了問題的關鍵 uview 會在每個組件里重復引css。導致包太大。 并且 它的格式是 data-v-哈希 沒法簡單的處理 需要壓縮通用規則。然后 再引用壓縮后的規則例如是然后 成功上傳

在線工具+網頁平臺來學習和操作Python與Excel相關技能

&#x1f517;一、在線平臺推薦&#xff08;免安裝&#xff09; ?Python平臺&#xff08;直接寫代碼、跑結果&#xff09;&#xff1a; 平臺 優點 地址 Google Colab 免費&#xff0c;支持圖表和文件操作&#xff0c;最推薦 https://colab.research.google.com …

R Excel 文件處理指南

R Excel 文件處理指南 引言 R語言作為一種強大的統計計算和圖形展示工具&#xff0c;在數據分析領域有著廣泛的應用。而Excel作為辦公軟件的佼佼者&#xff0c;在數據記錄和計算中也扮演著重要的角色。本文旨在介紹如何使用R語言處理Excel文件&#xff0c;包括讀取、寫入以及數…

億級流量短劇平臺架構演進:高并發場景下的微服務設計與性能調優

一、短劇系統概述與市場背景短劇作為一種新興的內容形式&#xff0c;近年來在移動互聯網領域迅速崛起。根據最新市場數據顯示&#xff0c;2023年中國短劇市場規模已突破300億元&#xff0c;用戶規模達到4.5億&#xff0c;平均每日觀看時長超過60分鐘。這種爆發式增長催生了對專…

4G手機控車模塊的核心功能與應用價值

4G手機控車模塊是基于4G無線通信技術實現車輛遠程監控、控制及數據交互的嵌入式設備。其核心功能包括通過4G網絡實現高速數據傳輸&#xff08;支持TCP/IP協議&#xff09;、遠程參數配置與設備管理、多網絡制式兼容&#xff0c;集成GPS/北斗定位功能&#xff0c;可實時獲取車輛…

【leetGPU】1. Vector Addition

問題 link: https://leetgpu.com/challenges/vector-addition Implement a program that performs element-wise addition of two vectors containing 32-bit floating point numbers on a GPU. The program should take two input vectors of equal length and produce a si…

瑞吉外賣學習筆記

TableField 作用: 當數據庫中表的列名與實體類中的屬性名不一致&#xff0c;使用TableField 使其對應 TableField("db_column_name") private String entityFieldName;exist 屬性 : 指定該字段是否參與增刪改查操作。 TableField(exist false) private String tempF…

RoPE:相對位置編碼的旋轉革命——原理、演進與大模型應用全景

“以復數旋轉解鎖位置關系的本質表達&#xff0c;讓Transformer突破長度藩籬” 旋轉位置編碼&#xff08;Rotary Position Embedding, RoPE&#xff09; 是由 Jianlin Su 等研究者 于2021年提出的突破性位置編碼方法&#xff0c;通過復數空間中的旋轉操作將相對位置信息融入Tra…

震網(Stuxnet):打開潘多拉魔盒的數字幽靈

在科技飛速發展的今天&#xff0c;代碼和數據似乎只存在于無形的數字世界。但如果我告訴大家&#xff0c;一段代碼曾悄無聲息地潛入一座受到嚴密物理隔離的核工廠&#xff0c;并成功摧毀了其中的物理設備&#xff0c;大家是否會感到一絲寒意&#xff1f;這不是科幻電影的情節&a…

一文讀懂:到底什么是 “具身智能” ?

今天咱們來好好聊聊一個最近很火的一個技術話題——具身智能&#xff01; 這個詞聽起來是不是有點難懂&#xff1f;其實我們可以簡單理解為&#xff1a;具身智能是具有身體的人工智能體。這樣是不是會容易理解一些&#xff1f; 具身智能&#xff08;Embodied Intelligence&…

企業級區塊鏈平臺Hyperchain核心原理剖析

Hyperchain作為國產自主可控的企業級聯盟區塊鏈平臺&#xff0c;其核心原理圍繞高性能共識、隱私保護、智能合約引擎及可擴展架構展開&#xff0c;通過多模塊協同實現企業級區塊鏈網絡的高效部署與安全運行。 以下從核心架構、關鍵技術、性能優化、安全機制、應用場景五個維度展…

論文閱讀-RaftStereo

文章目錄1 概述2 模塊說明2.1 特征抽取器2.2 相關金字塔2.3 多級更新算子2.4 Slow-Fast GRU2.5 監督3 效果1 概述 在雙目立體匹配中&#xff0c;基于迭代的模型是一種比較主流的方法&#xff0c;而其鼻祖就是本文要講的RaftStereo。 先來說下什么是雙目立體匹配。給定極線矯正…

內存優化:從堆分配到零拷貝的終極重構

引言 在現代高性能軟件開發中&#xff0c;內存管理往往是性能優化的關鍵戰場。頻繁的堆內存分配(new/delete)不僅會導致性能下降&#xff0c;還會引發內存碎片化問題&#xff0c;嚴重影響系統穩定性。本文將深入剖析高頻調用模塊中堆分配泛濫導致的性能塌方問題&#xff0c;并…

【GoLang#2】:基礎入門(工具鏈 | 基礎語法 | 內置函數)

前言&#xff1a;Go 的一些必備知識 1. Go 語言命名 Go的函數、變量、常量、自定義類型、包(package)的命名方式遵循以下規則&#xff1a; 首字符可以是任意的Unicode字符或者下劃線剩余字符可以是Unicode字符、下劃線、數字字符長度不限 Go 語言代碼風格及開發事項代碼每一行結…

Bert項目--新聞標題文本分類

目錄 技術細節 1、下載模型 2、config文件 3、BERT 文本分類數據預處理流程 4、對輸入文本進行分類 5、計算模型的分類性能指標 6、模型訓練 7、基于BERT的文本分類預測接口 問題總結 技術細節 1、下載模型 文件名稱--a0_download_model.py 使用 ModelScope 庫從模型倉…