初学者 WebRTC 视频连接教程:脚本逻辑深度解析

引言

WebRTC(Web Real-Time Communication)是一项令人惊叹的 Web 技术,它能让浏览器间实现实时音视频通信,无需额外插件。本文将基于最终版本的代码,深入剖析脚本逻辑,助力初学者掌握使用 WebRTC 连接视频的核心要点。

整体思路

在 Vue 项目里运用 WebRTC 连接视频,主要包含以下关键步骤:

  1. 解析视频流 URL。
  2. 创建 RTCPeerConnection 实例,建立 WebRTC 连接。
  3. 交换会话描述协议(SDP)信息。
  4. 处理 ICE 候选,完成连接。
  5. 监听远程流并播放视频。

脚本逻辑详细解析

1. 导入必要的依赖与定义变量

xml 复制代码
index.vue
<script setup>
import { ref, onMounted, computed, onUnmounted } from "vue";
import { ElMessage, ElLoading } from "element-plus";
import CircleButtonControlPanel from "./CircleButtonControlPanel.vue";

// 视频流地址
const videoStreamUrl = ref("webrtc://111.11.115.110:8080/live/xx");
// 视频播放器引用
const videoPlayer = ref(null);

// 当前时间戳
const currentTimestamp = computed(() => {
  const now = new Date();
  return now.toLocaleString();
});
</script>
  • 依赖导入 :从 vue 导入响应式相关 API、生命周期钩子和计算属性;从 element-plus 导入消息提示和加载组件;引入自定义的按钮控制面板组件。

  • 变量定义

    • videoStreamUrl:存储 WebRTC 视频流的 URL。
    • videoPlayer:引用页面上的 <video> 元素。
    • currentTimestamp:计算属性,用于显示当前时间。

2. URL 解析函数

ini 复制代码
index.vue
// 解析 URL 参数
const parseUrl = (url) => {
  const match = url.match(/^webrtc://([^/:]+):(\d+)/([^?]+)(?.*)?$/);
  if (!match) return {};
  const server = match[1];
  const port = match[2];
  const path = match[3];
  const queryString = match[4] || "";
  const userQuery = {};
  queryString.slice(1).split('&').forEach((param) => {
    if (param) {
      const [key, value] = param.split('=');
      userQuery[key] = decodeURIComponent(value || "");
    }
  });
  return { server, port, url, path, userQuery };
};

parseUrl 函数借助正则表达式解析 WebRTC URL,提取服务器地址、端口、路径和查询参数,方便后续构建请求 URL。

3. WebRTC 连接核心函数

ini 复制代码
index.vue
// 连接 WebRTC 流
const connectWebRTCStream = async () => {
  let loadingInstance;
  let peerConnection;
  try {
    // 显示加载提示
    loadingInstance = ElLoading.service({
      lock: true,
      text: 'WebRTC 连接中...',
      background: 'rgba(0, 0, 0, 0.7)'
    });

    // 检查 videoStreamUrl 是否有值
    if (!videoStreamUrl.value) {
      console.error('视频流地址为空');
      ElMessage.error('视频流地址为空,无法连接');
      return;
    }

    let retryCount = 0;
    const maxRetries = 3;

    const attemptConnection = async () => {
      try {
        peerConnection = new RTCPeerConnection({
          iceServers: [
            { urls: 'stun:stun.l.google.com:19302' },
            { urls: 'turn:your-turn-server.com', username: 'your-username', credential: 'your-credential' }
          ],
          iceTransportPolicy: 'relay' // 优先使用主机候选
        });

        // 监听 ICE 候选收集完成事件
        let iceGatheringComplete = new Promise((resolve) => {
          peerConnection.onicegatheringstatechange = () => {
            if (peerConnection.iceGatheringState === 'complete') {
              resolve();
            }
          };
        });

        // 监听远程流
        peerConnection.ontrack = (event) => {
          const remoteStream = event.streams[0];
          videoPlayer.value.srcObject = remoteStream;
        };

        // 解析视频流 URL
        const urlParams = parseUrl(videoStreamUrl.value);

        // 只添加视频 transceiver 并设置编码参数
        const transceiver = peerConnection.addTransceiver("video", {
          direction: "recvonly",
          streams: [],
          init: {
            codecs: [
              {
                mimeType: 'video/VP8',
                clockRate: 90000,
                rtcpFeedback: [
                  { type: 'nack' },
                  { type: 'nack', parameter: 'pli' },
                  { type: 'ccm', parameter: 'fir' },
                  { type: 'goog-remb' }
                ]
              }
            ]
          }
        });

        // 带宽自适应
        const observer = peerConnection.connectionState;
        const handleConnectionChange = () => {
          if (peerConnection.connectionState === 'connected') {
            const parameters = transceiver.sender.getParameters();
            if (!parameters.encodings) {
              parameters.encodings = [{ scalabilityMode: 'S3T3_KEY' }];
            } else {
              parameters.encodings[0].scalabilityMode = 'S3T3_KEY';
            }
            transceiver.sender.setParameters(parameters);
          }
        };
        peerConnection.onconnectionstatechange = handleConnectionChange;

        // 创建 offer
        const offer = await peerConnection.createOffer();
        await peerConnection.setLocalDescription(offer);

        // 等待 ICE 候选收集完成
        await iceGatheringComplete;

        const data = {
          api: "http://111.11.115.110:8080/rtc/v1/play/",
          streamurl: urlParams.url,
          clientip: null,
          sdp: peerConnection.localDescription.sdp
        };

        // 构建请求 URL
        const { port = 1985, userQuery } = urlParams;
        let api = userQuery.play || "/rtc/v1/play/";
        if (api.lastIndexOf("/") !== api.length - 1) api += "/";
        let url = `http://${urlParams.server}:${port}${api}`;
        for (const key in userQuery) {
          if (key !== "api" && key !== "play") {
            url += `&${key}=${userQuery[key]}`;
          }
        }
        const newBaseUrl = "111.11.115.110:8080";
        const originalPath = url.replace(/^https?://[^/]+/, "");
        url = newBaseUrl + originalPath;

        // 发送 offer 请求,添加超时设置
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 10000);

        const response = await fetch(url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(data),
          signal: controller.signal
        });

        clearTimeout(timeoutId);

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const answerData = await response.json();
        await peerConnection.setRemoteDescription(new RTCSessionDescription({ type: "answer", sdp: answerData.sdp }));

        console.log('WebRTC 连接成功');
        ElMessage.success('WebRTC 连接成功');
      } catch (error) {
        if (retryCount < maxRetries) {
          retryCount++;
          console.log(`连接失败,正在进行第 ${retryCount} 次重试...`, error);
          ElMessage.warning(`WebRTC 连接失败,正在进行第 ${retryCount} 次重试...`);
          await new Promise(resolve => setTimeout(resolve, 2000));
          await attemptConnection();
        } else {
          console.error('WebRTC 连接失败,已达到最大重试次数', error);
          ElMessage.error('WebRTC 连接失败,已达到最大重试次数');
          if (peerConnection) {
            peerConnection.close();
          }
        }
      }
    };

    await attemptConnection();
  } catch (error) {
    console.error('连接过程中出现错误:', error);
  } finally {
    // 隐藏加载提示
    if (loadingInstance) {
      loadingInstance.close();
    }
  }

  // 组件卸载时关闭连接
  onUnmounted(() => {
    if (peerConnection) {
      peerConnection.close();
    }
  });
};

