手把手教你使用 WebRTC:基于 jswebrtc.js 的详细教程

手把手教你使用 WebRTC:基于 jswebrtc.js 的详细教程

前言

WebRTC(Web Real-Time Communication)是一种支持浏览器之间进行实时通信的技术,它允许网页在不借助任何插件的情况下实现音视频通话、文件共享等功能。在本文中,我们将深入分析 jswebrtc1.js 文件,详细解释代码的各个部分,并教会你如何使用 WebRTC 来实现一个简单的视频播放器。

代码整体结构

jswebrtc1.js 文件主要实现了一个 WebRTC 视频播放器,它包含了公共函数、主对象 JSWebrtcVideoElement 类和 Player 类。下面我们将逐步分析每个部分。

公共函数

javascript:d:/wzzn_work/Geeker-Admin/src/utils/jswebrtc1.js 复制代码
// 提取公共的日志记录函数
function logMessage(level, message) {
  switch (level) {
    case "debug":
      console.debug(message);
      break;
    case "info":
      console.info(message);
      break;
    case "error":
      console.error(message);
      break;
    default:
      console.log(message);
  }
}

// 统一的错误处理函数
function handleError(error, context) {
  logMessage("error", `[${context}] ${error.message}`);
  const errorEvent = new CustomEvent("webrtcError", { detail: error });
  window.dispatchEvent(errorEvent);
}

// 提取 addStyles 函数到全局作用域,提高代码复用性
function addStyles(el, styles) {
  Object.assign(el.style, styles);
}
  • logMessage 函数:根据传入的日志级别(debuginfoerror),使用不同的 console 方法输出日志信息。
  • handleError 函数:记录错误信息,并触发一个自定义的 webrtcError 事件,方便在其他地方监听和处理错误。
  • addStyles 函数:将传入的样式对象合并到指定元素的 style 属性中,提高代码的复用性。

主对象 JSWebrtc

javascript:d:/wzzn_work/Geeker-Admin/src/utils/jswebrtc1.js 复制代码
const JSWebrtc = {
  Player: null, // 播放器实例占位符
  VideoElement: null, // 视频元素构造器占位符

  /**
   * 创建所有匹配 .jswebrtc 类的视频元素
   */
  createVideoElements() {
    const elements = document.querySelectorAll(".jswebrtc");
    logMessage("info", `找到的 .jswebrtc 元素数量: ${elements.length}`);
    elements.forEach(element => {
      if (!element.dataset.isInitialized) {
        // 检查是否已经初始化
        try {
          new JSWebrtc.VideoElement(element); // 初始化每个视频元素
          element.dataset.isInitialized = true; // 标记为已初始化
        } catch (error) {
          handleError(error, "createVideoElements");
        }
      }
    });
  },

  /**
   * 将 URL 查询参数填充到对象
   */
  fillQuery(queryString, obj) {
    obj.userQuery = {};
    if (queryString.length === 0) return;

    const query = queryString.includes("?") ? queryString.split("?")[1] : queryString;
    const queries = query.split("&");
    queries.forEach(q => {
      const [key, value] = q.split("=");
      obj[key] = value;
      obj.userQuery[key] = value;
    });

    if (obj.domain) obj.vhost = obj.domain; // 兼容旧版 domain 参数
  },

  /**
   * 解析 RTMP/WebRTC 格式的 URL
   */
  parseUrl(rtmpUrl) {
    const urlElement = document.createElement("a");
    const url = rtmpUrl.replace("rtmp://", "http://").replace("webrtc://", "http://").replace("rtc://", "http://");
    urlElement.href = url;

    let vhost = urlElement.hostname;
    let app = urlElement.pathname.slice(1, urlElement.pathname.lastIndexOf("/"));
    let stream = urlElement.pathname.slice(urlElement.pathname.lastIndexOf("/") + 1);

    app = app.replace("...vhost...", "?vhost=");
    if (app.includes("?")) {
      const params = app.slice(app.indexOf("?"));
      app = app.slice(0, app.indexOf("?"));
      if (params.includes("vhost=")) {
        vhost = params.slice(params.indexOf("vhost=") + "vhost=".length);
        if (vhost.includes("&")) {
          vhost = vhost.slice(0, vhost.indexOf("&"));
        }
      }
    }

    if (urlElement.hostname === vhost && /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.test(urlElement.hostname)) {
      vhost = "__defaultVhost__";
    }

    let schema = "rtmp";
    if (rtmpUrl.includes("://")) {
      schema = rtmpUrl.slice(0, rtmpUrl.indexOf("://"));
    }

    let port = urlElement.port;
    if (!port) {
      const portMap = {
        http: 80,
        https: 443,
        rtmp: 1935,
        webrtc: 1985,
        rtc: 1985
      };
      port = portMap[schema];
    }

    const ret = {
      url: rtmpUrl,
      schema,
      server: urlElement.hostname,
      port,
      vhost,
      app,
      stream
    };
    JSWebrtc.fillQuery(urlElement.search, ret);
    return ret;
  },

  /**
   * 发送 HTTP POST 请求(用于信令交互)
   */
  async httpPost(url, data) {
    try {
      const response = await fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: data,
        timeout: 5000
      });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      handleError(error, "httpPost");
      throw error;
    }
  },

  /**
   * 设置新的 URL 并更新播放器
   */
  setUrl(newUrl, oldUrl) {
    const elements = document.querySelectorAll(".jswebrtc");
    elements.forEach(element => {
      if (element.playerInstance) {
        // 关闭之前的连接
        element.playerInstance.destroy(oldUrl);
        // 设置新的 URL
        element.playerInstance.setUrl(newUrl);
        try {
          // 重新开始加载新的连接
          element.playerInstance.startLoading();
        } catch (error) {
          handleError(error, "setUrl startLoading");
        }
      }
    });
  }
};

