初学者 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 复制代码
相关推荐
ssshooter6 小时前
看完就懂 useSyncExternalStore
前端·javascript·react.js
Live000008 小时前
在鸿蒙中使用 Repeat 渲染嵌套列表,修改内层列表的一个元素,页面不会更新
前端·javascript·react native
柳杉8 小时前
使用Ai从零开发智慧水利态势感知大屏(开源)
前端·javascript·数据可视化
球球pick小樱花8 小时前
游戏官网前端工具库:海内外案例解析
前端·javascript·css
喝水的长颈鹿8 小时前
【大白话前端 02】网页从解析到绘制的全流程
前端·javascript
用户14536981458789 小时前
VersionCheck.js - 让前端版本更新变得简单优雅
前端·javascript
codingWhat9 小时前
整理「祖传」代码,就是在开发脚手架?
前端·javascript·node.js
码路飞9 小时前
写了个 AI 聊天页面,被 5 种流式格式折腾了一整天 😭
javascript·python
Lee川9 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
颜酱9 小时前
单调队列:滑动窗口极值问题的最优解(通用模板版)
javascript·后端·算法