详细步骤

  1. 加载提示 :使用 ElLoading 显示加载提示,提升用户体验。

  2. URL 检查 :若 videoStreamUrl 为空,输出错误信息并终止连接。

  3. 创建 RTCPeerConnection

    • iceServers:配置 STUN 和 TURN 服务器,帮助穿越 NAT 网络。
    • iceTransportPolicy: 'relay':优先使用主机候选。
  4. 监听 ICE 候选收集完成 :借助 onicegatheringstatechange 事件,确保在发送 SDP 时包含所有 ICE 候选。

  5. 监听远程流 :当接收到远程流时,将其赋值给 <video> 元素的 srcObject

  6. 设置视频编码参数 :在 addTransceiver 中设置视频编码为 VP8,并添加 RTCP 反馈机制,提升视频传输质量。

  7. 带宽自适应 :监听 connectionState 变化,连接成功后调整视频编码的 scalabilityMode 以适应带宽。

  8. 创建并发送 Offer

    • createOffer:创建 Offer SDP。
    • setLocalDescription:设置本地描述。
    • 构建请求 URL 和数据,使用 fetch 发送 POST 请求。
  9. 处理 Answer :接收服务器返回的 Answer SDP,使用 setRemoteDescription 设置远程描述。

  10. 重试机制:连接失败时最多重试 3 次,每次间隔 2 秒。

  11. 资源清理 :组件卸载时,关闭 RTCPeerConnection 释放资源。

4. 生命周期钩子

scss 复制代码
index.vue

onMounted(() => {
  connectWebRTCStream();
});

在组件挂载完成后,调用 connectWebRTCStream 函数开始连接 WebRTC 视频流。

总结

`` 通过上述脚本逻辑,我们实现了一个完整的 WebRTC 视频连接功能,涵盖 URL 解析、连接建立、SDP 交换、ICE 候选处理、视频播放、错误处理和资源清理等关键步骤。初学者可以根据这个示例深入理解 WebRTC 的工作原理,并在此基础上进行扩展和优化。

js 复制代码
相关推荐
斯普信专业组1 小时前
2025 最好的Coze入门到精通教程(下)
前端·javascript·ui
超龄超能程序猿1 小时前
(5)从零开发 Chrome 插件:Vue3 Chrome 插件待办事项应用
javascript·vue.js·前端框架·json·html5
德育处主任1 小时前
p5.js 圆弧的用法
前端·javascript·canvas
PegasusYu1 小时前
Electron使用WebAssembly实现CRC-16 原理校验
javascript·electron·nodejs·wasm·webassembly·crc·crc16
初遇你时动了情3 小时前
react/vue vite ts项目中,自动引入路由文件、 import.meta.glob动态引入路由 无需手动引入
javascript·vue.js·react.js
摇滚侠3 小时前
JavaScript 浮点数计算精度错误示例
开发语言·javascript·ecmascript
天蓝色的鱼鱼4 小时前
JavaScript垃圾回收:你不知道的内存管理秘密
javascript·面试
waillyer5 小时前
taro跳转路由取值
前端·javascript·taro
yume_sibai5 小时前
Vue 生命周期
前端·javascript·vue.js
讨厌吃蛋黄酥6 小时前
🌟 React Router Dom 终极指南:二级路由与 Outlet 的魔法之旅
前端·javascript