现在是客户端不推流到服务端,反而是服务端拉取客户端的流,再分发出去。
上边这个链接客户端推流模式,这个简单,实用。但有时候,却说推不了流,只能被动拉流。 这个会增加复杂度,使用范围变成了局域网。
开始
文件目录:
├── .env # 配置文件
├── srs.conf # SRS 5.x:RTMP 接流 + HLS/HTTP-FLV/WebRTC 三协议输出
├── docker-compose.yaml # 一键启动,CANDIDATE 环境变量注入 WebRTC 候选 IP
└── index.html # H5 播放页:WebRTC 优先 → HLS 兜底,断线自动重试
.env
# ============================================================
# 部署配置:改这里即可,无需动 docker-compose.yml
# 改完执行 docker compose up -d 生效
# ============================================================
# Pico 眼镜 RTMP 服务地址(puller 边车从这里拉流)
PICO_RTMP_URL=rtmp://192.168.31.54:3343/live/cast
# SRS 服务器 IP:WebRTC 候选地址,需与 index.html 的 SRS_HOST 一致
CANDIDATE=192.168.31.112
docker-compose.yaml
# ============================================================
# Pico RTMP 推流 -> H5 播放:SRS 流媒体服务器
# 启动:docker compose up -d
# 部署参数(Pico 地址、本机 IP)已抽到 .env,改 .env 后执行 docker compose up -d
# ============================================================
services:
srs:
image: ossrs/srs:6
container_name: srs-pico
restart: always
# 镜像默认启动命令加载的是 conf/docker.conf,必须显式指定为挂载的 srs.conf 才能生效
command: ["./objs/srs", "-c", "conf/srs.conf"]
ports:
- "1935:1935" # RTMP 推流(Pico 推到这里)
- "8080:8080" # HTTP:HLS / HTTP-FLV / H5 播放页
- "1985:1985" # HTTP API:WebRTC 信令 / SRS 控制台
- "8000:8000/udp" # WebRTC 媒体传输
environment:
# WebRTC 候选地址:必须与 H5 页面中的 SRS_HOST 指向同一台机器
CANDIDATE: ${CANDIDATE} # 取自 .env
volumes:
# SRS 主配置
- ./srs.conf:/usr/local/srs/conf/srs.conf
# H5 播放页挂进 SRS 内置 HTTP 服务,与 HLS 同源免跨域
# 访问: http://{IP}:8080/index.html
- ./index.html:/usr/local/srs/objs/nginx/html/index.html
# ---------- 拉流边车:从 Pico 拉 RTMP 转推到 SRS ----------
# 原理:独立 ffmpeg 进程循环执行 rtmp(Pico) -> rtmp(本地SRS),断流自动重连
# Pico 地址在 .env 的 PICO_RTMP_URL 里改
puller:
image: ossrs/srs:6
container_name: srs-puller
restart: always
network_mode: "service:srs" # 共享 srs 网络栈:可同时访问 Pico(LAN) 与 SRS(127.0.0.1)
command:
["sh", "-c",
"while true; do /usr/local/srs/objs/ffmpeg/bin/ffmpeg -hide_banner -loglevel warning -rw_timeout 5000000 -i '${PICO_RTMP_URL}' -c copy -f flv rtmp://127.0.0.1:1935/live/pico; echo '[puller] stream interrupted, retry in 3s'; sleep 3; done"]
src.conf
# ============================================================
# SRS 5.x 配置:Pico 眼镜 RTMP 推流 -> HLS / HTTP-FLV / WebRTC 三协议分发
# 配合 docker-compose.yml 使用,挂载到 /usr/local/srs/conf/srs.conf
# ============================================================
listen 1935; # RTMP 推流/拉流端口
max_connections 1000;
daemon off; # Docker 内必须前台运行
srs_log_tank console; # 日志输出到控制台,便于 docker logs 查看
# ---------- HTTP API:WebRTC 信令(/rtc/v1/play/) 与控制台 ----------
http_api {
enabled on;
listen 1985;
crossdomain on; # H5 页跨域调用信令接口必须开启
}
# ---------- HTTP 服务:HLS 切片 / HTTP-FLV / 播放页静态文件 ----------
http_server {
enabled on;
listen 8080;
dir ./objs/nginx/html;
crossdomain on; # H5 页与流不同源时必须开启
}
# ---------- WebRTC 服务(UDP) ----------
rtc_server {
enabled on;
listen 8000; # UDP
# 观看端能访问到的本机 IP,通过 docker-compose 的 CANDIDATE 环境变量注入
candidate $CANDIDATE;
}
vhost __defaultVhost__ {
# ===== 输出 1:HLS(兼容性最好,iOS/微信兜底) =====
hls {
enabled on;
hls_fragment 2; # 切片时长(秒),越小延迟越低
hls_window 10; # m3u8 列表窗口时长(秒)
hls_path ./objs/nginx/html;
hls_m3u8_file [app]/[stream].m3u8;
hls_ts_file [app]/[stream]-[seq].ts;
hls_cleanup on; # 流断开后自动清理过期切片
# 播放地址: http://{IP}:8080/{app}/{stream}.m3u8
}
# ===== 输出 2:HTTP-FLV(国内安卓低延迟备用,iOS 不支持) =====
http_remux {
enabled on;
mount [vhost]/[app]/[stream].flv;
# 播放地址: http://{IP}:8080/{app}/{stream}.flv
}
# ===== 输出 3:WebRTC(延迟 < 1s,实时互动首选) =====
rtc {
enabled on;
rtmp_to_rtc on; # 关键:RTMP 推流直接转 RTC 播放,无需转码
rtc_to_rtmp off;
# 播放地址: webrtc://{IP}/{app}/{stream}(信令走 1985 端口 HTTP API)
}
# ---------- 低延迟调优 ----------
play {
gop_cache off; # 关闭 GOP 缓存:降低首屏延迟,新观众等下一个关键帧
queue_length 10;
mw_latency 100;
}
tcp_nodelay on;
min_latency on;
}
# ============================================================
# 可选:主动拉流模式(Pico 端 App 内嵌 RTSP Server 时启用)
# 原理:SRS 通过内置 ffmpeg 从 Pico 拉取 RTSP 流,原封不动转推到本地
# RTMP 入口,后续的 HLS/HTTP-FLV/WebRTC 输出与 H5 播放页完全不用改。
# 说明:SRS 6 已移除 ingest 配置指令(启动会报 "illegal directive ingest"),
# 因此改用 docker-compose.yml 中的 puller 边车服务(独立 ffmpeg 进程)
# 持续从 Pico 拉流并转推本地,效果与 ingest 等价且与 SRS 版本无关。
# Pico 的拉流地址维护在 docker-compose.yml 的 puller.command 中。
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Pico 眼镜实时画面</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; background: #000; overflow: hidden;
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; }
#player { width: 100%; height: 100%; object-fit: contain; }
#status {
position: fixed; top: 12px; left: 12px; z-index: 10;
padding: 6px 12px; border-radius: 4px;
background: rgba(0, 0, 0, .6); color: #0f0; font-size: 13px;
}
#toolbar {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
z-index: 10; display: flex; gap: 12px;
}
#toolbar button {
padding: 8px 20px; border: none; border-radius: 20px;
background: rgba(255, 255, 255, .85); color: #000;
font-size: 14px; cursor: pointer;
}
</style>
</head>
<body>
<!-- muted + playsinline:满足浏览器自动播放策略;iOS 上 playsinline 防止强制全屏 -->
<video id="player" autoplay muted playsinline controls></video>
<div id="status">初始化…</div>
<div id="toolbar">
<button id="btnSound">开启声音</button>
<button id="btnRetry">重新连接</button>
</div>
<!-- 内网无公网环境:下载 hls.js 到本目录,改为 <script src="./hls.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
/* ==================== 配置区(部署时只需修改这里) ==================== */
const SRS_HOST = '192.168.31.112'; // SRS 服务器 IP/域名(与 docker-compose 的 CANDIDATE 一致)
const HTTP_PORT = 8080; // SRS HTTP 端口:HLS / HTTP-FLV
const API_PORT = 1985; // SRS HTTP API 端口:WebRTC 信令
const APP = 'live'; // 应用名
const STREAM = 'pico'; // 流名,对应 Pico 推流地址 rtmp://{IP}/live/pico
const RETRY_INTERVAL = 3000; // 断线/失败重试间隔(毫秒)
/* ==================== 以下一般无需修改 ==================== */
// 派生播放地址
const RTC_STREAM_URL = `webrtc://${SRS_HOST}/${APP}/${STREAM}`;
const RTC_API_URL = `http://${SRS_HOST}:${API_PORT}/rtc/v1/play/`;
const HLS_URL = `http://${SRS_HOST}:${HTTP_PORT}/${APP}/${STREAM}.m3u8`;
// HTTP-FLV 备用地址(本页未使用,安卓低延迟场景可用 flv.js 接入):
// const FLV_URL = `http://${SRS_HOST}:${HTTP_PORT}/${APP}/${STREAM}.flv`;
const video = document.getElementById('player');
const statusEl = document.getElementById('status');
const btnSound = document.getElementById('btnSound');
const btnRetry = document.getElementById('btnRetry');
let pc = null; // RTCPeerConnection 实例
let hls = null; // hls.js 实例
let retryTimer = null; // 重试定时器
function setStatus(text) {
statusEl.textContent = text;
console.log('[player]', text);
}
/* ---------- 入口:WebRTC 优先,失败降级 HLS,两者都失败则定时重试 ---------- */
async function start() {
clearTimeout(retryTimer);
cleanup();
try {
await playWebRTC();
} catch (e) {
console.warn('WebRTC 不可用,降级 HLS:', e);
try {
playHLS();
} catch (e2) {
console.warn('HLS 播放失败:', e2);
setStatus(e2.message + ',等待重试…');
scheduleRetry();
}
}
}
/* ---------- 方案一:WebRTC(延迟 < 1s) ---------- */
async function playWebRTC() {
setStatus('WebRTC 连接中…');
pc = new RTCPeerConnection();
// 只收不发:拉流场景
pc.addTransceiver('audio', { direction: 'recvonly' });
pc.addTransceiver('video', { direction: 'recvonly' });
pc.ontrack = (e) => {
video.srcObject = e.streams[0];
video.play().catch(() => {});
setStatus('WebRTC 播放中(低延迟模式)');
};
// 运行中断线 -> 走整体重试
pc.onconnectionstatechange = () => {
if (['failed', 'disconnected', 'closed'].includes(pc.connectionState)) {
setStatus('WebRTC 连接断开,准备重连…');
scheduleRetry();
}
};
// 与 SRS 交换 SDP(WHIP 风格 API)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const res = await fetch(RTC_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ api: RTC_API_URL, streamurl: RTC_STREAM_URL, sdp: offer.sdp })
});
const data = await res.json();
if (data.code !== 0) {
// 流未推上来时 SRS 也会返回错误码,抛出让上层降级/重试
throw new Error('RTC 信令失败,code=' + data.code);
}
await pc.setRemoteDescription({ type: 'answer', sdp: data.sdp });
}
/* ---------- 方案二:HLS 兜底(iOS Safari / 微信全兼容) ---------- */
function playHLS() {
setStatus('HLS 连接中…');
if (window.Hls && Hls.isSupported()) {
// Chrome / Edge / Firefox / 安卓微信
hls = new Hls({
liveSyncDurationCount: 2, // 贴近直播边缘,降低延迟
liveMaxLatencyDurationCount: 5,
maxBufferLength: 10
});
hls.loadSource(HLS_URL);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play().catch(() => {});
setStatus('HLS 播放中(兼容模式)');
});
hls.on(Hls.Events.ERROR, (_, data) => {
if (!data.fatal) return; // 非致命错误(如短暂卡顿)由 hls.js 自行恢复
console.warn('HLS 致命错误:', data.type, data.details);
setStatus('HLS 播放失败,等待重试…');
scheduleRetry(); // 重试会重新走 start(),优先再试 WebRTC
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// iOS Safari:原生 HLS
video.src = HLS_URL;
video.play().catch(() => {});
setStatus('HLS 播放中(原生模式)');
// 流未就绪(m3u8 404)或中断时触发重试
video.onerror = () => { setStatus('播放失败,等待重试…'); scheduleRetry(); };
video.onended = () => { setStatus('流已结束,等待重试…'); scheduleRetry(); };
} else {
throw new Error('当前浏览器不支持播放,请使用最新版 Chrome/Safari');
}
}
/* ---------- 工具函数 ---------- */
function scheduleRetry() {
clearTimeout(retryTimer);
retryTimer = setTimeout(start, RETRY_INTERVAL);
}
function cleanup() {
if (pc) { pc.close(); pc = null; }
if (hls) { hls.destroy(); hls = null; }
video.onerror = null;
video.onended = null;
video.srcObject = null;
video.removeAttribute('src');
video.load();
}
/* ---------- 页面交互 ---------- */
// 浏览器要求静音自动播放,声音需用户手动开启
btnSound.onclick = () => {
video.muted = !video.muted;
btnSound.textContent = video.muted ? '开启声音' : '关闭声音';
if (!video.muted) video.play().catch(() => {});
};
btnRetry.onclick = start;
start();
</script>
</body>
</html>
启动
docker compose up -d
