引言
WebRTC(Web Real-Time Communication)是一项令人惊叹的 Web 技术,它能让浏览器间实现实时音视频通信,无需额外插件。本文将基于最终版本的代码,深入剖析脚本逻辑,助力初学者掌握使用 WebRTC 连接视频的核心要点。
整体思路
在 Vue 项目里运用 WebRTC 连接视频,主要包含以下关键步骤:
- 解析视频流 URL。
- 创建
RTCPeerConnection
实例,建立 WebRTC 连接。 - 交换会话描述协议(SDP)信息。
- 处理 ICE 候选,完成连接。
- 监听远程流并播放视频。
脚本逻辑详细解析
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();
}
});
};
详细步骤
-
加载提示 :使用
ElLoading
显示加载提示,提升用户体验。 -
URL 检查 :若
videoStreamUrl
为空,输出错误信息并终止连接。 -
创建
RTCPeerConnection
:iceServers
:配置 STUN 和 TURN 服务器,帮助穿越 NAT 网络。iceTransportPolicy: 'relay'
:优先使用主机候选。
-
监听 ICE 候选收集完成 :借助
onicegatheringstatechange
事件,确保在发送 SDP 时包含所有 ICE 候选。 -
监听远程流 :当接收到远程流时,将其赋值给
<video>
元素的srcObject
。 -
设置视频编码参数 :在
addTransceiver
中设置视频编码为VP8
,并添加 RTCP 反馈机制,提升视频传输质量。 -
带宽自适应 :监听
connectionState
变化,连接成功后调整视频编码的scalabilityMode
以适应带宽。 -
创建并发送 Offer:
createOffer
:创建 Offer SDP。setLocalDescription
:设置本地描述。- 构建请求 URL 和数据,使用
fetch
发送 POST 请求。
-
处理 Answer :接收服务器返回的 Answer SDP,使用
setRemoteDescription
设置远程描述。 -
重试机制:连接失败时最多重试 3 次,每次间隔 2 秒。
-
资源清理 :组件卸载时,关闭
RTCPeerConnection
释放资源。
4. 生命周期钩子
scss
index.vue
onMounted(() => {
connectWebRTCStream();
});
在组件挂载完成后,调用 connectWebRTCStream
函数开始连接 WebRTC 视频流。
总结
`` 通过上述脚本逻辑,我们实现了一个完整的 WebRTC 视频连接功能,涵盖 URL 解析、连接建立、SDP 交换、ICE 候选处理、视频播放、错误处理和资源清理等关键步骤。初学者可以根据这个示例深入理解 WebRTC 的工作原理,并在此基础上进行扩展和优化。
js