玩转视频推流、拉流、摄像头拉流...

一、视频推流与拉流技术指南

1. 核心概念:推流与拉流

2. 实现流程与技术栈

a. 推流端实现

编码:

​ 封装:​​

​ 传输 (推流协议):​​

b. 流媒体服务器端

​ 接收:​​

监听特定端口(如 RTMP 1935, SRT 的 UDP 端口),接收来自推流客户端的连接和数据。

处理:​​

分发:​​

将处理后的流通过不同的协议分发给拉流客户端。服务器需要高效处理大量并发连接。

集群与 CDN:​​

对于大规模应用,需要多台服务器组成集群,并利用 CDN 进行边缘分发,降低源服务器压力和用户访问延迟。

c. 拉流端实现

请求:​​

播放器向流媒体服务器请求播放指定的流。需要知道流的访问地址(URL)。

传输 (拉流协议):

解码:​​

播放器下载到数据后,使用解码器(如浏览器内置的 MediaSource Extensions, 移动端 MediaCodec/VideoToolbox, 桌面端 FFmpeg)将压缩的音视频数据还原成原始数据。

渲染:​​

将解码后的视频帧绘制到屏幕,音频数据输出到扬声器。

二、开箱即用的网关服务

推荐网关解决方案: ​MediaMTX (原 rtsp-simple-server)​​

MediaMTX 是一个开箱即用、零依赖的实时媒体服务器和媒体代理,支持发布、读取、代理、录制和回放音视频流。它被设计为一个“媒体路由器”,可将媒体流从一端路由到另一端

1. 协议支持

直播流可以通过以下协议推送到服务器:

协议 变体 视频编码 音频编码
SRT 客户端   H265, H264, MPEG-4 视频 (H263, Xvid), MPEG-1/2 视频 Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3
SRT 摄像头和服务器   H265, H264, MPEG-4 视频 (H263, Xvid), MPEG-1/2 视频 Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3
WebRTC 客户端 WHIP AV1, VP9, VP8, H265, H264 Opus, G722, G711 (PCMA, PCMU)
WebRTC 服务器 WHEP AV1, VP9, VP8, H265, H264 Opus, G722, G711 (PCMA, PCMU)
RTSP 客户端 UDP, TCP, RTSPS AV1, VP9, VP8, H265, H264, MPEG-4 视频 (H263, Xvid), MPEG-1/2 视频, M-JPEG 及任何 RTP 兼容编码 Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM 及任何 RTP 兼容编码
RTSP 摄像头和服务器 UDP, UDP-组播, TCP, RTSPS AV1, VP9, VP8, H265, H264, MPEG-4 视频 (H263, Xvid), MPEG-1/2 视频, M-JPEG 及任何 RTP 兼容编码 Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM 及任何 RTP 兼容编码
RTMP 客户端 RTMP, RTMPS, 增强型 RTMP AV1, VP9, H265, H264 Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3, G711 (PCMA, PCMU), LPCM
RTMP 摄像头和服务器 RTMP, RTMPS, 增强型 RTMP AV1, VP9, H265, H264 Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3, G711 (PCMA, PCMU), LPCM
HLS 摄像头和服务器 低延迟 HLS, 基于 MP4 的 HLS, 传统 HLS AV1, VP9, H265, H264 Opus, MPEG-4 音频 (AAC)
UDP/MPEG-TS 单播, 广播, 组播 H265, H264, MPEG-4 视频 (H263, Xvid), MPEG-1/2 视频 Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3
树莓派摄像头   H264  

可以通过以下协议从服务器读取直播流:

协议 变体 视频编码 音频编码
SRT   H265, H264, MPEG-4 视频 (H263, Xvid), MPEG-1/2 视频 Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3
WebRTC WHEP AV1, VP9, VP8, H265, H264 Opus, G722, G711 (PCMA, PCMU)
RTSP UDP, UDP-组播, TCP, RTSPS AV1, VP9, VP8, H265, H264, MPEG-4 视频 (H263, Xvid), MPEG-1/2 视频, M-JPEG 及任何 RTP 兼容编码 Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM 及任何 RTP 兼容编码
RTMP RTMP, RTMPS, 增强型 RTMP H264 MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3)
HLS 低延迟 HLS, 基于 MP4 的 HLS, 传统 HLS AV1, VP9, H265, H264 Opus, MPEG-4 音频 (AAC)

直播流可以被录制和回放:

格式 视频编码 音频编码
fMP4 AV1, VP9, H265, H264, MPEG-4 视频 (H263, Xvid), MPEG-1/2 视频, M-JPEG Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3, G711 (PCMA, PCMU), LPCM
MPEG-TS H265, H264, MPEG-4 视频 (H263, Xvid), MPEG-1/2 视频 Opus, MPEG-4 音频 (AAC), MPEG-1/2 音频 (MP3), AC-3

2. 部署步骤:

  1. ​Docker 部署 ​ (推荐)
docker run -d \
  --name mediamtx \
  -e MTX_WEBRTCADDITIONALHOSTS=192.168.50.55 \
  -v "/home/1panel/apps/mediamtx/mediamtx/config/mediamtx.yml:/mediamtx.yml" \
  -p 8888:8888   # WebRTC/WHEP端口 \
  -p 9997:9997   # API管理接口 \
  -p 8554:8554   # RTSP端口 \
  -p 8889:8889   # webRTC HTTP监听端口 \
  -p 8189:8189/udp # webRTC 数据传输UDP端口 \
  bluenviron/mediamtx:latest
  1. 二进制文件安装 ​
