Write the Code. Change the World.

8月 04

现在是客户端不推流到服务端,反而是服务端拉取客户端的流,再分发出去。

https://blog.vini123.com/1414

上边这个链接客户端推流模式,这个简单,实用。但有时候,却说推不了流,只能被动拉流。 这个会增加复杂度,使用范围变成了局域网。

开始

文件目录:

├── .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

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注