手把手教你使用 WebRTC:基于 jswebrtc.js 的详细教程
前言
WebRTC(Web Real-Time Communication)是一种支持浏览器之间进行实时通信的技术,它允许网页在不借助任何插件的情况下实现音视频通话、文件共享等功能。在本文中,我们将深入分析 jswebrtc1.js
文件,详细解释代码的各个部分,并教会你如何使用 WebRTC 来实现一个简单的视频播放器。
代码整体结构
jswebrtc1.js
文件主要实现了一个 WebRTC 视频播放器,它包含了公共函数、主对象 JSWebrtc
、VideoElement
类和 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
函数:根据传入的日志级别(debug
、info
、error
),使用不同的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;
}
});
}
Player
和VideoElement
:作为占位符,后续会被具体的类实例或构造函数填充。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_BUTTON
和UNMUTE_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
方法:触发自定义错误事件,方便统一处理错误。play
、pause
和stop
方法:实现播放、暂停和停止的控制逻辑。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。