./mediamtx  # Linux/macOS
mediamtx.exe # Windows
  1. 配置文件 (mediamtx.yml)
api: yes # 启用API
allowOrigin: "*"
webrtc: yes # 启用WebRTC

3. 推流配置

a. 手动在mediamtx.yml文件中配置

手动在mediamtx.yml中配置输入流,支持不同协议

大多数 IP 摄像头通过其内部嵌入的 RTSP 服务器公开其视频流。 可以使用 MediaMTX 连接到一个或多个现有 RTSP 服务器并读取其视频流:

paths:
  proxied: # 自定义流名称
    # 源流的 URL,格式为 rtsp://user:pass@host:port/path
    source: rtsp://original-url

若源流是 H.265,需通过 FFmpeg 转码为 H.264(WebRTC 要求 H.264 Baseline Profile):

paths:
  my_stream:
    source: ffmpeg://rtsp://源地址 -c:v libx264 -preset ultrafast -tune zerolatency -f rtsp

若要从现有服务器、摄像头或监听模式的客户端(即在 URL 后附加 mode=listener)向服务器摄取 SRT 流,请将相应的 URL 添加到路径的 source 参数中:

paths:
  proxied: # 自定义流名称
    # 源流的 URL,格式为 srt://host:port?streamid=streamid&other_parameters
    source: srt://original-url

要从现有服务器摄取 WebRTC 流,请将相应的 WHEP URL 添加到路径的 source 参数中:

paths:
  proxied: # 自定义流名称
    # 源流的 URL,格式为 whep://host:port/path (HTTP) 或 wheps:// (HTTPS)
    source: wheps://host:port/path

可以使用 MediaMTX 连接到一个或多个现有 RTMP 服务器并读取其视频流:

paths:
  proxied: # 自定义流名称
    # 源流的 URL,格式为 rtmp://user:pass@host:port/path
    source: rtmp://original-url

HLS 是一种流媒体协议,通过将流分割成片段并通过 HTTP 协议提供这些片段和播放列表来工作。 可以使用 MediaMTX 连接到一个或多个现有 HLS 服务器并读取其视频流:

paths:
  proxied: # 自定义流名称
    # 流的播放列表的 URL,格式为 http://user:pass@host:port/path
    source: http://original-url/stream/index.m3u8

服务器支持任意数量的源流(数量仅受可用硬件资源的限制),只需向路径部分添加其他条目即可:

paths:
  proxied1: # 自定义流名称
    source: rtsp://url1

  proxied2: # 自定义流名称
    source: rtsp://url1

b. 通过 API 接口配置

详情可参考官方API文档

获取全部流路径

curl http://localhost:9997/v3/config/paths/list

# 返回示例
{
  "pageCount": 0,
  "itemCount": 0,
  "items": [
    {
      "name": "string",
      "source": "string",
      ...
    }
  ]
}

动态添加流路径 ​

# 添加名为"proxied"的RTSP输入流
# ​参数说明​:
#   - source:输入流地址
#   - webrtc:是否启用WebRTC输出
curl -X POST http://localhost:9997/v3/config/paths/add/proxied \
  -H "Content-Type: application/json" \
  -d '{
      "source": "rtsp://admin:zhixing123@192.168.50.64:554/Streaming/Channels/101",
      "webrtc": "yes"
  }'

删除流路径​

# 删除流路径"proxied"
curl -X DELETE http://localhost:9997/v3/config/paths/delete/proxied

更新流路径

# 更新流路径"proxied"
curl -X POST http://localhost:9997/v3/config/paths/patch/proxied

热重载配置​

# 应用配置文件变更(mediamtx.yml修改后生效)
curl -X POST http://localhost:9997/v3/config/reload
  -H "Content-Type: application/json" \
  -d '{
      "source": "rtsp://admin:zhixing123@192.168.50.64:554/Streaming/Channels/101",
      "webrtc": "yes"
  }'

获取所有活跃发布者​

# 查询当前所有推流客户端
curl http://localhost:9997/v3/publishers/list

# ​返回示例​
[{
  "path": "proxied",
  "clientId": "123abc",
  "protocol": "RTSP"
}]

强制断开指定客户端​

# 根据ID断开推流客户端
curl -X POST http://localhost:9997/v3/publishers/kick/123abc

全局状态概览​

# 获取CPU/内存使用率、活跃流数量
curl http://localhost:9997/v3/stats

指定流路径详情​

# 查询流路径"proxied"的订阅者信息
curl http://localhost:9997/v3/paths/get/proxied

# 返回示例
{
  "subscribers": [{
    "protocol": "WebRTC",
    "clientIP": "192.168.1.100"
  }]
}

手动触发录制​

# 开始录制流"proxied"到磁盘
curl -X POST http://localhost:9997/v3/record/start/proxied

停止录制​

curl -X POST http://localhost:9997/v3/record/stop/proxied

4.拉流配置

a. FFmpeg

FFmpeg 可以通过多种方式从服务器读取流(RTSP、RTMP、HLS、WebRTC 与 WHEP、SRT)。 推荐的方式是通过 RTSP 读取:

ffmpeg -i rtsp://localhost:8554/mystream -c copy output.mp4

RTSP 协议支持多种底层传输协议,每种协议具有不同的特性,可以使用 rtsp_transport 标志设置传输协议:

