WebRtcStreamer 是一个基于 WebRTC 协议的轻量级开源工具,可以在浏览器中直接播放 RTSP 视频流。它利用 WebRTC 的强大功能,提供低延迟的视频流播放体验,非常适合实时监控和其他视频流应用场景。
本文将介绍如何在Vue.js
项目中使用 WebRtcStreamer
实现实时视频流播放,并分享相关的代码示例。
注意
:只支持H264
格式
流媒体方式文章
使用 Vue 和 flv.js 实现流媒体视频播放:完整教程
VUE项目中优雅使用EasyPlayer实时播放摄像头多种格式视频使用版本信息为5.xxxx
实现步骤
-
安装和配置 WebRtcStreamer 服务端
要使用 WebRtcStreamer,需要先在服务器上部署其服务端。以下是基本的安装步骤: -
从 WebRtcStreamer 官方仓库 下载代码。
启动命令
或者双击exe程序
服务启动后,默认会监听8000
端口,访问 http://<server_ip>:8000 可查看状态。
更改默认端口命令:webrtc-streamer.exe -o -H 0.0.0.0:9527
2.集成到vue中
webRtcStreamer.js
不需要在html文件中引入webRtcStreamer相关代码
/**
* @constructor
* @param {string} videoElement - dom ID
* @param {string} srvurl - WebRTC 流媒体服务器的 URL(默认为当前页面地址)
*/
class WebRtcStreamer {
constructor(videoElement, srvurl) {
if (typeof videoElement === 'string') {
this.videoElement = document.getElementById(videoElement);
} else {
this.videoElement = videoElement;
}
this.srvurl =
srvurl || `${location.protocol}//${window.location.hostname}:${window.location.port}`;
this.pc = null; // PeerConnection 实例
// 媒体约束条件
this.mediaConstraints = {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
this.iceServers = null; // ICE 服务器配置
this.earlyCandidates = []; // 提前收集的候选者
}
/**
* HTTP 错误处理器
* @param {Response} response - HTTP 响应
* @throws {Error} 当响应不成功时抛出错误
*/
_handleHttpErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
/**
* 连接 WebRTC 视频流到指定的 videoElement
* @param {string} videourl - 视频流 URL
* @param {string} audiourl - 音频流 URL
* @param {string} options - WebRTC 通话的选项
* @param {MediaStream} localstream - 本地流
* @param {string} prefmime - 优先的 MIME 类型
*/
connect(videourl, audiourl, options, localstream, prefmime) {
this.disconnect();
if (!this.iceServers) {
console.log('获取 ICE 服务器配置...');
fetch(`${this.srvurl}/api/getIceServers`)
.then(this._handleHttpErrors)
.then((response) => response.json())
.then((response) =>
this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream, prefmime),
)
.catch((error) => this.onError(`获取 ICE 服务器错误: ${error}`));
} else {
this.onReceiveGetIceServers(
this.iceServers,
videourl,
audiourl,
options,
localstream,
prefmime,
);
}
}
/**
* 断开 WebRTC 视频流,并清空 videoElement 的视频源
*/
disconnect() {
if (this.videoElement?.srcObject) {
this.videoElement.srcObject.getTracks().forEach((track) => {
track.stop();
this.videoElement.srcObject.removeTrack(track);
});
}
if (this.pc) {
fetch(`${this.srvurl}/api/hangup?peerid=${this.pc.peerid}`)
.then(this._handleHttpErrors)
.catch((error) => this.onError(`hangup ${error}`));
try {
this.pc.close();
} catch (e) {
console.log(`Failure close peer connection: ${e}`);
}
this.pc = null;
}
}
/**
* 获取 ICE 服务器配置的回调
* @param {Object} iceServers - ICE 服务器配置
* @param {string} videourl - 视频流 URL
* @param {string} audiourl - 音频流 URL
* @param {string} options - WebRTC 通话的选项
* @param {MediaStream} stream - 本地流
* @param {string} prefmime - 优先的 MIME 类型
*/
onReceiveGetIceServers(iceServers, videourl, audiourl, options, stream, prefmime) {
this.iceServers = iceServers;
this.pcConfig = iceServers || { iceServers: [] };
try {
this.createPeerConnection();
let callurl = `${this.srvurl}/api/call?peerid=${this.pc.peerid}&url=${encodeURIComponent(
videourl,
)}`;
if (audiourl) {
callurl += `&audiourl=${encodeURIComponent(audiourl)}`;
}
if (options) {
callurl += `&options=${encodeURIComponent(options)}`;
}
if (stream) {
this.pc.addStream(stream);
}
this.earlyCandidates.length = 0;
this.pc
.createOffer(this.mediaConstraints)
.then((sessionDescription) => {
// console.log(`创建 Offer: ${JSON.stringify(sessionDescription)}`);
if (prefmime !== undefined) {
const [prefkind] = prefmime.split('/');
const codecs = RTCRtpReceiver.getCapabilities(prefkind).codecs;
const preferredCodecs = codecs.filter((codec) => codec.mimeType === prefmime);
this.pc
.getTransceivers()
.filter((transceiver) => transceiver.receiver.track.kind === prefkind)
.forEach((tcvr) => {
if (tcvr.setCodecPreferences) {
tcvr.setCodecPreferences(preferredCodecs);
}
});
}
this.pc
.setLocalDescription(sessionDescription)
.then(() => {
fetch(callurl, {
method: 'POST',
body: JSON.stringify(sessionDescription),
})
.then(this._handleHttpErrors)
.then((response) => response.json())
.then((response) => this.onReceiveCall(response))
.catch((error) => this.onError(`调用错误: ${error}`));
})
.catch((error) => console.log(`setLocalDescription error: ${JSON.stringify(error)}`));
})
.catch((error) => console.log(`创建 Offer 失败: ${JSON.stringify(error)}`));
} catch (e) {
this.disconnect();
alert(`连接错误: ${e}`);
}
}
/**
* 创建 PeerConnection 实例
*/
createPeerConnection() {
console.log('创建 PeerConnection...');
this.pc = new RTCPeerConnection(this.pcConfig);
this.pc.peerid = Math.random(); // 生成唯一的 peerid
// 监听 ICE 候选者事件
this.pc.onicecandidate = (evt) => this.onIceCandidate(evt);
this.pc.onaddstream = (evt) => this.onAddStream(evt);
this.pc.oniceconnectionstatechange = () => {
if (this.videoElement) {
if (this.pc.iceConnectionState === 'connected') {
this.videoElement.style.opacity = '1.0';
} else if (this.pc.iceConnectionState === 'disconnected') {
this.videoElement.style.opacity = '0.25';
} else if (['failed', 'closed'].includes(this.pc.iceConnectionState)) {
this.videoElement.style.opacity = '0.5';
} else if (this.pc.iceConnectionState === 'new') {
this.getIceCandidate();
}
}
};
return this.pc;
}
onAddStream(event) {
console.log(`Remote track added: ${JSON.stringify(event)}`);
this.videoElement.srcObject = event.stream;
const promise = this.videoElement.play();
if (promise !== undefined) {
promise.catch((error) => {
console.warn(`error: ${error}`);
this.videoElement.setAttribute('controls', true);
});
}
}
onIceCandidate(event) {
if (event.candidate) {
if (this.pc.currentRemoteDescription) {
this.addIceCandidate(this.pc.peerid, event.candidate);
} else {
this.earlyCandidates.push(event.candidate);
}
} else {
console.log('End of candidates.');
}
}
/**
* 添加 ICE 候选者到 PeerConnection
* @param {RTCIceCandidate} candidate - ICE 候选者
*/
addIceCandidate(peerid, candidate) {
fetch(`${this.srvurl}/api/addIceCandidate?peerid=${peerid}`, {
method: 'POST',
body: JSON.stringify(candidate),
})
.then(this._handleHttpErrors)
.catch((error) => this.onError(`addIceCandidate ${error}`));
}
/**
* 处理 WebRTC 通话的响应
* @param {Object} message - 来自服务器的响应消息
*/
onReceiveCall(dataJson) {
const descr = new RTCSessionDescription(dataJson);
this.pc
.setRemoteDescription(descr)
.then(() => {
while (this.earlyCandidates.length) {
const candidate = this.earlyCandidates.shift();
this.addIceCandidate(this.pc.peerid, candidate);
}
this.getIceCandidate();
})
.catch((error) => console.log(`设置描述文件失败: ${JSON.stringify(error)}`));
}
getIceCandidate() {
fetch(`${this.srvurl}/api/getIceCandidate?peerid=${this.pc.peerid}`)
.then(this._handleHttpErrors)
.then((response) => response.json())
.then((response) => this.onReceiveCandidate(response))
.catch((error) => this.onError(`getIceCandidate ${error}`));
}
onReceiveCandidate(dataJson) {
if (dataJson) {
dataJson.forEach((candidateData) => {
const candidate = new RTCIceCandidate(candidateData);
this.pc
.addIceCandidate(candidate)
.catch((error) => console.log(`addIceCandidate error: ${JSON.stringify(error)}`));
});
}
}
/**
* 错误处理器
* @param {string} message - 错误信息
*/
onError(status) {
console.error(`WebRTC 错误: ${status}`);
}
}
export default WebRtcStreamer;
组件中使用
<template>
<div>
<video id="video" controls muted autoplay></video>
<button @click="startStream">开始播放</button>
<button @click="stopStream">停止播放</button>
</div>
</template>
<script>
import WebRtcStreamer from "@/utils/webRtcStreamer";
export default {
name: "VideoStreamer",
data() {
return {
webRtcServer: null,
};
},
methods: {
startStream() {
const srvurl = "127.0.0.1:9527"
this.webRtcServer = new WebRtcStreamer(
"video",
`${location.protocol}//${srvurl}`
);
const videoPath = "stream_name"; // 替换为你的流地址
this.webRtcServer.connect(videoPath);
},
stopStream() {
if (this.webRtcServer) {
this.webRtcServer.disconnect(); // 销毁
}
},
},
};
</script>
<style>
video {
width: 100%;
height: 100%;
object-fit: fill;
}
</style>