Python项目集成WebRTC,实现Web视频通话
现在python项目越来越多,有需要需要在python项目里集成webrtc视频通话
WebRTC(Web Real-Time Communication)是一种用于在Web浏览器之间实时传输音频、视频和数据的开放标准和技术集合。 WebRTC 提供了一组 API 和协议,使得开发者可以直接在Web浏览器中实现点对点的实时通信,而无需使用第三方插件或应用程序。它通过使用浏览器内置的音频、视频和数据通道,实现了实时的音视频传输和数据传输。
点对点视频连接
根据上面,我们对基本WebRTC有了最基本的认识,下面就从点对点实际例子来从代码角度进一步了解其原理。
Peer:通信双方设备。
Signaling Server: 信令服务器,用于交互连接双方的信令数据(SDP、ICE等),以保证通信的对等连接建立。
NAT:处理私有网络和公共网络之间的地址转换问题(因为大多数设置都处于内网中,需要转换为公共网络才能进行外网访问)
STUN:用于发现设备的公共地址(通过NAT转换的公网地址),辅助穿越NAT进行点对点连接。
TURN:在无法建立直接连接时提供数据中继,确保通信的可靠性。对等连接异常时的兜底方案。
SDP:会话描述协议,用于描述和协商媒体会话的协议,它定义了会话的所有技术细节,包括媒体格式、编解码器、网络地址等。,
ICE:用于发现和选择最优网络路径的框架,确保在各种网络环境下都能成功建立和维持连接。
实现点对点连接:
没有使用STUN/TRUN服务器,可以使用Chrome提供的公共服务器stun:stun.l.google.com:19302
主要步骤如下:
1、和信令服务器建立连接,并获取自身的clientid作为唯一标识
2、申请方将信令通过信令服务器到达接受方
3、接受方接受,将发起方的信令保存到对等连接peer中,并且将自己的信令通过信令服务器给到发送方
4、发送方将接受方的信令数据保存到对等连接peer中,至此发送方-接受方对等连接建立完成
5、在发送方和接受方监听peer的stream,来获取视频流,然后展示在页面
服务端部分代码:
from flask import Flask,jsonify,request from flask_cors import CORS from flask_socket import Socket #import urllib.parse userlist={} app = Flask(__name__, static_folder='client/dist') CORS(app,cors_allowed_origins="*") socket = Socket(app,cors_allowed_origins='*') @socket.on('init') def client_init(data): print('Client init-----------',data) clientid=data.get("id") if userlist.get(clientid): emit('error', { message: '用户已登录!' }) elif clientid: userlist[clientid] = request.sid join_room(clientid) emit('init', {'id': clientid}) print(userlist) else: emit('error', { message: '无法取的用户id' }) @socket.on('disconnect') def client_disconnect(): clientid=get_key_by_value(userlist,request.sid) if clientid: del userlist[clientid] leave_room(clientid) print('Client disconnected---------',userlist) if __name__ == '__main__': socketio.run(app,host="0.0.0.0",port=5000,ssl_context=("keys/server.crt","keys/server.key")) #app.run(debug=True,host="0.0.0.0",port=5000,ssl_context=("keys/server.crt","keys/server.key"))
客户端代码:
import React, { Component } from 'react'; import _ from 'lodash'; import { socket, PeerConnection } from './communication'; import MainWindow from './components/MainWindow'; import CallWindow from './components/CallWindow'; import CallModal from './components/CallModal'; import UrlParse from 'url-parse'; class App extends Component { constructor() { super(); this.state = { callWindow: '', callModal: '', callFrom: '', localSrc: null, peerSrc: null }; this.pc = {}; this.config = null; this.startCallHandler = this.startCall.bind(this); this.endCallHandler = this.endCall.bind(this); this.rejectCallHandler = this.rejectCall.bind(this); } componentDidMount() { let MIN = 1000; let MAX = 9999; let num = Math.floor(Math.random() * ((MAX + 1) - MIN)) + MIN; //let clientid =""+num; const urlParser = new UrlParse(window.location.href, true); let clientid =urlParser.query.username || ""+num; socket .on('request', ({ from: callFrom }) => { this.setState({ callModal: 'active', callFrom }); }) .on('call', (data) => { if (data.sdp) { this.pc.setRemoteDescription(data.sdp); if (data.sdp.type === 'offer') this.pc.createAnswer(); } else this.pc.addIceCandidate(data.candidate); }) .on('end', this.endCall.bind(this, false)) .emit('init',{id:clientid}); } startCall(isCaller, friendID, config) { this.config = config; this.pc = new PeerConnection(friendID) .on('localStream', (src) => { const newState = { callWindow: 'active', localSrc: src }; if (!isCaller) newState.callModal = ''; this.setState(newState); }) .on('peerStream', (src) => this.setState({ peerSrc: src })) .start(isCaller); } rejectCall() { const { callFrom } = this.state; socket.emit('end', { to: callFrom }); this.setState({ callModal: '' }); } endCall(isStarter) { if (_.isFunction(this.pc.stop)) { this.pc.stop(isStarter); } this.pc = {}; this.config = null; this.setState({ callWindow: '', callModal: '', localSrc: null, peerSrc: null }); } render() { const { callFrom, callModal, callWindow, localSrc, peerSrc } = this.state; return ( <div> <MainWindow startCall={this.startCallHandler} /> {!_.isEmpty(this.config) && ( <CallWindow status={callWindow} localSrc={localSrc} peerSrc={peerSrc} config={this.config} mediaDevice={this.pc.mediaDevice} endCall={this.endCallHandler} /> ) } <CallModal status={callModal} startCall={this.startCallHandler} rejectCall={this.rejectCallHandler} callFrom={callFrom} /> </div> ); } } export default App;
演示:https://m.ovmeet.com:5000 可以支持android,IOS,微信内打开
总结
实现点对点通信,主要就是信令数据的交换sdp,通知对端地址(通信参数、IP地址等)以保证对等连接的成功建立,然后采集视频流传给对方。
其中信令服务器仅用于对等连接前的信令交换。NAT是将设备内网地址转换为外网公共地址。STUN来获取设置的公网地址。TURN服务器是用于对等连接不能Nat后做流转发。