📝 背景

在视频监控、直播等场景中,RTSP (Real Time Streaming Protocol) 是一种广泛使用的视频流传输协议。然而,痛点在于:**浏览器原生并不支持 RTSP 协议 **,传统的 <video> 标签无法直接播放 RTSP 流。

为了在 Web 前端实现 RTSP 视频播放,我们需要“曲线救国”:将 RTSP 流转换为浏览器原生支持的 WebRTC 协议。

本文将手把手教你如何使用 MediaMTX 作为流媒体服务器,在 Vue3 前端项目中实现丝滑的 RTSP 视频流播放。

🛠️ 解决方案

我们需要两个核心工具:

  • MediaMTX : 一个轻量级的开源流媒体服务器,核心能力是支持 RTSP 转 WebRTC。
  • **FFmpeg **: 强大的音视频处理瑞士军刀,用于模拟 RTSP 视频流进行开发测试。

技术架构图:

1
RTSP源流  -->  MediaMTX服务器  -->  WebRTC协议  -->  浏览器播放

💻 实操演练

1. 视频流模拟

首先,下载并启动 MediaMTX。

然后,下载 FFmpeg 工具,在FFmpeg的 bin 目录下打开 cmd,复制一个 mp4 文件到同目录下,执行以下命令进行推流:

1
.\ffmpeg.exe -re -stream_loop -1 -i test.mp4 -c:v libx264 -s 1280x720 -preset ultrafast -tune zerolatency -an -rtsp_transport tcp -bsf:v h264_mp4toannexb -f rtsp rtsp://127.0.0.1:8554/mystream

💡 提示: 端口 8554 是 MediaMTX 的默认 RTSP 端口。

命令参数说明:

  • -re: 以原始帧率读取输入
  • -stream_loop -1: 无限循环播放
  • -i test.mp4: 输入文件
  • -c:v libx264: 使用H.264编码
  • -s 1280x720: 设置分辨率
  • -preset ultrafast: 编码速度优先
  • -tune zerolatency: 低延迟调优
  • -an: 禁用音频
  • -rtsp_transport tcp: 使用TCP传输

✅ 执行成功结果:

1
2
3
4
5
6
7
8
9
10
11
12
Output #0, rtsp, to 'rtsp://127.0.0.1:8554/mystream':
Metadata:
major_brand : mp42
minor_version : 0
compatible_brands: mp42isom
encoder : Lavf62.6.103
Stream #0:0(und): Video: h264, yuv420p(tv, progressive), 1280x720 [SAR 423:416 DAR 47:26], q=2-31, 30 fps, 90k tbn (default)
Metadata:
encoder : Lavc62.21.100 libx264
Side data:
CPB properties: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
frame= 6747 fps= 30 q=21.0 size=N/A time=00:03:44.90 bitrate=N/A dup=0 drop=6 speed= 1x elapsed=0:03:44.74

2.封装webRTCPlayer

创建WebRTC播放器类,处理WebRTC连接和媒体流播放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
export class WebRTCPlayer {
private pc: RTCPeerConnection | null = null;
private videoElement: HTMLVideoElement;
private iceConfig: RTCConfiguration;
private restartTimeout: number | null = null;

constructor(videoElement: HTMLVideoElement, iceConfig: RTCConfiguration) {
this.videoElement = videoElement;
this.iceConfig = iceConfig;
}

async start(offerData: RTCSessionDescriptionInit): Promise<RTCSessionDescriptionInit> {
this.stop();

this.pc = new RTCPeerConnection(this.iceConfig);

// 处理远程媒体流
this.pc.ontrack = (event) => {
if (event.streams && event.streams[0]) {
this.videoElement.srcObject = event.streams[0];
}
};

// 设置远程描述
await this.pc.setRemoteDescription(offerData);

// 创建应答
const answer = await this.pc.createAnswer();
await this.pc.setLocalDescription(answer);

return answer;
}

stop(): void {
if (this.restartTimeout !== null) {
clearTimeout(this.restartTimeout);
this.restartTimeout = null;
}

if (this.pc) {
this.pc.close();
this.pc = null;
}

if (this.videoElement.srcObject) {
this.videoElement.srcObject = null;
}
}
}

3.封装MediaMTXLinker

创建MediaMTX连接管理类,负责与MediaMTX服务器通信:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { WebRTCPlayer } from './webRTCPlayer';

interface MediaMTXConfig {
apiBase: string;
rtcBase: string;
iceConfig: RTCConfiguration;
refreshInterval?: number;
}

interface CallbackHandlers {
onStateChange?: (state: string, message?: string) => void;
onError?: (error: Error) => void;
}