ffmpeg -rtsp_transport tcp -i rtsp://localhost:8554/mystream -c copy output.mp4

b. SRT

SRT 是一种允许发布和读取实时数据流的协议,提供加密、完整性和重传机制。它通常用于传输编码为 MPEG-TS 的媒体流。 要使用 SRT 协议从服务器读取流,请使用此 URL:

srt://localhost:8890?streamid=read:mystream

mystream 替换为路径名称。

如果启用了凭据,可以通过 userpass 查询参数将凭据传递给服务器:

srt://localhost:8890?streamid=read:mystream:user:pass

可以使用 SRT 读取的已知客户端包括 FFmpeg、GStreamer 和 VLC。

c. WebRTC

WebRTC 是一种 API,它利用一组协议和方法将两个客户端连接在一起,允许它们交换实时媒体或数据流。 可以通过访问以下地址读取流 WebRTC 和网页:

http://localhost:8889/mystream

WHEP 是一种 WebRTC 扩展,允许通过 URL 读取流,而无需通过网页。 这允许将 WebRTC 用作通用流媒体协议。 如果使用支持 WHEP 的软件,可以通过以下 URL 从服务器读取流:

http://localhost:8889/mystream/whep
<script>
  class CameraPlayer {
    constructor(videoElement, cameraId) {
      this.video = videoElement;
      this.cameraId = cameraId;
      this.pc = new RTCPeerConnection();

      this.pc.ontrack = (event) => {
        if (event.track.kind === "video") {
          this.video.srcObject = event.streams[0];
        }
      };
    }

    async start() {
      // 创建本地描述
      await this.pc.setLocalDescription(await this.pc.createOffer());

      // 获取网关SDP
      const whepUrl = `http://localhost:8889/${this.cameraId}/whep`;
      const response = await fetch(whepUrl, {
        method: "POST",
        headers: { "Content-Type": "application/sdp" },
        body: this.pc.localDescription.sdp,
      });

      // 设置远程描述
      await this.pc.setRemoteDescription({
        type: "answer",
        sdp: await response.text(),
      });
    }
  }

  // 使用示例
  const video = document.getElementById("camera1");
  const player = new CameraPlayer(video, "cam101");
  player.start();
</script>

d. RTSP

RTSP 是一种允许发布和读取流的协议。它支持不同的底层传输协议,并允许在传输中加密流,要使用 RTSP 协议读取流,使用此 URL:

rtsp://localhost:8554/mystream

可以使用 RTSP 读取的已知客户端包括 FFmpeg、GStreamer 和 VLC。

e. RTMP

RTMP 是一种允许读取和发布流的协议,但其灵活性和效率不如 RTSP 和 WebRTC(不支持 UDP,不支持大多数 RTSP 编码,不支持反馈机制)。 可以通过以下 URL 从服务器读取流:

rtmp://localhost/mystream

在启用凭据的情况下,可以通过 userpass 查询参数将凭据传递给服务器:

rtmp://localhost/mystream?user=myuser&pass=mypass

可以使用 RTMP 读取的已知客户端包括 FFmpeg、GStreamer 和 VLC。

f. HLS

HLS 是一种通过将流分割成片段并通过 HTTP 协议提供这些片段和播放列表来工作的协议。 可以使用 MediaMTX 生成 HLS 流,该流可以通过网页访问:

http://localhost:8888/mystream

还可以通过支持 HLS 协议的软件(例如 VLC 或 MediaMTX 本身)通过以下 URL 访问:

http://localhost:8888/mystream/index.m3u8

可以使用 HLS 读取的已知客户端包括 FFmpeg、GStreamer、VLC 和 Web 浏览器。

5.MediaMTX 实现 RTSP 到 WebRTC 的转换原理

MediaMTX 充当媒体路由器角色,在输入(RTSP)和输出(WebRTC)协议间建立双向通道,无需深度解封装视频数据,而是通过以下关键步骤实现

性能优势 ​:

6. MediaMTX 其他配置

所有配置参数均在 mediamtx.yml 中列出并注释。

a. 认证

服务器提供三种身份验证用户的方法:

内部认证:用户存储在配置文件中,格式如下:

authMethod: internal
# 用户列表
authInternalUsers:
  # 默认的非特权用户。
  # 用户名。'any' 表示任何用户,包括匿名用户。
- user: any
  # 密码。 在 'any' 用户的情况下不使用。
  pass:
  # 允许使用此用户的 IP 或网络。 空列表表示任何 IP。
  ips: []
  # 权限列表。
    # 可用操作:publish、read、playback、api、metrics、pprof。
  - action: publish
    # 可以通过设置路径进一步限制访问特定路径。
    # 空路径表示任何路径。
    # 可以通过在前面加波浪号来使用正则表达式。
    path:
  - action: read
    path:
  - action: playback
    path:

基于HTTP的认证:身份验证可以委托给外部 HTTP 服务器:

authMethod: http
# 如果响应码为20x,则认证通过,否则认证失败。
authHTTPAddress: http://myauthserver/auth

每当需要对用户进行身份验证时,都会使用 POST 方法向指定的 URL 发送请求,并带有以下有效负载:

{
  "user": "user",
  "password": "password",
  "token": "token",
  "ip": "ip",
  "action": "publish|read|playback|api|metrics|pprof",
  "path": "path",
  "protocol": "rtsp|rtmp|hls|webrtc|srt",
  "id": "id",
  "query": "query"
}

