WebRTC实现APP与WEB通信
项目总览:四大步骤
-
【第一步】搭建并测试信令服务器:建立一个通信中转站,让APP和WEB可以互相传递消息。
-
【第二步】构建基础的WEB接收端:创建一个能连接信令服务器,并准备好接收视频流的网页。
-
【第三步】构建基础的APP发送端:创建一个能连接信令服务器,并准备好发送视频流的APP。
-
【第四步】联调与实现完整WebRTC流程:将APP的屏幕画面真正通过WebRTC传输到WEB端并播放。
第一步:搭建并测试信令服务器
目标: 创建一个简单的Node.js服务器,它能接收客户端(未来是APP和WEB)的连接,并将一个客户端发来的消息转发给另一个。我们将用两个浏览器标签页来模拟APP和WEB,以验证服务器功能是否正常。
技术栈:
-
Node.js: JavaScript运行环境。请确保您的电脑已安装。可以在命令行输入 node -v 查看。
-
Express: 一个流行的Node.js Web框架,用于快速搭建HTTP服务。
-
Socket.IO: 一个强大的实时通信库,封装了WebSocket,让双向通信变得非常简单。
操作指南
在您喜欢的位置(例如桌面)打开命令行工具(如Terminal, PowerShell, CMD),然后执行以下命令:
Generated bash# 1. 创建一个新文件夹作为项目目录 mkdir webrtc-project cd webrtc-project # 2. 在项目内再为服务器创建一个子目录 mkdir signaling-server cd signaling-server # 3. 初始化Node.js项目,生成 package.json 文件 npm init -y # 4. 安装所需的库 npm install express socket.io
现在你的 signaling-server 文件夹里应该有 node_modules 文件夹和 package.json 文件了。
在 signaling-server 文件夹里,创建一个名为 server.js 的文件,然后将以下代码复制进去:
Generated javascript
// 引入所需模块
const express = require('express');
const http = require('http');
const { Server } = require("socket.io");
// 初始化
const app = express();
const httpServer = http.createServer(app);
const io = new Server(httpServer, {
// 配置CORS,允许来自任何源的连接,方便开发
cors: {
origin: "*",
}
});
const PORT = 3000; // 我们使用3000端口
// 当有新的客户端连接时触发
io.on('connection', (socket) => {
console.log(`[连接成功] 一个新客户端连接了,ID: ${socket.id}`);
// 监听客户端发来的 'message' 事件
socket.on('message', (data) => {
// 打印收到的消息
console.log(`[收到消息] 从 ${socket.id} 收到消息:`, data);
// **核心逻辑:将消息广播给除了自己以外的所有其他客户端**
socket.broadcast.emit('message', data);
console.log(`[转发消息] 已将消息广播给其他客户端`);
});
// 当客户端断开连接时触发
socket.on('disconnect', () => {
console.log(`[连接断开] 客户端 ${socket.id} 已断开连接`);
});
});
// 让服务器监听指定端口
httpServer.listen(PORT, () => {
console.log(`信令服务器已启动,正在监听端口: http://localhost:${PORT}`);
console.log('请确保您的防火墙已允许3000端口的TCP连接。');
});
代码解析:
-
我们创建了一个HTTP服务器,并附加了Socket.IO。
-
io.on('connection', ...): 这是入口,每当有新的浏览器或APP连接上来,这里的代码就会执行一次。
-
socket.on('message', ...): 我们定义了一个名为 message 的事件。当任何一个客户端发送 message 事件时,服务器就会收到。
-
socket.broadcast.emit('message', data): 这是关键!broadcast 意味着“广播”,它会将收到的 data 转发给除发送者之外的所有其他连接者。这正是我们需要的“信鸽”功能。
-
获取本机IP地址:
-
Windows: 在命令行输入 ipconfig,找到“无线局域网适配器 WLAN”或“以太网适配器”下的 "IPv4 地址"。
-
macOS: 在命令行输入 ifconfig | grep "inet ",找到 en0 (Wi-Fi) 或 en1 (以太网) 对应的IP地址。
-
假设你的IP地址是 192.168.1.10。
-
-
启动服务器:
Generated bash
在 signaling-server 目录下,运行命令:node server.js
如果一切顺利,你会看到:
Generated code
信令服务器已启动,正在监听端口: http://localhost:3000
请确保您的防火墙已允许3000端口的TCP连接。
创建测试网页:
为了验证服务器功能,我们快速创建一个HTML文件。在 webrtc-project 根目录 (与 signaling-server 同级) 下,创建一个名为 test.html 的文件,并粘贴以下代码:
Generated html
<!DOCTYPE html>
<html>
<head>
<title>信令服务器测试</title>
</head>
<body>
<h1>信令服务器测试</h1>
<p>你的ID: <span id="myId"></span></p>
<input type="text" id="messageInput" placeholder="输入要发送的消息">
<button onclick="sendMessage()">发送消息</button>
<h2>收到的消息:</h2>
<div id="messages"></div>
<!-- 引入Socket.IO客户端库 -->
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<script>
// !!! 重要:请将这里的IP地址换成你自己的电脑IP地址 !!!
const SERVER_IP = '192.168.1.10';
const socket = io(`http://${SERVER_IP}:3000`);
socket.on('connect', () => {
console.log('成功连接到信令服务器! My ID is', socket.id);
document.getElementById('myId').innerText = socket.id;
});
socket.on('message', (data) => {
console.log('收到一条广播消息:', data);
const msgDiv = document.createElement('div');
msgDiv.innerText = `收到消息: ${data.text}`;
document.getElementById('messages').appendChild(msgDiv);
});
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value;
if (message) {
console.log('发送消息:', message);
socket.emit('message', { text: message });
input.value = '';
}
}
</script>
</body>
</html>
-
注意: 一定要把代码中的 SERVER_IP 变量值改成你上一步查到的实际IP地址。
-
进行测试:
-
在你的电脑上,用浏览器(推荐Chrome)打开 test.html 文件(可以直接双击文件打开)。
-
再打开一个浏览器标签页,同样访问 test.html 文件。
-
现在你有了两个客户端。
-
在其中一个页面的输入框里输入 "你好",点击“发送消息”。
-
观察另一个页面,你应该会看到 "收到的消息: 你好"。
-
同时,观察你运行node server.js的那个命令行窗口,你应该能看到服务器打印出的连接、接收和转发日志。
-
如果测试成功,恭喜你!项目最基础、也是最关键的通信骨架已经搭建完毕。 我们的“信鸽”已经准备好为APP和WEB传递密信了。
如果遇到问题(例如网页无法连接),请检查:
-
test.html 里的IP地址是否正确。
-
你的电脑防火墙是否阻止了3000端口的访问。
-
手机和电脑是否连接在同一个Wi-Fi网络下。
完成这一步后我们就可以进入第二步:构建基础的WEB接收端了。
第二步:构建基础的WEB接收端
目标: 创建一个网页,它能:
-
连接到我们刚搭建的信令服务器。
-
初始化WebRTC的核心对象 RTCPeerConnection。
-
编写好处理信令(Offer, Answer, Candidate)的逻辑,处于“随时准备接收视频”的状态。
-
在收到远程视频流时,能将其播放出来。
在这一步,我们还不会真正收到来自APP的视频,但我们会把所有的“管道”和“接口”都准备好。
操作指南
在你的项目根目录 webrtc-project 下,创建一个新的文件夹 web-client。在 web-client 文件夹里,创建两个文件:
-
index.html (网页结构)
-
main.js (JavaScript逻辑)
Generated code
webrtc-project/
├── signaling-server/
│ ├── ...
├── web-client/
│ ├── index.html <-- 新建
│ └── main.js <-- 新建
└── test.html
将以下代码粘贴到 web-client/index.html 文件中:
Generated html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC 监控接收端</title>
<style>
body { font-family: sans-serif; background-color: #f0f0f0; margin: 0; padding: 20px; }
h1 { text-align: center; color: #333; }
#video-container {
max-width: 800px;
margin: 20px auto;
background-color: black;
border: 1px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
video {
width: 100%;
height: auto;
display: block;
}
#status {
text-align: center;
margin-top: 10px;
font-size: 1.2em;
color: #666;
}
</style>
</head>
<body>
<h1>WebRTC 监控接收端</h1>
<div id="video-container">
<!-- 视频将在这里播放 -->
<video id="remoteVideo" autoplay playsinline></video>
</div>
<p id="status">正在等待APP连接...</p>
<!-- 引入Socket.IO客户端库 -->
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<!-- 引入我们自己的JS逻辑 -->
<script src="main.js"></script>
</body>
</html>
代码解析:
-
我们创建了一个简单的布局,包含一个标题、一个用于显示状态的段落 <p id="status">。
-
最核心的是 <video id="remoteVideo"> 标签。autoplay 属性让视频在有数据时自动播放,playsinline 在移动设备上很重要,能让视频在页面内播放而不是全屏。
-
最后,我们引入了 socket.io.js 和我们即将编写的 main.js。
这是本步骤的核心。将以下代码粘贴到 web-client/main.js 文件中:
Generated javascript
// --- 1. 配置 ---
// !!! 重要:请将这里的IP地址换成你自己的电脑IP地址 !!!
const SERVER_IP = '192.168.1.10';
const SERVER_PORT = 3000;
// 获取HTML元素
const remoteVideo = document.getElementById('remoteVideo');
const statusDiv = document.getElementById('status');
// WebRTC变量
let peerConnection;
// --- 2. 初始化信令连接 ---
const socket = io(`http://${SERVER_IP}:${SERVER_PORT}`);
socket.on('connect', () => {
console.log('成功连接到信令服务器!');
statusDiv.textContent = '已连接到信令服务器,等待APP发起连接...';
});
// --- 3. 初始化WebRTC ---
function createPeerConnection() {
console.log('创建 PeerConnection...');
try {
// 创建 RTCPeerConnection 对象
peerConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' } // 使用Google的公共STUN服务器
]
});
// 监听ICE Candidate事件,当找到候选者时,通过信令服务器发送给对方
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
console.log('发送 ICE candidate:', event.candidate);
socket.emit('message', { type: 'candidate', payload: event.candidate });
}
};
// 监听远程视频流轨道事件,这是最关键的一步
peerConnection.ontrack = (event) => {
console.log('接收到远程视频轨道!');
statusDiv.textContent = '已接收到视频流!';
if (remoteVideo.srcObject !== event.streams[0]) {
remoteVideo.srcObject = event.streams[0];
}
};
console.log('PeerConnection 创建成功!');
} catch (e) {
console.error('创建 PeerConnection 失败:', e);
statusDiv.textContent = 'WebRTC 初始化失败,请检查浏览器支持情况。';
}
}
// --- 4. 监听信令消息 ---
socket.on('message', async (data) => {
console.log('收到信令消息:', data);
const { type, payload } = data;
// 确保在处理Offer之前,peerConnection已经创建
if (!peerConnection && type === 'offer') {
createPeerConnection();
}
switch (type) {
case 'offer':
// 收到来自APP的Offer
console.log('收到 Offer');
statusDiv.textContent = '收到连接请求,正在应答...';
await peerConnection.setRemoteDescription(new RTCSessionDescription(payload));
console.log('创建 Answer...');
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
console.log('发送 Answer');
socket.emit('message', { type: 'answer', payload: answer });
statusDiv.textContent = '已发送应答,等待连接建立...';
break;
case 'candidate':
// 收到来自APP的ICE Candidate
if (peerConnection) {
console.log('收到 Candidate,正在添加...');
await peerConnection.addIceCandidate(new RTCIceCandidate(payload));
}
break;
default:
console.log('收到未知类型的消息:', type);
break;
}
});
// --- 5. 页面加载时自动初始化 ---
// 初始时,我们先不创建PeerConnection,等待APP发起Offer时再创建。
// 这样可以确保APP是呼叫方(Caller),WEB是接收方(Callee)。
console.log('WEB端已准备就绪,等待信令消息...');
代码解析:
-
配置 (1): 和上一步一样,务必修改 SERVER_IP 为你的电脑IP地址。
-
信令连接 (2): 使用socket.io连接到服务器,并更新页面状态。
-
初始化WebRTC (3):
-
createPeerConnection() 函数是核心。它创建RTCPeerConnection对象。
-
iceServers: 我们配置了一个公共的STUN服务器。STUN服务器帮助设备发现自己的公网IP和端口,这对于NAT穿透至关重要(即使在局域网内也很有用)。
-
onicecandidate: 当WebRTC找到一个可能的网络路径(ICE候选者)时,这个事件会被触发。我们必须把它通过信令服务器发送给对方。
-
ontrack: 这是魔法发生的地方! 当P2P连接建立,对方的视频流数据到达时,这个事件被触发。我们把接收到的流 event.streams[0] 赋值给 <video> 元素的 srcObject 属性,视频就会开始播放。
-
-
监听信令消息 (4): 这是与信令服务器交互的核心逻辑。
-
当收到 offer 类型的消息时,说明APP想和我们建立连接。我们把它设为“远程描述”,然后创建自己的“应答”(Answer),设为“本地描述”,再把这个Answer发回给APP。
-
当收到 candidate 类型的消息时,我们把它添加到peerConnection中,帮助WebRTC建立网络连接。
-
-
页面加载 (5): 我们让页面处于被动等待状态,一切由APP端发起。
-
确保信令服务器仍在运行。如果没有,请回到第一步的目录 signaling-server 并运行 node server.js。
-
用HTTP方式打开网页。直接双击index.html文件可能因安全策略导致JS无法正常工作。我们需要一个简单的HTTP服务器来托管web-client文件夹。
-
方法一 (推荐): 如果你电脑上有Node.js,可以安装一个全局的HTTP服务器。
Generated bash
Use code with caution.Bash# 只需要安装一次 npm install -g http-server # 进入 web-client 文件夹 cd web-client # 启动HTTP服务器 http-server命令行会显示一个地址,通常是 http://127.0.0.1:8080 或 http://<你的IP>:8080。
-
方法二: 如果你使用VS Code,可以安装一个叫 "Live Server" 的插件,右键点击index.html文件,选择 "Open with Live Server"。
-
-
在浏览器中访问页面。使用上一步得到的地址(如http://127.0.0.1:8080)打开网页。
-
打开浏览器开发者工具 (在Chrome中按F12或右键->检查),切换到 "Console" (控制台) 标签页。
你应该能看到:
-
网页上显示:“已连接到信令服务器,等待APP发起连接...”。
-
在控制台中,打印出:“成功连接到信令服务器!”和“WEB端已准备就绪,等待信令消息...”。
现在,你的WEB端已经像一个准备就绪的雷达站,静静地扫描着天空,等待着APP这个“飞机”的信号。
我们已经完成了第二步。这个网页现在已经具备了接收和播放WebRTC视频流的全部能力。下一步,我们将着手构建发射信号的“飞机”——APP端。
请确认WEB端运行正常后我们就可以开始最复杂的第三步:构建基础的APP发送端了。
第三步:构建基础的APP发送端 (Android)
目标: 创建一个Android应用,它能:
-
集成WebRTC和Socket.IO库。
-
请求屏幕录制权限。
-
连接到我们的信令服务器。
-
初始化WebRTC,并能创建和发送Offer。
-
处理来自WEB端的Answer和Candidate。
核心: 在这一步,我们先不急于将屏幕画面真正接入WebRTC,而是先搭建好APP端的WebRTC框架,并确保它能与WEB端成功完成信令交互。这叫“打通信令链路”。
技术栈:
-
Android Studio (请确保已安装)
-
Java 或 Kotlin (本教程将以Java为例,逻辑与Kotlin相通)
-
Google官方WebRTC库
-
Socket.IO官方Java客户端库
操作指南
-
打开Android Studio。
-
选择 "File" -> "New" -> "New Project..."。
-
选择 "Empty Views Activity" 模板,点击 "Next"。
-
填写项目信息:
-
Name: WebRTC Sender (或其他你喜欢的名字)
-
Package name: (例如 com.example.webrtcsender)
-
Language: Java
-
Minimum SDK: 选择 API 21: Android 5.0 (Lollipop) 或更高版本。WebRTC和屏幕录制需要这个版本以上。
-
-
打开项目左侧导航栏 Gradle Scripts 下的 build.gradle (Module :app) 文件,在 dependencies 代码块中添加以下两行:
Generated groovydependencies { // ... 其他默认依赖 ... // 添加WebRTC库 implementation 'org.webrtc:google-webrtc:1.0.32006' // 使用一个稳定版本 // 添加Socket.IO客户端库 implementation ('io.socket:socket.io-client:2.0.1') { // 排除它自带的 okhttp,避免与Android项目中的版本冲突 exclude group: 'org.json', module: 'json' } } -
点击 "Finish",等待项目构建完成。
添加后,Android Studio顶部会提示 "Sync Now",点击它,等待库下载完成。
打开 app/src/main/AndroidManifest.xml 文件,在 <manifest> 标签内,<application> 标签外,添加必要的权限:
Generated xml<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 如果需要用摄像头测试,可以加上相机权限 --> <!-- <uses-permission android:name="android.permission.CAMERA" /> --> <!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->
同时,为了允许与本地的http协议信令服务器通信(默认高版本Android禁止),需要在 <application> 标签内添加 android:usesCleartextTraffic="true":
Generated xml
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WebRTCSender"
android:usesCleartextTraffic="true"> <!-- 添加这一行 -->
<activity ...>
...
</activity>
</application>
打开 app/src/main/res/layout/activity_main.xml,我们创建一个简单的按钮来触发连接。将内容替换为:
Generated xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/start_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始连接"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
这是最复杂的部分。我们将把所有逻辑都放在 MainActivity.java 中。请仔细阅读注释,理解每一步的作用。
将 MainActivity.java 的全部内容替换为以下代码:
Generated java
package com.example.webrtcsender; // 请确保这是你自己的包名
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import io.socket.client.IO;
import io.socket.client.Socket;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// !!! 重要:请将这里的IP地址换成你自己的电脑IP地址 !!!
private static final String SERVER_URL = "http://192.168.1.10:3000";
private Socket socket;
private PeerConnection peerConnection;
private PeerConnectionFactory peerConnectionFactory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 1. 初始化WebRTC
initWebRTC();
// 2. 找到按钮并设置点击事件
Button startButton = findViewById(R.id.start_button);
startButton.setOnClickListener(v -> {
// 3. 连接信令服务器
connectToSignalingServer();
});
}
private void initWebRTC() {
// 初始化PeerConnectionFactory
PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(this).createInitializationOptions());
peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory();
}
private void connectToSignalingServer() {
try {
// 4. 创建Socket实例并连接
socket = IO.socket(SERVER_URL);
// 5. 设置信令事件监听
socket.on(Socket.EVENT_CONNECT, args -> {
Log.d(TAG, "成功连接到信令服务器!");
// 连接成功后,可以开始创建PeerConnection并发起呼叫
runOnUiThread(() -> {
call();
});
}).on("message", args -> {
// 监听从WEB端发来的消息 (Answer, Candidate)
JSONObject data = (JSONObject) args[0];
try {
String type = data.getString("type");
Log.d(TAG, "收到消息,类型: " + type);
if (type.equals("answer")) {
handleAnswer(data.getJSONObject("payload"));
} else if (type.equals("candidate")) {
handleRemoteCandidate(data.getJSONObject("payload"));
}
} catch (JSONException e) {
e.printStackTrace();
}
}).on(Socket.EVENT_DISCONNECT, args -> {
Log.d(TAG, "与信令服务器断开连接");
});
// 启动连接
socket.connect();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private void call() {
Log.d(TAG, "开始呼叫流程...");
// 6. 创建PeerConnection
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
peerConnection = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnection.Observer() {
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
// 当本地找到ICE候选者时,发送给对方
Log.d(TAG, "onIceCandidate: " + iceCandidate);
try {
JSONObject payload = new JSONObject();
payload.put("type", "candidate");
payload.put("label", iceCandidate.sdpMLineIndex);
payload.put("id", iceCandidate.sdpMid);
payload.put("candidate", iceCandidate.sdp);
JSONObject message = new JSONObject();
message.put("type", "candidate");
message.put("payload", payload);
socket.emit("message", message);
} catch (Exception e) {
e.printStackTrace();
}
}
// 其他回调暂时忽略
@Override public void onSignalingChange(PeerConnection.SignalingState signalingState) {}
@Override public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {}
@Override public void onIceConnectionReceivingChange(boolean b) {}
@Override public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {}
@Override public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {}
@Override public void onAddStream(org.webrtc.MediaStream mediaStream) {}
@Override public void onRemoveStream(org.webrtc.MediaStream mediaStream) {}
@Override public void onDataChannel(org.webrtc.DataChannel dataChannel) {}
@Override public void onRenegotiationNeeded() {}
@Override public void onAddTrack(org.webrtc.RtpReceiver rtpReceiver, org.webrtc.MediaStream[] mediaStreams) {}
});
// 7. 创建Offer
// 注意:即使我们现在不发送任何视频流,也需要创建Offer来启动连接过程
// 'OfferToReceiveVideo': true 表示我们虽然不发,但准备好接收对方(如果有)的视频
MediaConstraints sdpMediaConstraints = new MediaConstraints();
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"));
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
peerConnection.createOffer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(TAG, "Offer 创建成功!");
peerConnection.setLocalDescription(this, sessionDescription);
// 将Offer发送给信令服务器
try {
JSONObject payload = new JSONObject();
payload.put("type", sessionDescription.type.canonicalForm());
payload.put("sdp", sessionDescription.description);
JSONObject message = new JSONObject();
message.put("type", "offer");
message.put("payload", payload);
socket.emit("message", message);
Log.d(TAG, "Offer 已发送");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override public void onSetSuccess() { Log.d(TAG, "setLocalDescription 成功!"); }
@Override public void onCreateFailure(String s) { Log.e(TAG, "Offer 创建失败: " + s); }
@Override public void onSetFailure(String s) { Log.e(TAG, "setLocalDescription 失败: " + s); }
}, sdpMediaConstraints);
}
private void handleAnswer(JSONObject answerSdp) {
Log.d(TAG, "处理 Answer");
try {
String sdp = answerSdp.getString("sdp");
peerConnection.setRemoteDescription(new SdpObserver() {
@Override public void onCreateSuccess(SessionDescription sessionDescription) {}
@Override public void onSetSuccess() { Log.d(TAG, "setRemoteDescription 成功!"); }
@Override public void onCreateFailure(String s) {}
@Override public void onSetFailure(String s) { Log.e(TAG, "setRemoteDescription 失败: " + s); }
}, new SessionDescription(SessionDescription.Type.ANSWER, sdp));
} catch (Exception e) {
e.printStackTrace();
}
}
private void handleRemoteCandidate(JSONObject candidate) {
Log.d(TAG, "处理远程 Candidate");
try {
peerConnection.addIceCandidate(new IceCandidate(
candidate.getString("id"),
candidate.getInt("label"),
candidate.getString("candidate")
));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (socket != null) {
socket.disconnect();
socket.close();
}
if (peerConnection != null) {
peerConnection.close();
}
}
}
代码解析(必读):
-
配置 (SERVER_URL): 再次强调,把IP地址换成你的电脑IP!
-
initWebRTC(): 初始化WebRTC工厂类,这是创建其他WebRTC对象的基础。
-
connectToSignalingServer(): 创建socket.io客户端,设置各种事件监听(连接成功、收到消息、断开连接),然后发起连接。
-
call(): 这是APP作为“呼叫方”的核心流程。
-
创建PeerConnection实例,并传入STUN服务器和一个Observer。这个Observer非常重要,onIceCandidate回调就在这里,用于将本地找到的网络路径发送出去。
-
调用peerConnection.createOffer()。这是向WEB端发出的“我们连接吧”的请求。
-
在createOffer的onCreateSuccess回调中,先调用setLocalDescription将这个Offer设置为自己的状态,然后通过socket.emit把它发送给信令服务器。
-
-
handleAnswer() / handleRemoteCandidate(): 这两个方法分别用于处理从WEB端通过信令服务器转发来的Answer和Candidate消息。
现在,我们到了激动人心的时刻!我们将让APP和WEB第一次“对话”。
-
启动所有组件:
-
信令服务器: 确保 signaling-server 目录下的 node server.js 正在运行。
-
WEB接收端: 确保 web-client 正在被 http-server 托管,并在浏览器中打开了 index.html 页面。打开浏览器的开发者工具(F12)并切换到Console标签页,这是观察WEB端日志的关键!
-
-
运行APP:
-
用USB线连接你的Android手机到电脑(确保已开启开发者模式和USB调试)。
-
在Android Studio中,点击顶部的绿色三角运行按钮 (▶),选择你的手机作为目标设备。
-
APP会安装并启动。
-
-
开始测试:
-
在手机上,点击“开始连接”按钮。
-
同时观察三方的日志!
-
Android Studio Logcat: 过滤TAG为MainActivity,你应该能看到连接服务器、创建Offer、发送Offer、收到Answer、收到Candidate、设置成功等一系列日志。
-
浏览器开发者工具Console: 你应该能看到收到Offer、创建Answer、发送Answer、收到Candidate等日志。
-
信令服务器命令行窗口: 你会看到客户端连接、消息接收和转发的日志。
-
-
预期结果:
如果一切顺利,APP和WEB端会通过信令服务器交换一系列SDP和ICE Candidate信息。虽然此时WEB端仍然是黑屏(因为我们还没发送视频),但P2P连接的“握手”过程已经完成了!你可能会在WEB端的status区域看到状态从“等待连接”变为“收到请求”、“发送应答”等。
如果连接成功建立,说明最复杂的部分已经打通。
如果失败,如何排查?
-
连接不上信令服务器: 检查APP和WEB端的IP地址配置是否正确;检查手机和电脑是否在同一Wi-Fi下;检查电脑防火墙。
-
日志中出现createOffer或createAnswer失败: 通常是WebRTC初始化问题或参数错误。
-
日志中出现setLocal/RemoteDescription失败: 可能是SDP格式问题,或者状态机错误(比如在错误的时间调用了它)。
-
仔细比对三方的日志,看消息是否正确地从一端发出,经过服务器,然后被另一端接收。
完成这一步,我们就等于建好了数据传输的“管道”。下一步,也是最后一步,就是往这个管道里“灌水”——将APP的屏幕画面数据灌进去。
请完成这一步的测试,确认信令交互成功后,我们再进行最后一步。
第四步:注入屏幕视频流并完成监控
目标:
-
APP端:获取屏幕录制权限,创建屏幕视频采集器,将视频轨道(Video Track)添加到PeerConnection中。
-
WEB端:无需修改代码,它已经准备好接收并播放任何到来的视频流。
此部分代码可使用AI自动生成,不是很复杂。
更多推荐


所有评论(0)