# 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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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 库从模型仓…