客户端向服务端推流,前端 h5 播放流。
SRS(Simple Realtime Server)是一个简单高效的实时视频服务器,支持RTMP、WebRTC、HLS、HTTP-FLV、SRT等多种实时流媒体协议。Oryx是一个一体化、开箱即用、开源的视频解决方案,可部署在云上或自建机房,以直播和WebRTC等能力赋能你的业务。
开始
文件目录:
├── srs.conf # SRS 5.x:RTMP 接流 + HLS/HTTP-FLV/WebRTC 三协议输出
├── docker-compose.yaml # 一键启动,CANDIDATE 环境变量注入 WebRTC 候选 IP
└── index.html # H5 播放页:WebRTC 优先 → HLS 兜底,断线自动重试
docker-compose.yaml
# ============================================================
# Pico RTMP 推流 -> H5 播放:SRS 流媒体服务器
# 启动:docker compose up -d
# 部署前修改下方 CANDIDATE 为本机 IP(局域网填内网 IP,公网填公网 IP)
# ============================================================
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: 192.168.31.112
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
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;
}
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