如果 URL 返回以 20 开头的状态码(即 200),则身份验证成功,否则失败。 请注意,身份验证服务器接收到的请求中用户名和密码为空是完全正常的,即:

{
  "user": "",
  "password": ""
}

这是因为 RTSP 客户端在被要求时才会提供凭据。 为了接收凭据,身份验证服务器必须回复状态码 401,然后客户端将发送凭据。

可以从该过程排除某些操作:

# 在 HTTP 基于身份验证的过程中排除的操作。
# 格式与用户权限相同。
authHTTPExclude:
- action: api
- action: metrics
- action: pprof

JWT-based:身份验证可以委托给外部身份服务器,该服务器能够生成 JWT 并提供 JWKS 端点。 与基于 HTTP 的方法相比,这种方法的优点在于,仅联系一次外部服务器,而不是每次请求都联系,从而大大提高了性能。将JWT放在 Authorization: Bearer HTTP 头中。

authMethod: jwt
authJWTJWKS: http://my_identity_server/jwks_endpoint
authJWTClaimKey: mediamtx_permissions

JWT 被期望包含一个声明,其中包含与用户权限相同格式的权限列表:

{
 "mediamtx_permissions": [
    {
      "action": "publish",
      "path": ""
    }
  ]
}

b. 重封装、重新编码、压缩

要更改流的格式、编码器或压缩,使用 FFmpegGStreamerMediaMTX 一起使用。 例如,要重新编码现有流,该流可在 /original 路径中获得,并将生成的流发布到 /compressed 路径,请编辑 mediamtx.yml,并用以下内容替换 paths 部分中的所有内容:

paths:
  compressed:
  original:
    runOnReady: >
      ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
        -c:v libx264 -pix_fmt yuv420p -preset ultrafast -b:v 600k
        -max_muxing_queue_size 1024 -f rtsp rtsp://localhost:$RTSP_PORT/compressed
    runOnReadyRestart: yes

c. 录制流到磁盘

要将可用流保存到磁盘,请设置 recordrecordPath 参数在配置文件中:

# 录制流到磁盘。
record: yes
# 录制片段的路径。
# 扩展名会自动添加。
# 可用变量有 %path (路径名称), %Y %m %d (年, 月, 日),
# %H %M %S (小时, 分钟, 秒), %f (微秒), %z (时区), %s (unix 纪元).
recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f

所有可用的录制参数都在 示例配置文件/mediamtx.yml 中列出。

d. 回放录制的流

可以通过专用 HTTP 服务器向用户提供现有录音流,该服务器可以通过设置 playback 参数在配置中启用:

# 启用从回放服务器下载录制内容。
playback: no
# 回放服务器监听器的地址。
playbackAddress: :9996

服务器提供一个端点来列出录制的时间段:

http://localhost:9996/list?path=[mypath]&start=[start]&end=[end]

其中:

服务器将以 JSON 格式返回时间段列表:

[
  {
    "start": "2006-01-02T15:04:05Z07:00",
    "duration": 60.0,
    "url": "http://localhost:9996/get?path=[mypath]&start=2006-01-02T15%3A04%3A05Z07%3A00&duration=60.0"
  },
  {
    "start": "2006-01-02T15:07:05Z07:00",
    "duration": 32.33,
    "url": "http://localhost:9996/get?path=[mypath]&start=2006-01-02T15%3A07%3A05Z07%3A00&duration=32.33"
  }
]

服务器提供一个端点来下载录制:

http://localhost:9996/get?path=[mypath]&start=[start]&duration=[duration]&format=[format]

其中:

所有参数都必须是 url-encoded。 例如:

http://localhost:9996/get?path=mypath&start=2024-01-14T16%3A33%3A17%2B00%3A00&duration=200.5

生成的流使用 fMP4 格式,该格式与任何浏览器本地兼容,因此其 URL 可以直接插入到 <video> 标签中:

<video controls>
  <source src="http://localhost:9996/get?path=[mypath]&start=[start_date]&duration=[duration]" type="video/mp4" />
</video>

由于某些播放器的兼容性有限,fMP4 格式可能会出现问题。 要解决此问题,可以使用标准 MP4 格式,通过在 /get 请求中添加 format=mp4

http://localhost:9996/get?path=[mypath]&start=[start_date]&duration=[duration]&format=mp4

e. 将流转发到其他服务器

要将传入流转发到其他服务器,请在 runOnReady 参数中使用 FFmpeg

pathDefaults:
  runOnReady: >
    ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
    -c copy
    -f rtsp rtsp://other-server:8554/another-path
  runOnReadyRestart: yes

f. 按需发布

编辑 mediamtx.yml,用以下内容替换 paths 部分中的所有内容:

paths:
  ondemand:
    runOnDemand: ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH
    runOnDemandRestart: yes

runOnDemand 中插入的命令仅在客户端请求路径 ondemand 时启动,因此文件仅在请求时开始流媒体。

g. 路由绝对时间戳

某些流媒体协议允许路由与每帧关联的绝对时间戳,这对于同步多个视频或数据流非常有用。 特别是,MediaMTX 支持使用以下协议和设备接收绝对时间戳:

并支持通过以下协议发送绝对时间戳:

一个可以与 HLS 一起读取绝对时间戳的库是 gohlslib

一个可以与 RTSP 一起读取绝对时间戳的库是 gortsplib

如果浏览器支持 WebRTC,则可以通过 estimatedPlayoutTimestamp 指标来连接。

