添加实时语音通话功能时,通常会使用 WebRTC 技术。WebRTC 是一种用于实时通信的协议,允许在对等网络之间直接传输音频和视频。在 Flask 中实现 WebRTC 可能稍微复杂一些,但下面是一个简化的示例,指导你如何将语音通话功能整合到我们之前的实时聊天应用中。

1. 修改 app.py

确保你已经安装 Flask、Flask-SocketIO 和其他依赖库。将 app.py 更新如下:

from flask import Flask, render_template
from flask_socketio import SocketIO, emit
import base64

app = Flask(__name__)
socketio = SocketIO(app)

@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('message')
def handle_message(msg):
    emit('message', msg, broadcast=True)

@socketio.on('image')
def handle_image(image_data):
    emit('image', image_data, broadcast=True)

@socketio.on('webrtc_offer')
def handle_webrtc_offer(data):
    emit('webrtc_offer', data, broadcast=True)

@socketio.on('webrtc_answer')
def handle_webrtc_answer(data):
    emit('webrtc_answer', data, broadcast=True)

@socketio.on('ice_candidate')
def handle_ice_candidate(data):
    emit('ice_candidate', data, broadcast=True)

if __name__ == '__main__':
    socketio.run(app)

2. 修改 index.html

在 index.html 中添加 WebRTC 的相关代码,更新后的代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>聊天应用</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.0.3/socket.io.js"></script>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        #messages { border: 1px solid #ccc; height: 300px; overflow-y: scroll; padding: 10px; }
        #form { display: flex; }
        #input { flex: 1; padding: 10px; }
        #send { padding: 10px; }
        #imageInput { display: none; }
    </style>
</head>
<body>

<h1>实时聊天应用</h1>
<div id="messages"></div>
<div id="form">
    <input id="input" autocomplete="off" placeholder="输入消息..." />
    <input type="file" id="imageInput" accept="image/*" />
    <button id="send">发送</button>
    <button id="uploadImage">上传图片</button>
    <button id="startCall">开始语音通话</button>
</div>

<audio id="remoteAudio" autoplay></audio>

<script>
    const socket = io();
    let localStream;
    let peerConnection;
    const config = {
        iceServers: [
            { urls: 'stun:stun.l.google.com:19302' } // STUN 服务器
        ]
    };

    document.getElementById('send').onclick = function() {
        const input = document.getElementById('input');
        socket.emit('message', input.value);
        input.value = '';
        input.focus();
    };

    document.getElementById('uploadImage').onclick = function() {
        document.getElementById('imageInput').click();
    };

    document.getElementById('imageInput').onchange = function(event) {
        const file = event.target.files[0];
        const reader = new FileReader();

        reader.onloadend = function() {
            socket.emit('image', reader.result);
        };

        if (file) {
            reader.readAsDataURL(file);
        }
    };

    document.getElementById('startCall').onclick = async function() {
        localStream = await navigator.mediaDevices.getUserMedia({ audio: true });
        peerConnection = new RTCPeerConnection(config);

        localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));

        peerConnection.onicecandidate = event => {
            if (event.candidate) {
                socket.emit('ice_candidate', event.candidate);
            }
        };

        peerConnection.ontrack = event => {
            const remoteAudio = document.getElementById('remoteAudio');
            remoteAudio.srcObject = event.streams[0];
        };

        const offer = await peerConnection.createOffer();
        await peerConnection.setLocalDescription(offer);
        socket.emit('webrtc_offer', offer);
    };

    socket.on('message', function(msg) {
        const messages = document.getElementById('messages');
        messages.innerHTML += '<div>' + msg + '</div>';
        messages.scrollTop = messages.scrollHeight; 
    });

    socket.on('image', function(imageData) {
        const messages = document.getElementById('messages');
        messages.innerHTML += '<div><img src="' + imageData + '" style="max-width: 300px; max-height: 300px;" /></div>';
        messages.scrollTop = messages.scrollHeight; 
    });

    socket.on('webrtc_offer', async function(offer) {
        peerConnection = new RTCPeerConnection(config);
        peerConnection.setRemoteDescription(new RTCSessionDescription(offer));

        localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));

        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);
        socket.emit('webrtc_answer', answer);

        peerConnection.onicecandidate = event => {
            if (event.candidate) {
                socket.emit('ice_candidate', event.candidate);
            }
        };

        peerConnection.ontrack = event => {
            const remoteAudio = document.getElementById('remoteAudio');
            remoteAudio.srcObject = event.streams[0];
        };
    });

    socket.on('webrtc_answer', function(answer) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
    });

    socket.on('ice_candidate', function(candidate) {
        peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
    });

    document.getElementById('input').addEventListener('keypress', function(event) {
        if (event.key === 'Enter') {
            document.getElementById('send').click();
        }
    });
</script>

</body>
</html>

说明

  1. 获取用户媒体:使用 navigator.mediaDevices.getUserMedia 获取音频流,并将其传递到 WebRTC 连接中。
  2. STUN 服务器:添加一个 STUN 服务器(stun:stun.l.google.com:19302),用于获取 NAT(网络地址转换)和防火墙后的 IP 地址。
  3. 创建和处理 Offer:当点击“开始语音通话”按钮时,获取本地音频流并创建 WebRTC 连接,同时生成并发送 offer。
  4. 接收 Offer 和 Answer:当接收到远程 offer 时,建立连接并发送 answer。
  5. ICE 候选者管理:在建立连接过程中,处理 ICE 候选者,并将其发送到 socket 进行广播。

运行应用

在终端中运行 python app.py 并在浏览器中打开 http://127.0.0.1:5000/。你可以尝试使用多个标签页进行测试,确保你允许浏览器使用麦克风。

注意事项

  • 这个简单的示例是基于信任的用户环境。要在生产环境中使用 WebRTC,需要更复杂的信号处理机制(例如使用 TURN 服务器)、身份验证和数据安全性。
  • 语音质量可能会受到网络带宽和延迟的影响,要确保使用稳定的网络连接。
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