本项目是一个基于 Web 技术的视频通话系统,可在本地部署后在局域网内一对一使用,或者部署在服务器上在公网上使用。其中使用 Python 的 Flask 框架作为后端,SocketIO 实现实时通信,WebRTC 完成点对点音视频传输,支持浏览器间双人通话。本项目支持本地部署,非常适合学习和实际应用。需要的配置环境较为简单。感谢 Open AI 在其中的帮助。
完整的项目代码已上传至 github ,欢迎前去查看下载,并在本地部署。
1. 项目基本框架
整个系统分为三部分:
- 前端: 使用 HTML + JavaScript 进行页面构建,WebRTC 处理本地摄像头与远程视频流,SocketIO 实现信令通信。
- 后端: 使用 Flask 提供静态文件服务,SocketIO 用于转发信令信息(如 offer、answer、ICE candidate)。
- 通信机制: 客户端通过 SocketIO 建立连接,发送 offer / answer,交换 ICE 信息,最终建立 WebRTC 直连,实现音视频数据点对点传输。
文件架构如下:(目前共两个文件)
├── app.py # Flask 后端逻辑
├── templates/
│ └── index.html # 前端页面(包含 HTML、CSS 和 JS)
└── static/
└── (可扩展样式或资源)
2. 项目最终效果
在本地局域网部署并运行 server.py 文件后,利用两个设备访问服务器的部署地址并开始匹配,允许浏览器使用视频功能后即可使用。
运行效果如下:

其中最上方有匹配与退出功能,中间有聊天功能,最下方有两人的实时视频与关闭/开启摄像头和麦克风的功能。
3. 后端 python 代码逻辑
from flask import Flask, render_template, request
from flask_socketio import SocketIO, emit, join_room, leave_room
import socket
app = Flask(__name__, template_folder='templates')
socketio = SocketIO(app, cors_allowed_origins='*')
@app.route('/')
def index():
return render_template('index.html')
- Flask 启动一个简单服务器,渲染前端页面 index.html
- SocketIO 负责客户端间的实时事件传递,处理信令交换
@socketio.on('match')
def handle_match():
global waiting_player
if waiting_player is None:
waiting_player = request.sid
emit('status', '等待匹配中...')
else:
room_id = f"room_{waiting_player}_{request.sid}"
join_room(room_id)
socketio.server.enter_room(waiting_player, room_id)
rooms[request.sid] = room_id
rooms[waiting_player] = room_id
partners[request.sid] = waiting_player
partners[waiting_player] = request.sid
emit('matched', {'partner_id': waiting_player}, room=request.sid)
emit('matched', {'partner_id': request.sid}, room=waiting_player)
waiting_player = None
- 作用: 处理用户点击“开始匹配”的行为。
- 工作流程:
- 如果没人等待:
- 当前用户等待中,存入 waiting list
- 告知客户端:等待匹配中…
- 如果已经有用户等待:
- 创建房间 ID
- 当前用户与等待者都加入该房间
- 更新 room 和 partners 映射:每个用户对应房间名,用户之间互为 partner
- 通知双方已成功匹配
@socketio.on('video_offer')
def handle_video_offer(data):
partner_id = data['partner_id']
offer = data['offer']
emit('video_offer', {'offer': offer, 'partner_id': request.sid}, room=partner_id)
- 作用: 当用户 A 想建立视频通话时,发送一个 WebRTC 的
offer
给用户 B。 - 触发时机: A 创建
RTCPeerConnection
并调用createOffer()
后。 - 工作流程:
- 从
data
中获取目标用户partner_id
和生成的offer
描述信息。 - 用
emit
将offer
发送给指定房间(即目标用户)
- 从
@socketio.on('video_answer')
def handle_video_answer(data):
partner_id = data['partner_id']
answer = data['answer']
emit('video_answer', {'answer': answer, 'partner_id': request.sid}, room=partner_id)
- 作用: 用户 B 收到 offer 后,生成 answer 发送给用户 A。
- 触发时机:B 调用 createAnswer() 并设置远程描述后。
- 工作流程:
- 从 data 中获取目标用户 partner_id 和生成的 offer 描述信息。
- 用 emit 将 offer 发送给指定房间(即目标用户)
4. 前端 html + js 设计思路
socket.on("video_offer", (data) => {
partnerId = data.partner_id;
peerConnection = new RTCPeerConnection(iceServers);
peerConnection.ontrack = (event) => {
document.getElementById("remote-video").srcObject = event.streams[0];
};
peerConnection.onicecandidate = (event) => {
if (event.candidate && partnerId) {
socket.emit("ice_candidate", {
candidate: event.candidate,
partner_id: partnerId,
});
}
};
// 设置远端的 offer 描述
peerConnection
.setRemoteDescription(new RTCSessionDescription(data.offer))
.then(() =>
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
)
.then((stream) => {
localStream = stream;
document.getElementById("local-video").srcObject = stream;
stream
.getTracks()
.forEach((track) => peerConnection.addTrack(track, stream));
})
.catch((err) => {
console.warn("作为接收端加入,无本地流", err);
})
.finally(() => {
peerConnection
.createAnswer()
.then((answer) => peerConnection.setLocalDescription(answer))
.then(() => {
socket.emit("video_answer", {
answer: peerConnection.localDescription,
partner_id: partnerId,
});
});
});
});
- 接收到对方发来的 offer(视频连接请求);
- 设置远程描述 setRemoteDescription(data.offer);
- 获取本地摄像头与麦克风流;
- 添加到连接中,并创建 answer 回复;
- 通过 socket.emit(‘video_answer’) 发回给发起方。
socket.on("video_answer", (data) => {
peerConnection.setRemoteDescription(
new RTCSessionDescription(data.answer)
);
});
- 收到对方返回的 answer(视频连接响应);
- 设置为远程描述以完成 WebRTC 的双向连接建立。
peerConnection.ontrack = (event) => {
document.getElementById("remote-video").srcObject = event.streams[0];
};
- 接收到远端的视频流;
- 将其绑定到页面上的 video 元素进行播放
5. 本地部署与使用方式
1)运行代码,打开服务器
在运行前,记得安装python相关依赖!
运行 server.py 后,本机服务器作为后端,会在 http://localhost:6789 或者本机 IP地址 + : 6789 运行、打开服务器端口。理论上,此时访问该链接就可进入网页进行视频通话的匹配了。终端的命令行提供了具体的访问链接。
注意:这里的6789端口是代码里设置的,可以通过更改 index.html 中的代码来更换端口。
然而还有一点,项目中最关键的 getUserMedia 要求运行的服务器是安全的——这个函数使得浏览器可以获取你的视频与音频:也就是说,要么运行服务器有 http 协议,要么是在 localhost 运行。但是局域网下部署的服务器处于内网之中,通常是很难手动加入协议,或是没有 http 协议的。
诚然此时在 localhost 是可以成功获得视频的,但是同局域网下的外来设备就不能访问本地的 localhost。这意味着无法实时两人的视频功能。因此我采取的解决方法是使用内网穿透的方法,将本地的 localhost 映射到外部安全的 http 协议下的外网,从而使得 getUserMedia 函数能够使用。
2)内网穿透
如果你部署在了自己的云服务器,拥有带 http 的域名,就不用这一步了!
方法是前往 CPolar 官网下载软件,注册后可以打开,在左侧创建隧道,输入希望打开的端口号(也就是原来代码默认的是6789),它会将这个端口映射到一个 http 的域名下。当我们再像往常一样运行 server.py 文件打开 localhost 后,访问 CPolar给我们提供的外部连接,就可以进入网页,成功匹配视频通话了!

本项目逻辑清晰,代码量偏少,架构较为简易,功能比较简单,只实现了基本的视频通话与文字发送功能,但是可以本地部署,并让他人通过局域网访问,使用外部设备进行沟通。快来试试吧!