// 初始化:当文档加载完成后自动创建视频元素
let isVideoElementsCreated = false;
if (document.readyState === "complete") {
  if (!isVideoElementsCreated) {
    JSWebrtc.createVideoElements();
    isVideoElementsCreated = true;
  }
} else {
  document.addEventListener("DOMContentLoaded", () => {
    if (!isVideoElementsCreated) {
      JSWebrtc.createVideoElements();
      isVideoElementsCreated = true;
    }
  });
}
  • PlayerVideoElement:作为占位符,后续会被具体的类实例或构造函数填充。
  • createVideoElements 方法:查找所有类名为 .jswebrtc 的元素,并对未初始化的元素进行初始化。
  • fillQuery 方法:将 URL 查询参数解析并填充到指定对象中。
  • parseUrl 方法:解析 RTMP 或 WebRTC 格式的 URL,提取出协议、服务器、端口、虚拟主机等信息。
  • httpPost 方法:发送 HTTP POST 请求,用于信令交互。
  • setUrl 方法:更新播放器的 URL,并重新加载连接。
  • 初始化部分:在文档加载完成后,自动调用 createVideoElements 方法创建视频元素。

VideoElement

javascript:d:/wzzn_work/Geeker-Admin/src/utils/jswebrtc1.js 复制代码
JSWebrtc.VideoElement = (function () {
  "use strict";

  /**
   * 视频元素构造函数
   */
  class VideoElement {
    constructor(element) {
      if (element.playerInstance) {
        return;
      }
      const url = element.dataset.url;
      if (!url) {
        throw new Error("VideoElement has no `data-url` attribute");
      }

      this.container = element;
      addStyles(this.container, {
        display: "inline-block",
        position: "relative",
        minWidth: "80px",
        minHeight: "80px"
      });

      this.video = document.createElement("video");
      this.video.style.width = "100%";
      this.video.style.aspectRatio = "16 / 9";
      addStyles(this.video, { display: "block", width: "100%" });
      this.container.appendChild(this.video);

      this.playButton = document.createElement("div");
      this.playButton.innerHTML = VideoElement.PLAY_BUTTON;
      addStyles(this.playButton, {
        zIndex: 2,
        position: "absolute",
        top: "0",
        bottom: "0",
        left: "0",
        right: "0",
        maxWidth: "75px",
        maxHeight: "75px",
        margin: "auto",
        opacity: "0.7",
        cursor: "pointer"
      });
      this.container.appendChild(this.playButton);

      const options = { video: this.video };
      for (const option in element.dataset) {
        try {
          options[option] = JSON.parse(element.dataset[option]);
        } catch (err) {
          options[option] = element.dataset[option];
        }
      }
      // 强制设置 autoplay 为 true
      options.autoplay = true;

      try {
        this.player = new JSWebrtc.Player(url, options);
        element.playerInstance = this.player;
      } catch (error) {
        handleError(error, "VideoElement constructor");
      }

      if (options.poster && !options.autoplay) {
        options.decodeFirstFrame = false;
        this.poster = new Image();
        this.poster.src = options.poster;
        this.poster.addEventListener("load", this.posterLoaded.bind(this));
        addStyles(this.poster, {
          display: "block",
          zIndex: 1,
          position: "absolute",
          top: 0,
          left: 0,
          bottom: 0,
          right: 0
        });
        this.container.appendChild(this.poster);
      }

      if (this.player && !this.player.options.streaming) {
        this.container.addEventListener("click", this.onClick.bind(this));
      }
      if (options.autoplay && this.player) {
        this.playButton.style.display = "none";
        try {
          this.player.play();
        } catch (error) {
          handleError(error, "VideoElement play");
        }
        if (this.poster) {
          this.poster.style.display = "none";
        }
      }

      if (this.player && this.player.audioOut && !this.player.audioOut.unlocked) {
        let unlockAudioElement = this.container;
        if (options.autoplay) {
          this.unmuteButton = document.createElement("div");
          this.unmuteButton.innerHTML = VideoElement.UNMUTE_BUTTON;
          addStyles(this.unmuteButton, {
            zIndex: 2,
            position: "absolute",
            bottom: "10px",
            right: "20px",
            width: "75px",
            height: "75px",
            margin: "auto",
            opacity: "0.7",
            cursor: "pointer"
          });
          this.container.appendChild(this.unmuteButton);
          unlockAudioElement = this.unmuteButton;
        }
        this.unlockAudioBound = this.onUnlockAudio.bind(this, unlockAudioElement);
        unlockAudioElement.addEventListener("touchstart", this.unlockAudioBound, false);
        unlockAudioElement.addEventListener("click", this.unlockAudioBound, true);
      }
    }

    // 音频解锁处理方法
    onUnlockAudio(element, ev) {
      if (this.unmuteButton) {
        ev.preventDefault();
        ev.stopPropagation();
      }
      if (this.player && this.player.audioOut) {
        this.player.audioOut.unlock(() => {
          if (this.unmuteButton) {
            this.unmuteButton.style.display = "none";
          }
          element.removeEventListener("touchstart", this.unlockAudioBound);
          element.removeEventListener("click", this.unlockAudioBound);
        });
      }
    }

    // 点击事件处理(播放/暂停切换)
    onClick(ev) {
      if (this.player && this.player.isPlaying) {
        this.player.pause();
        this.playButton.style.display = "block";
      } else if (this.player) {
        this.player.play();
        this.playButton.style.display = "none";
        if (this.poster) {
          this.poster.style.display = "none";
        }
      }
    }

    // 播放按钮 SVG 模板
    static PLAY_BUTTON =
      '<svg style="max-width: 75px; max-height: 75px;" ' +
      'viewBox="0 0 200 200" alt="Play video">' +
      '<circle cx="100" cy="100" r="90" fill="none" ' +
      'stroke-width="15" stroke="#fff"/>' +
      '<polygon points="70, 55 70, 145 145, 100" fill="#fff"/>' +
      "</svg>";

    // 取消静音按钮 SVG 模板
    static UNMUTE_BUTTON =
      '<svg style="max-width: 75px; max-height: 75px;" viewBox="0 0 75 75">' +
      '<polygon class="audio-speaker" stroke="none" fill="#fff" ' +
      'points="39,13 22,28 6,28 6,47 21,47 39,62 39,13"/>' +
      '<g stroke="#fff" stroke-width="5">' +
      '<path d="M 49,50 69,26"/>' +
      '<path d="M 69,50 49,26"/>' +
      "</g>" +
      "</svg>";

    // 海报加载完成处理方法
    posterLoaded() {
      // 可添加海报加载完成后的额外逻辑
    }
  }

  return VideoElement;
})();
  • 构造函数:初始化视频元素,包括创建视频标签、播放按钮、海报等,并根据配置项创建播放器实例。
  • onUnlockAudio 方法:处理音频解锁事件,当用户点击取消静音按钮时,解锁音频并隐藏按钮。
  • onClick 方法:处理点击事件,实现播放和暂停的切换。
  • PLAY_BUTTONUNMUTE_BUTTON:静态属性,定义了播放按钮和取消静音按钮的 SVG 模板。
  • posterLoaded 方法:海报加载完成后的回调函数,可用于添加额外的逻辑。