export class MediaMTXLinker {
private config: MediaMTXConfig;
private player: WebRTCPlayer;
private callbacks: CallbackHandlers;
private sessionUrl: string | null = null;
private refreshTimer: number | null = null;

constructor(config: MediaMTXConfig, videoElement: HTMLVideoElement, callbacks: CallbackHandlers = {}) {
this.config = config;
this.callbacks = callbacks;
this.player = new WebRTCPlayer(videoElement, config.iceConfig);
}

async play(rtspUrl: string): Promise<void> {
try {
this.stop();
this.emitState('connecting', '正在连接...');

// 1. 获取WebRTC Offer
const offerResponse = await fetch(`${this.config.rtcBase}/${encodeURIComponent(rtspUrl)}/whep`, {
method: 'OPTIONS'
});

if (!offerResponse.ok) {
throw new Error(`获取Offer失败: ${offerResponse.status}`);
}

const offerData = await offerResponse.json();

// 2. 启动WebRTC播放器
const answer = await this.player.start(offerData);

// 3. 发送Answer到MediaMTX
const sessionResponse = await fetch(`${this.config.rtcBase}/${encodeURIComponent(rtspUrl)}/whep`, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp'
},
body: answer.sdp
});

if (!sessionResponse.ok) {
throw new Error(`建立会话失败: ${sessionResponse.status}`);
}

this.sessionUrl = sessionResponse.headers.get('Location');
this.emitState('playing', '播放中');

// 4. 启动会话保活
if (this.config.refreshInterval) {
this.startRefresh();
}

} catch (error) {
this.emitError(error as Error);
throw error;
}
}

stop(): void {
this.stopRefresh();

if (this.sessionUrl) {
fetch(this.sessionUrl, { method: 'DELETE' }).catch(console.error);
this.sessionUrl = null;
}

this.player.stop();
this.emitState('stopped', '已停止');
}

private startRefresh(): void {
if (!this.config.refreshInterval || !this.sessionUrl) return;

this.refreshTimer = window.setInterval(() => {
if (this.sessionUrl) {
fetch(this.sessionUrl, { method: 'PATCH' }).catch(console.error);
}
}, this.config.refreshInterval);
}

private stopRefresh(): void {
if (this.refreshTimer !== null) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
}

private emitState(state: string, message?: string): void {
this.callbacks.onStateChange?.(state, message);
}

private emitError(error: Error): void {
this.callbacks.onError?.(error);
}
}

4.Vue 组件实战

封装vue组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<template>
<div class="video-box">
<video ref="videoElement" autoplay playsinline controls></video>
</div>
</template>

<script setup lang="ts">
import { MediaMTXLinker } from '/@/cool/utils/rtsp/mediaMTXLinker';
import { ref, onMounted, onBeforeUnmount } from 'vue';

const props = defineProps<{
rtspUrl: string;
}>();

// MediaMTX配置
const CONFIG = {
apiBase: '/media/api', // MediaMTX API服务器
rtcBase: '/media/webRTC', // MediaMTX WebRTC服务器
iceConfig: {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
},
refreshInterval: 15000 // 会话保活间隔(15秒)
};

const videoElement = ref<HTMLVideoElement | null>(null);
let mediaLinker: MediaMTXLinker | null = null;

// 开始播放
const startPlayback = async () => {
if (mediaLinker && videoElement.value) {
try {
await mediaLinker.play(props.rtspUrl);
console.log('播放已开始');
} catch (error) {
console.error('播放失败:', error);
}
}
};

// 停止播放
const stopPlayback = () => {
if (mediaLinker) {
mediaLinker.stop();
console.log('播放已停止');
}
};

onMounted(() => {
if (videoElement.value) {
// 初始化MediaMTX连接器
mediaLinker = new MediaMTXLinker(CONFIG, videoElement.value, {
onStateChange: (state, message) => {
console.log('状态变更:', state, message);
},
onError: error => {
console.error('发生错误:', error);
}
});

// 延迟启动播放,确保DOM完全加载
setTimeout(() => {
startPlayback();
}, 100);
}
});

onBeforeUnmount(() => {
if (mediaLinker) {
mediaLinker.stop();
mediaLinker = null;
}
});
</script>

<style lang="scss" scoped>
.video-box {
width: 100%;
height: 100%;
}

video {
width: 100%;
min-height: 600px;
background: black;
}
</style>

使用示例:

1
2
3
<template>
<rtsp-preview rtsp-url="rtsp://127.0.0.1:8554/mystream" />
</template>

结果展示:

✨ 总结

通过以上步骤,我们成功实现了:

  1. FFmpeg 推流:模拟真实的 RTSP 监控环境 📹
  2. WebRTCPlayer:底层处理媒体流的“播放引擎” 🚂
  3. MediaMTXLinker:负责信令交换和心跳保活的“调度员” 👮‍♂️
  4. Vue 组件:即插即用的 UI 封装 🧩

🌟 关键特性:

  • 低延迟: WebRTC 带来的毫秒级体验
  • 自动保活: 内置心跳机制,防止连接断开
  • 高复用: 组件化设计,随处调用

⚠️ 注意事项:

  • 需配置 Nginx 反向代理 解决跨域问题。
  • 生产环境建议部署 TURN 服务器 以应对复杂网络环境。
  • 注意处理网络波动导致的异常和自动重连逻辑。