本地部署简易双人视频通话系统

本项目是一个基于 Web 技术的视频通话系统,可在本地部署后在局域网内一对一使用,或者部署在服务器上在公网上使用。其中使用 Python 的 Flask 框架作为后端,SocketIO 实现实时通信,WebRTC 完成点对点音视频传输,支持浏览器间双人通话。本项目支持本地部署,非常适合学习和实际应用。需要的配置环境较为简单。感谢 Open AI 在其中的帮助。

完整的项目代码已上传至 github ,欢迎前去查看下载,并在本地部署。

1. 项目基本框架

查看如何在本地部署 ——>

获取完整代码 ——>

整个系统分为三部分:

  1. 前端: 使用 HTML + JavaScript 进行页面构建,WebRTC 处理本地摄像头与远程视频流,SocketIO 实现信令通信。
  2. 后端: 使用 Flask 提供静态文件服务,SocketIO 用于转发信令信息(如 offer、answer、ICE candidate)。
  3. 通信机制: 客户端通过 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 描述信息。
    • emitoffer 发送给指定房间(即目标用户)
@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给我们提供的外部连接,就可以进入网页,成功匹配视频通话了!

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

发表评论

滚动至顶部