h. 在子文件夹中暴露服务器

基于 HTTP 的服务(WebRTC、HLS、控制 API、回放服务器、指标、pprof)可以暴露在现有 HTTP 服务器或反向代理的子文件夹中。 反向代理必须能够拦截发送到 MediaMTX 的 HTTP 请求及其相应的响应,并执行以下操作:

如果 nginx 是反向代理,则可以通过以下配置实现:

location /subpath/ {
    proxy_pass http://mediamtx-ip:8889/;
    proxy_redirect / /subpath/;
}

如果 Apache HTTP Server 是反向代理,则可以通过以下配置实现:

<Location /subpath>
    ProxyPass http://mediamtx-ip:8889
    ProxyPassReverse http://mediamtx-ip:8889
    Header edit Location ^(.*)$ "/subpath$1"
</Location>

如果 Caddy 是反向代理,则可以通过以下配置实现:

:80 {
    handle_path /subpath/* {
        reverse_proxy {
            to mediamtx-ip:8889
            header_down Location ^/ /subpath/
        }
    }
}

i. 开机自启

Linux

在大多数 Linux 发行版(包括 Ubuntu 和 Debian,但不包括 OpenWrt)中,systemd 负责管理服务并在启动时启动它们。

将服务器可执行文件和配置移动到全局文件夹:

sudo mv mediamtx /usr/local/bin/
sudo mv mediamtx.yml /usr/local/etc/

创建一个 systemd 服务:

sudo tee /etc/systemd/system/mediamtx.service >/dev/null << EOF
[Unit]
Wants=network.target
[Service]
ExecStart=/usr/local/bin/mediamtx /usr/local/etc/mediamtx.yml
[Install]
WantedBy=multi-user.target
EOF

如果启用了 SELinux(例如在 RedHat、Rocky、CentOS++ 的情况下),请添加正确的安全上下文:

semanage fcontext -a -t bin_t /usr/local/bin/mediamtx
restorecon -Fv /usr/local/bin/mediamtx

启用并启动服务:

sudo systemctl daemon-reload
sudo systemctl enable mediamtx
sudo systemctl start mediamtx
OpenWrt

将服务器可执行文件和配置移动到全局文件夹:

mv mediamtx /usr/bin/
mkdir -p /usr/etc && mv mediamtx.yml /usr/etc/

创建一个 procd 服务:

tee /etc/init.d/mediamtx >/dev/null << EOF
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=95
STOP=01
start_service() {
    procd_open_instance
    procd_set_param command /usr/bin/mediamtx
    procd_set_param stdout 1
    procd_set_param stderr 1
    procd_close_instance
}
EOF

启用并启动服务:

chmod +x /etc/init.d/mediamtx
/etc/init.d/mediamtx enable
/etc/init.d/mediamtx start

查看服务器日志:

logread
Windows

下载 WinSW v2 可执行文件 并将其放置在与 mediamtx.exe 相同的文件夹中。

在同一文件夹中,创建一个名为 WinSW-x64.xml 的文件,内容如下:

<service>
  <id>mediamtx</id>
  <name>mediamtx</name>
  <description></description>
  <executable>%BASE%/mediamtx.exe</executable>
</service>

打开终端,导航到文件夹并运行:

WinSW-x64 install

服务器现在作为系统服务安装,并将在启动时自动启动。

j. Hooks

服务器允许指定在发生某个事件时执行的命令,将事件传播到外部软件。

runOnConnect 允许在客户端连接到服务器时运行命令:

# 当客户端连接到服务器时要运行的命令。
# 当客户端断开与服务器的连接时,这将与 SIGINT 一起终止。
# 可用的环境变量有:
# * MTX_CONN_TYPE: 连接类型
# * MTX_CONN_ID: 连接 ID
# * RTSP_PORT: RTSP 服务器端口
runOnConnect: curl http://my-custom-server/webhook?conn_type=$MTX_CONN_TYPE&conn_id=$MTX_CONN_ID
# 如果命令退出,则重新启动该命令。
runOnConnectRestart: no

runOnDisconnect 允许在客户端断开与服务器的连接时运行命令:

# 当客户端断开与服务器的连接时要运行的命令。
# 环境变量与 runOnConnect 相同。
runOnDisconnect: curl http://my-custom-server/webhook?conn_type=$MTX_CONN_TYPE&conn_id=$MTX_CONN_ID

runOnInit 允许在初始化路径时运行命令。 这可用于在服务器启动时发布流:

paths:
  mypath:
    # 当初始化此路径时要运行的命令。
    # 这可用于在服务器启动时发布流。
    # 可用的环境变量有:
    # * MTX_PATH: 路径名称
    # * RTSP_PORT: RTSP 服务器端口
    # * G1, G2, ...: 正则表达式组,如果路径名称是
    #   正则表达式。
    runOnInit: ffmpeg -i my_file.mp4 -c copy -f rtsp rtsp://localhost:8554/mypath
    # 如果退出,则重新启动该命令。
    runOnInitRestart: no

runOnDemand 允许在读取器请求路径时运行命令。 这可用于按需发布流:

pathDefaults:
  # 当此路径被读取器请求时运行此命令
  # 且没有人再向此路径发布流时。
  # 当没有读者时,这将与 SIGINT 一起终止。
  # 可用的环境变量有:
  # * MTX_PATH: 路径名称
  # * MTX_QUERY: 查询参数(由第一个读取器传递)
  # * RTSP_PORT: RTSP 服务器端口
  # * G1, G2, ...: 正则表达式组,如果路径名称是
  #   正则表达式。
  runOnDemand: ffmpeg -i my_file.mp4 -c copy -f rtsp rtsp://localhost:8554/mypath
  # 如果退出,则重新启动该命令。
  runOnDemandRestart: no

runOnUnDemand 允许在不再有读取器时运行命令:

pathDefaults:
  # 当不再有读取器时要运行的命令。
  # 环境变量与 runOnDemand 相同。
  runOnUnDemand:

runOnReady 允许在流准备好被读取时运行命令:

pathDefaults:
  # 当流准备好被读取时要运行的命令,无论是
  # 由客户端发布还是从服务器/摄像头拉取。
  # 当流不再可用时,这将与 SIGINT 一起终止。
  # 可用的环境变量有:
  # * MTX_PATH: 路径名称
  # * MTX_QUERY: 查询参数(由发布者传递)
  # * MTX_SOURCE_TYPE: 源类型
  # * MTX_SOURCE_ID: 源 ID
  # * RTSP_PORT: RTSP 服务器端口
  # * G1, G2, ...: 正则表达式组,如果路径名称是
  #   正则表达式。
  runOnReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID
  # 如果退出,则重新启动该命令。
  runOnReadyRestart: no

runOnNotReady 允许在流不再可用时运行命令:

pathDefaults:
  # 当流不再可用时要运行的命令。
  # 环境变量与 runOnReady 相同。
  runOnNotReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID

runOnRead 允许在客户端开始读取时运行命令:

pathDefaults:
  # 当客户端开始读取时要运行的命令。
  # 当客户端停止读取时,这将与 SIGINT 一起终止。
  # 可用的环境变量有:
  # * MTX_PATH: 路径名称
  # * MTX_QUERY: 查询参数(由读取器传递)
  # * MTX_READER_TYPE: 读取器类型
  # * MTX_READER_ID: 读取器 ID
  # * RTSP_PORT: RTSP 服务器端口
  # * G1, G2, ...: 正则表达式组,如果路径名称是
  #   正则表达式。
  runOnRead: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID
  # 如果退出,则重新启动该命令。
  runOnReadRestart: no

runOnUnread 允许在客户端停止读取时运行命令:

pathDefaults:
  # 当客户端停止读取时要运行的命令。
  # 环境变量与 runOnRead 相同。
  runOnUnread: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID

runOnRecordSegmentCreate 允许在创建录制片段时运行命令:

pathDefaults:
  # 当创建录制片段时要运行的命令。
  # 可用的环境变量有:
  # * MTX_PATH: 路径名称
  # * MTX_SEGMENT_PATH: 段文件路径
  # * RTSP_PORT: RTSP 服务器端口
  # * G1, G2, ...: 正则表达式组,如果路径名称是
  #   正则表达式。
  runOnRecordSegmentCreate: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH

runOnRecordSegmentComplete 允许在录制片段完成时运行命令:

pathDefaults:
  # 当录制片段完成时要运行的命令。
  # 可用的环境变量有:
  # * MTX_PATH: 路径名称
  # * MTX_SEGMENT_PATH: 段文件路径
  # * MTX_SEGMENT_DURATION: 段持续时间
  # * RTSP_PORT: RTSP 服务器端口
  # * G1, G2, ...: 正则表达式组,如果路径名称是
  #   正则表达式。
  runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH

k. 控制 API

服务器可以通过设置 api 参数进行查询和控制,该参数可以通过设置 api 参数进行启用:

api: yes

要获取活动路径的列表,请运行:

curl http://127.0.0.1:9997/v3/paths/list

控制 API 的完整文档可在 专用网站 上找到。

请注意,默认情况下,控制 API 仅可通过 localhost 访问;要提高可见性或添加身份验证

三、附录

本地推流测试脚本

使用FFmpeg调取本地摄像头采集资源,推流到流媒体服务器

#!/bin/bash

# 压力测试配置
RTSP_SERVER="rtsp://192.168.50.55:8554"  # 流媒体服务器地址
STREAM_PREFIX="stream"              # 流名称前缀
NUM_STREAMS=10                       # 并发流数量
CAMERA_DEVICE="0"                    # 摄像头设备索引(0通常是内置摄像头)
RESOLUTION="640x480"                # 分辨率
FPS=30                               # 帧率
DURATION=3600                        # 测试持续时间(秒)

# 用于存储 ffmpeg 进程的 PID
pids=()

# 清理函数,用于终止所有 ffmpeg 进程
cleanup() {
    echo -e "\n正在终止所有推流进程..."
    # 先尝试使用保存的PID终止进程
    if [ ${#pids[@]} -gt 0 ]; then
        kill "${pids[@]}" 2>/dev/null
    fi
    
    # 额外确保所有ffmpeg进程都被终止
    pkill -f ffmpeg
    
    echo "测试完成,所有推流进程已终止。"
    exit 0
}

# 捕获中断信号 (Ctrl+C) 和终止信号,执行清理操作
trap cleanup INT TERM

# 启动多路推流
for ((i=1; i<=$NUM_STREAMS; i++)); do
    echo "启动摄像头推流 ${i}/${NUM_STREAMS}"
    sleep 0.5
    ffmpeg \
            -hide_banner \
            -loglevel error \
            -f avfoundation \
            -video_size $RESOLUTION \
            -framerate $FPS \
            -i "$CAMERA_DEVICE" \
            -c:v libx264 \
            -preset veryfast \
            -tune zerolatency \
            -pix_fmt yuv420p \
            -g $(($FPS*2)) \
            -b:v 800k \
            -maxrate 1000k \
            -bufsize 1000k \
            -an \
            -f rtsp \
            "$RTSP_SERVER/${STREAM_PREFIX}_${i}" > /dev/null 2>&1 &

    # 保存 ffmpeg 进程的 PID
    pids+=($!)
done

echo "已启动 $NUM_STREAMS 路摄像头RTSP推流,持续 $DURATION 秒"
echo "按  Ctrl+C  可提前终止测试"

# 等待指定的测试时间
sleep $DURATION

# 正常时间到达后执行清理
cleanup

网页通过WebRTC从流媒体服务器拉取全部流进行展示

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>多路WebRTC播放器</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background-color: #f5f5f5;
            font-size: 15px;
        }
        .container {
            width: 100%;
            margin: 0 auto;
            padding: 2vw;
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }
        .batch-config {
            margin-bottom: 18px;
            padding: 12px 0 8px 0;
        }
        .batch-config input {
            margin: 0 8px 0 0;
            padding: 7px;
            width: 180px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .batch-config label {
            margin-right: 8px;
        }
        .batch-btn {
            padding: 8px 20px;
            font-size: 16px;
        }
        .stream-list {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
            gap: 8px;
            margin-top: 10px;
        }
        .stream-item {
            border: 1px solid #eee;
            border-radius: 6px;
            padding: 8px 6px 6px 6px;
            background: #fafbfc;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .stream-config input {
            margin: 5px;
            padding: 7px;
            width: 160px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .controls {
            display: inline-block;
            margin-left: 10px;
        }
        .btn {
            margin: 5px;
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 15px;
        }
        .btn-success {
            background-color: #28a745;
            color: white;
        }
        .btn-danger {
            background-color: #dc3545;
            color: white;
        }
        .btn:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }
        .video-container {
            text-align: center;
            margin: 10px 0 0 0;
            width: 100%;
        }
        video {
            width: 100%;
            max-width: 300px;
            height: auto;
            background-color: #000;
            border-radius: 8px;
        }
        .status {
            padding: 6px 10px;
            margin: 8px 0;
            border-radius: 4px;
            text-align: center;
            font-size: 14px;
        }
        .status.success { background-color: #d4edda; color: #155724; }
        .status.error { background-color: #f8d7da; color: #721c24; }
        .status.info { background-color: #d1ecf1; color: #0c5460; }
        .add-btn {
            margin: 10px 0 0 0;
            padding: 8px 20px;
            font-size: 16px;
        }
        .stream-count {
            margin: 10px 0;
            font-weight: bold;
            font-size: 16px;
            color: #333;
        }
    </style>
</head>
<body>
<div class="container">
    <h1>多路WebRTC播放器</h1>
    <div class="batch-config">
        <label>服务器地址: <input type="text" id="batchServerUrl" value="http://192.168.50.55:8889" placeholder="服务器地址"></label>
        <label>流路径前缀: <input type="text" id="batchStreamPrefix" value="stream_" placeholder="流路径前缀"></label>
        <label>数量: <input type="number" id="batchCount" value="4" min="1" max="32" style="width:60px;"></label>
        <button id="batchAddBtn" class="btn btn-success batch-btn">批量添加</button>
        <button id="fetchStreamsBtn" class="btn btn-success batch-btn">从服务器获取流列表</button>
    </div>
    <div style="margin-bottom:18px;">
        <button id="playAllBtn" class="btn btn-success batch-btn">播放全部</button>
        <button id="stopAllBtn" class="btn btn-danger batch-btn">停止全部</button>
    </div>
    <div id="streamCountDisplay" class="stream-count" style="display:none;"></div>
    <div id="streamList" class="stream-list"></div>
</div>
<script>
    class MultiWebRTCPlayer {
        constructor() {
            this.streamList = document.getElementById('streamList');
            this.batchAddBtn = document.getElementById('batchAddBtn');
            this.batchServerUrl = document.getElementById('batchServerUrl');
            this.batchStreamPrefix = document.getElementById('batchStreamPrefix');
            this.batchCount = document.getElementById('batchCount');
            this.playAllBtn = document.getElementById('playAllBtn');
            this.stopAllBtn = document.getElementById('stopAllBtn');
            this.fetchStreamsBtn = document.getElementById('fetchStreamsBtn');
            this.streamCountDisplay = document.getElementById('streamCountDisplay');
            this.streamCount = 0;
            this.players = {};
            this.streamInfos = [];
            this.batchAddBtn.onclick = () => this.batchAdd();
            this.playAllBtn.onclick = () => this.playAll();
            this.stopAllBtn.onclick = () => this.stopAll();
            this.fetchStreamsBtn.onclick = () => this.fetchRtspStreams();
        }

        batchAdd() {
            const serverUrl = this.batchServerUrl.value.trim();
            const prefix = this.batchStreamPrefix.value.trim();
            let count = parseInt(this.batchCount.value, 10);
            if (!serverUrl || !prefix || isNaN(count) || count < 1) return;
            this.streamList.innerHTML = '';
            this.players = {};
            this.streamInfos = [];
            this.streamCount = 0;
            this.streamCountDisplay.style.display = 'none';

            for (let i = 1; i <= count; i++) {
                const streamPath = `${prefix}${i}`;
                this.addStream(serverUrl, streamPath);
            }
        }

        async fetchRtspStreams() {
            try {
                const serverUrl = this.batchServerUrl.value.trim();
                if (!serverUrl) {
                    alert('请输入服务器地址');
                    return;
                }

                // 解析服务器地址,获取IP
                const urlParts = serverUrl.split('://');
                const hostPart = urlParts[1] ? urlParts[1].split(':')[0] : urlParts[0].split(':')[0];

                // 构建RTSP会话列表API URL
                const rtspApiUrl = `http://${hostPart}:9997/v3/rtspsessions/list`;

                const response = await fetch(rtspApiUrl, {
                    method: 'GET',
                    headers: {
                        'Authorization': 'Basic YWRtaW46WlhXTDIwMjU='
                    }
                });

                if (!response.ok) {
                    throw new Error(`HTTP错误: ${response.status}`);
                }

                const data = await response.json();

                // 清空现有流
                this.streamList.innerHTML = '';
                this.players = {};
                this.streamInfos = [];
                this.streamCount = 0;

                // 显示流数量
                this.streamCountDisplay.textContent = `服务器上共有 ${data.itemCount} 路流`;
                this.streamCountDisplay.style.display = 'block';

                // 添加每个流
                data.items.forEach(item => {
                    if (item.state === 'publish' && item.path) {
                        this.addStream(serverUrl, item.path);
                    }
                });

            } catch (error) {
                alert(`获取流列表失败: ${error.message}`);
                console.error('获取流列表失败:', error);
            }
        }

        addStream(serverUrl, streamPath) {
            const idx = ++this.streamCount;
            const whepUrl = `${serverUrl}/${streamPath}/whep`;
            const item = document.createElement('div');
            item.className = 'stream-item';
            item.id = `stream-item-${idx}`;
            item.innerHTML = `
            <div style="width:100%;text-align:center;font-weight:bold;word-break:break-all;font-size:13px;">${whepUrl}</div>
            <div class="video-container">
                <video id="video-${idx}" controls autoplay muted playsinline></video>
            </div>
            <div id="status-${idx}" class="status" style="display:none;"></div>
        `;
            this.streamList.appendChild(item);
            this.players[idx] = new SimpleWebRTCPlayer(idx, whepUrl);
            this.streamInfos.push({idx, whepUrl});
        }

        playAll() {
            Object.values(this.players).forEach(player => player.start());
        }

        stopAll() {
            Object.values(this.players).forEach(player => player.stop());
        }
    }

    class SimpleWebRTCPlayer {
        constructor(idx, whepUrl) {
            this.idx = idx;
            this.whepUrl = whepUrl;
            this.pc = null;
            this.video = document.getElementById(`video-${idx}`);
            this.status = document.getElementById(`status-${idx}`);
        }

        showStatus(message, type = 'info') {
            this.status.textContent = message;
            this.status.className = `status ${type}`;
            this.status.style.display = 'block';
        }

        async start() {
            try {
                this.showStatus('正在连接...', 'info');
                this.pc = new RTCPeerConnection({});
                this.pc.ontrack = (event) => {
                    if (event.track.kind === 'video') {
                        this.video.srcObject = event.streams[0];
                        this.showStatus('视频流已连接', 'success');
                        this.video.play().catch(() => this.createPlayButton());
                    }
                };
                this.pc.oniceconnectionstatechange = () => {
                    const state = this.pc.iceConnectionState;
                    if (state === 'failed' || state === 'disconnected') {
                        this.showStatus('连接失败', 'error');
                    } else if (state === 'connected' || state === 'completed') {
                        this.showStatus('连接成功', 'success');
                    }
                };
                this.pc.addTransceiver('video', {direction: 'recvonly'});
                this.pc.addTransceiver('audio', {direction: 'recvonly'});
                const offer = await this.pc.createOffer();
                await this.pc.setLocalDescription(offer);
                const response = await fetch(this.whepUrl, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/sdp'},
                    body: offer.sdp
                });
                if (!response.ok) throw new Error(`HTTP错误: ${response.status}`);
                const answer = await response.text();
                await this.pc.setRemoteDescription({type: 'answer', sdp: answer});
            } catch (error) {
                this.showStatus(`连接失败: ${error.message}`, 'error');
            }
        }

        stop() {
            if (this.pc) {
                this.pc.close();
                this.pc = null;
            }
            if (this.video.srcObject) {
                this.video.srcObject.getTracks().forEach(track => track.stop());
                this.video.srcObject = null;
            }
            this.status.style.display = 'none';
            const playBtn = document.getElementById(`manualPlayBtn-${this.idx}`);
            if (playBtn) playBtn.remove();
        }

        createPlayButton() {
            if (document.getElementById(`manualPlayBtn-${this.idx}`)) return;
            const playBtn = document.createElement('button');
            playBtn.id = `manualPlayBtn-${this.idx}`;
            playBtn.textContent = '点击播放视频';
            playBtn.className = 'btn btn-success';
            playBtn.style.margin = '10px';
            playBtn.onclick = () => {
                this.video.play().then(() => {
                    this.showStatus('视频正在播放', 'success');
                    playBtn.remove();
                });
            };
            this.video.parentNode.appendChild(playBtn);
        }
    }

    document.addEventListener('DOMContentLoaded', () => {
        new MultiWebRTCPlayer();
    });
</script>
</body>
</html>