123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- import { defineStore } from "pinia";
- import {
- MSG_TYPE,
- MESSAGE_TYPE_USER,
- MESSAGE_TYPE_GROUP,
- } from "@/common/constant/msgType";
- import { useWebSocketStore } from "@/stores/modules/webSocketStore";
- import * as Constant from "@/common/constant/Constant";
- export const useWebRTCStore = defineStore("webrtc", {
- state: () => ({
- // WebRTC 连接实例
- peerConnection: null,
- // ICE 候选信息
- iceCandidates: [],
- pendingIceCandidates: [], // 缓存未处理的候选
- // 连接状态
- connectionState: "disconnected",
- // 媒体流
- localStream: null, // 本地媒体流
- remoteStream: null, // 远端媒体流
- streamType: "audio", //"audio",
- // 视频元素引用
- localVideoElement: null,
- remoteVideoElement: null,
- // 媒体控制状态
- isVideoEnabled: true,
- isAudioEnabled: true,
- // 通话类型
- callType: "audio", // 'audio' 或 'video'
- // 是否是发起方
- isCaller: false,
- // 配置项
- config: {
- iceServers: [
- { urls: "stun:stun.l.google.com:19302" },
- // 可以添加更多 STUN/TURN 服务器
- ],
- },
- imSate: {
- videoCallModal: false, // 视频通话模态框
- callName: "", // 通话对象名称
- fromUserUuid: "", // 通话对象 uuid
- },
- }),
- actions: {
- //
- bindRemoteAudio() {
- // video && audio
- // 如果已经存在 audio 元素,先移除旧的
- const oldAudio = document.getElementById(`remote-audio`);
- if (oldAudio) {
- oldAudio.remove();
- }
- // 创建新的 <audio> 元素
- const audioElement = document.createElement("audio");
- audioElement.id = `remote-audio`;
- audioElement.autoplay = true; // 自动播放
- audioElement.muted = false; // 取消静音
- audioElement.controls = true; // 显示控制条(可选)
- audioElement.srcObject = this.remoteStream;
- // 添加到 DOM(可以放在任意位置,比如 body)
- document.body.appendChild(audioElement);
- console.log("✅ 远程音频已绑定到 <audio> 元素");
- },
- // 初始化 WebRTC 连接
- initConnection(isCaller, video) {
- this.cleanup();
- this.isCaller = isCaller;
- this.streamType = video ? "video" : "audio";
- const wsStore = useWebSocketStore();
- try {
- this.peerConnection = new RTCPeerConnection();
- // 设置事件监听: 对等方收到ice信息后,通过调用 addIceCandidate 将接收的候选者信息传递给浏览器的ICE代理
- this.peerConnection.onicecandidate = (event) => {
- if (event.candidate) {
- let candidate = {
- type: this.isCaller ? "offer_ice" : "answer_ice",
- iceCandidate: event.candidate,
- };
- wsStore.sendMessage({
- content: JSON.stringify(candidate),
- type: Constant.MESSAGE_TRANS_TYPE,
- });
- this.iceCandidates.push(event.candidate);
- }
- };
- // 监听 ICE 状态变化
- this.peerConnection.onconnectionstatechange = () => {
- this.connectionState = this.peerConnection.connectionState;
- };
- // 当连接成功后,从里面获取语音视频流: 监听 ICE candidate:包含语音视频流
- this.peerConnection.ontrack = (event) => {
- // 添加远程媒体流
- if (!this.remoteStream) {
- this.remoteStream = new MediaStream();
- }
- // 添加远程媒体流
- event.streams[0].getTracks().forEach((track) => {
- this.remoteStream.addTrack(track);
- });
-
- };
- // 监听 ICE 连接状态(关键修复!)
- this.peerConnection.oniceconnectionstatechange = () => {
- const state = this.peerConnection.iceConnectionState;
- if (state === "connected") {
- console.log("✅ P2P 连接成功,可以开始语音通话!");
- if(this.streamType == "audio") this.bindRemoteAudio();
- } else if (state === "failed") {
- console.error("❌ ICE 连接失败,尝试重启...");
- this.restartICE();
- }
- };
- console.log("WebRTC 连接初始化成功");
- } catch (error) {
- console.error("初始化 WebRTC 连接失败:", error);
- this.cleanup();
- throw error;
- }
- },
- // 添加本地媒体流
- async addLocalStream(stream) {
- if (!this.peerConnection) {
- throw new Error("WebRTC 连接未初始化");
- }
- this.localStream = stream;
- stream.getTracks().forEach((track) => {
- this.peerConnection.addTrack(track, stream);
- });
- },
- // 创建 Offer
- async createOffer() {
- if (!this.peerConnection) {
- throw new Error("WebRTC 连接未初始化");
- }
- try {
- const offer = await this.peerConnection.createOffer();
- await this.peerConnection.setLocalDescription(offer);
- return offer;
- } catch (error) {
- console.error("创建 Offer 失败:", error);
- throw error;
- }
- },
- // 创建 Answer
- async createAnswer() {
- if (!this.peerConnection) {
- throw new Error("WebRTC 连接未初始化");
- }
- try {
- const answer = await this.peerConnection.createAnswer();
- await this.peerConnection.setLocalDescription(answer);
- return answer;
- } catch (error) {
- console.error("创建 Answer 失败:", error);
- throw error;
- }
- },
- // 设置远程描述后处理缓存
- async setRemoteDescription(desc) {
- await this.peerConnection.setRemoteDescription(desc);
- // 处理缓存的候选
- while (this.pendingIceCandidates.length > 0) {
- const candidate = this.pendingIceCandidates.shift();
- await this.peerConnection
- .addIceCandidate(candidate)
- .catch((e) => console.error(e));
- }
- },
- // 添加 ICE 候选
- async addIceCandidate(candidate) {
- if (!candidate) {
- console.warn("收到空的 ICE 候选");
- return;
- }
- // 如果是候选结束信号(candidate:null)
- if (candidate.candidate === "") {
- console.log("ICE 候选收集完成");
- return;
- }
- try {
- // 确保 PeerConnection 和远程描述已就绪
- if (!this.peerConnection) {
- this.pendingIceCandidates.push(candidate);
- return;
- }
- // Answer 方必须等待远程描述
- if (!this.peerConnection.remoteDescription && !this.isCaller) {
- this.pendingIceCandidates.push(candidate);
- return;
- }
- await this.peerConnection.addIceCandidate(candidate);
- console.log("✅ 成功添加 ICE 候选:", candidate.candidate);
- } catch (error) {
- console.error("❌ 添加 ICE 候选失败:", error);
- // 失败后重试缓存
- this.pendingIceCandidates.push(candidate);
- }
- },
- // 清理资源:挂断
- cleanup() {
- if (this.peerConnection) {
- this.peerConnection.close();
- this.peerConnection = null;
- }
- if (this.localStream) {
- this.localStream.getTracks().forEach((track) => track.stop());
- this.localStream = null;
- }
- if (this.remoteStream) {
- this.remoteStream.getTracks().forEach((track) => track.stop());
- this.remoteStream = null;
- }
- this.iceCandidates = [];
- this.connectionState = "disconnected";
- },
- },
- });
|