Player

javascript:d:/wzzn_work/Geeker-Admin/src/utils/jswebrtc1.js 复制代码
JSWebrtc.Player = (function () {
  "use strict";

  // 节流函数
  function throttle(func, delay) {
    let timer = null;
    return function () {
      if (!timer) {
        func.apply(this, arguments);
        timer = setTimeout(() => {
          timer = null;
        }, delay);
      }
    };
  }

  /**
   * 播放器构造函数
   */
  class Player {
    constructor(url, options) {
      this.options = options || {};
      if (!url.match(/^webrtc?:\/\//)) {
        throw new Error("JSWebrtc just work with webrtc");
      }
      if (!this.options.video) {
        throw new Error("VideoElement is null");
      }

      this.urlParams = JSWebrtc.parseUrl(url);
      this.pc = null;
      this.autoplay = !!options.autoplay || false;
      this.paused = true;

      if (this.autoplay) {
        this.options.video.muted = true;
      }

      try {
        this.startLoading();
      } catch (error) {
        handleError(error, "Player constructor");
      }
    }

    // 核心连接流程
    async startLoading() {
      logMessage("info", "开始加载 WebRTC 连接");
      if (this.pc) {
        this.pc.close();
      }

      const RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
      if (!RTCPeerConnection) {
        const compatibilityError = new Error("当前浏览器不支持 WebRTC");
        handleError(compatibilityError, "startLoading");
        return;
      }

      this.pc = new RTCPeerConnection(null);

      // 避免重复添加事件监听器
      if (!this.pc.hasOwnProperty("ontrack")) {
        this.pc.ontrack = event => {
          logMessage("info", "接收到视频流");
          // 检查是否已经设置过 srcObject
          if (!this.options.video.srcObject) {
            this.options.video.srcObject = event.streams[0];
          }
          // 尝试播放视频并处理可能的错误
          this.options.video.play().catch(error => {
            handleError(error, "video play");
          });
        };
      }

      this.pc.addTransceiver("audio", { direction: "recvonly" });
      this.pc.addTransceiver("video", { direction: "recvonly" });

      try {
        const offer = await this.pc.createOffer();
        await this.pc.setLocalDescription(offer);

        const port = this.urlParams.port || 1985;
        let api = this.urlParams.userQuery.play || "/rtc/v1/play/";
        if (api.lastIndexOf("/") !== api.length - 1) {
          api += "/";
        }
        let url = `http://${this.urlParams.server}:${port}${api}`;
        for (const key in this.urlParams.userQuery) {
          if (key !== "api" && key !== "play") {
            url += `&${key}=${this.urlParams.userQuery[key]}`;
          }
        }

        // 替换 URL
        const newBaseUrl = "https://www.welltransai.com:8095";
        const originalPath = url.replace(/^https?:\/\/[^/]+/, '');
        url = newBaseUrl + originalPath;

        const data = {
          api: 'http://58.211.155.126:1985/rtc/v1/play/',
          streamurl: this.urlParams.url,
          clientip: null,
          sdp: offer.sdp
        };
        logMessage("info", `发送的 offer: ${JSON.stringify(data)}`);

        const res = await JSWebrtc.httpPost(url, JSON.stringify(data));
        logMessage("info", `接收到的 answer: ${JSON.stringify(res)}`);

        await this.pc.setRemoteDescription(new RTCSessionDescription({ type: "answer", sdp: res.sdp }));
      } catch (error) {
        handleError(error, "startLoading");
        throw error;
      }

      if (this.autoplay) {
        try {
          this.play();
        } catch (error) {
          handleError(error, "Player autoplay");
        }
      }
    }

    // 触发自定义错误事件
    triggerErrorEvent(error) {
      handleError(error, "triggerErrorEvent");
    }

    // 播放控制方法
    play(ev) {
      if (this.animationId) return;
      this.animationId = requestAnimationFrame(this.update.bind(this));
      this.paused = false;
    }

    pause(ev) {
      if (this.paused) return;
      cancelAnimationFrame(this.animationId);
      this.animationId = null;
      this.isPlaying = false;
      this.paused = true;
      this.options.video.pause();
      if (this.options.onPause) {
        this.options.onPause(this);
      }
    }

    stop(ev) {
      this.pause();
    }

    // 资源清理
    destroy() {
      logMessage("info", "调用了 Player 的 destroy 方法");
      this.pause();
      if (this.pc) {
        this.pc.close();
        this.pc = null;
      }
      if (this.audioOut) {
        this.audioOut.destroy();
      }
      if (this.options.video) {
        this.options.video.srcObject = null; // 释放视频源
      }
    }

    // 状态更新循环,添加节流逻辑
    update = throttle(function () {
      this.animationId = requestAnimationFrame(this.update.bind(this));
      if (this.options.video.readyState < 4) return;
      if (!this.isPlaying) {
        this.isPlaying = true;
        this.options.video.play();
        if (this.options.onPlay) {
          this.options.onPlay(this);
        }
      }
    }, 100); // 每 100ms 执行一次

    /**
     * 设置新的 URL 并重新加载连接
     */
    setUrl(newUrl, oldUrl) {
      if (!newUrl.match(/^webrtc?:\/\//)) {
        throw new Error("JSWebrtc just work with webrtc");
      }
      this.urlParams = JSWebrtc.parseUrl(newUrl);
      try {
        this.startLoading();
      } catch (error) {
        handleError(error, "Player setUrl");
      }
    }
  }

  return Player;
})();

export default JSWebrtc;
  • 构造函数:初始化播放器实例,检查 URL 格式和视频元素是否存在,解析 URL 参数,并调用 startLoading 方法开始加载连接。
  • startLoading 方法:核心连接流程,包括创建 RTCPeerConnection 对象、添加音视频 transceiver、创建并发送 offer、接收并处理 answer 等。
  • triggerErrorEvent 方法:触发自定义错误事件,方便统一处理错误。
  • playpausestop 方法:实现播放、暂停和停止的控制逻辑。
  • destroy 方法:清理资源,关闭 RTCPeerConnection 对象,释放视频源。
  • update 方法:状态更新循环,使用节流函数控制更新频率,确保视频在准备好时自动播放。
  • setUrl 方法:设置新的 URL 并重新加载连接。

如何使用 WebRTC

引入 jswebrtc1.js 文件

在 HTML 文件中引入 jswebrtc1.js 文件:

html 复制代码
<script src="path/to/jswebrtc1.js"></script>

创建视频元素

在 HTML 文件中创建一个带有 data-url 属性的 div 元素,并添加 jswebrtc 类:

html 复制代码
<div class="jswebrtc" data-url="webrtc://example.com/stream"></div>

运行代码

当页面加载完成后,jswebrtc1.js 会自动初始化视频元素,并尝试建立 WebRTC 连接,播放视频流。

总结

通过本文的详细解释,你应该已经了解了 jswebrtc1.js 文件的代码结构和实现原理,以及如何使用 WebRTC 来实现一个简单的视频播放器。WebRTC 是一项强大的技术,它为实时通信提供了便捷的解决方案,希望本文能帮助你更好地掌握和应用 WebRTC。

相关推荐
我自纵横202336 分钟前
第一章:欢迎来到 HTML 星球!
前端·html
发财哥fdy37 分钟前
3.12-2 html
前端·html
ziyu_jia44 分钟前
React 组件测试【React Testing Library】
前端·react.js·前端框架
祈澈菇凉44 分钟前
如何在 React 中实现错误边界?
前端·react.js·前端框架
撸码到无法自拔1 小时前
❤React-组件的新旧生命周期
前端·javascript·react.js·前端框架·ecmascript
betterangela1 小时前
react基础语法视图层&类组件
前端·javascript·vue.js
小段hy1 小时前
在小程序中/uni-app中,当没有登录时,点击结算按钮,3s后自动跳转到登录页面
前端·小程序·uni-app
CSDN专家-微编程1 小时前
UNIAPP圈子社区纯前端万能源码模板 H5小程序APP多端兼容 酷炫UI
前端·小程序·uni-app
冴羽2 小时前
SvelteKit 最新中文文档教程(2)—— 路由
前端·javascript·svelte
2401_853275732 小时前
ajax组件是什么
前端·javascript·ajax