Featured image of post 海康摄像头按需推流:Java + FFmpeg + Nginx-rtmp 浏览器播放

海康摄像头按需推流:Java + FFmpeg + Nginx-rtmp 浏览器播放

海康威视摄像头 RTSP → RTMP 浏览器播放完整方案:Vue video-player 前端、Java 后端调 FFmpeg 推流、Nginx-rtmp-module 服务端、按需关流

一、为什么浏览器不能直接播 RTSP

海康 / 大华等安防摄像头默认协议是 RTSP(Real Time Streaming Protocol),浏览器原生不支持 RTSP——Chrome / Edge / Firefox 都不行。

两条主流路径让浏览器能播摄像头:

  1. RTSP → RTMP → Flash / HLS老方案)—— 需 nginx-rtmp-module + video.js / videojs-flash
  2. RTSP → WebRTC / HLS新方案)—— 用 ZLMediaKit / MediaMTX / go2rtc 媒体网关,浏览器原生 video 标签

本文讲第一种老方案——因为很多 2018-2020 年的项目(GB28181 视频监控平台)仍依赖此架构,且控制粒度细按需推流 = 节省服务器资源)。

二、整体流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
点击播放按钮
1. 判断 ffmpeg 进程是否存在
    ↓ 存在
2. 获取进程 PID
3. 用 taskkill 关闭 ffmpeg(关流)
    ↓ 不存在
4. 启动 ffmpeg 进程:rtsp → rtmp
5. nginx-rtmp-module 接收 rtmp 流
6. 浏览器用 video.js / vue-video-player 播放

关键点

  • 按需推流 = 不点就不推ffmpeg 进程按需启动 / 关闭
  • FFmpeg = RTSP → RTMP 转协议 + 转封装
  • Nginx-rtmp-module = RTMP 服务端,支持 HLS / DASH 切片
  • vue-video-player = 浏览器播放器,带 RTMP 播放能力(需 videojs-flash 插件)

三、服务端部署

3.1 安装 Nginx + nginx-rtmp-module

Windows 编译 nginx-rtmp-module 较麻烦,推荐用现成的 Windows 集成版

  • 项目:nginx-rtmp-win32(GitHub 搜 nginx-rtmp-win32nginx-rtmp-module-win32
  • 下载后解压到 D:\nginx-ffmpeg\

3.2 nginx.conf 配置

 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
worker_processes  1;
events {
    worker_connections  1024;
}

# RTMP 服务
rtmp {
    server {
        listen 1935;
        chunk_size 4096;

        application live {
            live on;
            record off;

            # 推流后自动 HLS 切片
            hls on;
            hls_path D:/nginx-ffmpeg/hls;
            hls_fragment 5s;
            hls_playlist_length 30s;
        }
    }
}

# HTTP 服务(提供 HLS 播放)
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile      on;
    keepalive_timeout 65;

    server {
        listen 8080;
        server_name localhost;

        # 静态 HLS 文件
        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root D:/nginx-ffmpeg/;
            add_header Cache-Control no-cache;
        }
    }
}

启动 nginx:

1
2
cd D:\nginx-ffmpeg
nginx.exe

测试 RTMP 端口:

1
Test-NetConnection localhost -Port 1935

3.3 安装 FFmpeg

  • 官方:https://ffmpeg.org/download.html#build-windows
  • 推荐:gyan.dev 出品的 ffmpeg-release-essentials.zip
  • 解压到 D:\nginx-ffmpeg\ffmpeg\bin\

D:\nginx-ffmpeg\ffmpeg\bin\ 加到 PATH

四、FFmpeg 推流命令

1
2
3
ffmpeg -i "rtsp://USERNAME:PASSWORD@IP:PORT/Streaming/Channels/<通道ID>" ^
   -b 4096k -f flv -r 25 -s 1920x1080 -an ^
   "rtmp://127.0.0.1:1935/live/<自定义名称>"

参数说明:

  • -i —— 输入 RTSP URL
  • -b 4096k —— 视频比特率 4 Mbps
  • -f flv —— 输出格式 FLV(RTMP 走 FLV 封装)
  • -r 25 —— 帧率 25 fps
  • -s 1920x1080 —— 分辨率
  • -an —— 去掉音频RTMP 推摄像头画面不需要音轨
  • 最后是 RTMP URL,<自定义名称> 是 stream key

4.1 海康 RTSP URL 格式

1
2
3
4
5
6
7
rtsp://<username>:<password>@<ip>:<port>/Streaming/Channels/<channel_id>

# 主码流(高清)
rtsp://admin:password123@192.168.1.100:554/Streaming/Channels/101

# 子码流(流畅)
rtsp://admin:password123@192.168.1.100:554/Streaming/Channels/102

101 = 通道 1 主码流,102 = 通道 1 子码流。

五、Java 后端:调 FFmpeg 推流

5.1 项目结构

1
2
3
4
src/main/java/com/example/videoplay/
├── VideoPlayService.java    # 服务类
├── StreamManager.java       # 流管理(启停)
└── ...

5.2 ProcessBuilder 启动 FFmpeg

 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
import java.io.*;
import java.util.*;

public class StreamManager {

    /**
     * 启动 ffmpeg 推流
     * @param rtsp  摄像头 RTSP 地址
     * @param rtmp  RTMP 推流地址
     * @param s     分辨率(如 "1920x1080")
     * @return      启动的进程(用于后续关流)
     */
    public static Process openLiu(String rtsp, String rtmp, String s) {
        if (s == null) s = "1920x1080";

        ProcessBuilder builder = new ProcessBuilder();
        List<String> commend = new ArrayList<>();
        commend.add("D:/nginx-ffmpeg/ffmpeg/bin/ffmpeg.exe");
        commend.add("-i");
        commend.add(rtsp);
        commend.add("-f");
        commend.add("flv");
        commend.add("-r");
        commend.add("25");
        commend.add("-s");
        commend.add(s);
        commend.add("-an");
        commend.add(rtmp);

        builder.command(commend);
        builder.redirectErrorStream(true);

        try {
            return builder.start();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 关闭 ffmpeg 进程(关流)
     * @param pid  ffmpeg 进程 PID
     */
    public static void closeLiu(String pid) {
        try {
            Runtime.getRuntime().exec("taskkill /F /PID " + pid);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.3 获取 FFmpeg 进程 PID

 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
/**
 * 查 ffmpeg 进程的 PID
 */
public static String getPid() {
    String pid = null;
    try {
        Process process = Runtime.getRuntime().exec(
            "tasklist /fi \"imagename eq ffmpeg.exe\" /fo list"
        );
        InputStream inputStream = process.getInputStream();
        BufferedReader bf = new BufferedReader(
            new InputStreamReader(inputStream, "GBK")
        );
        String line;
        while ((line = bf.readLine()) != null) {
            if (line.startsWith("PID")) {
                String[] array = line.split(":");
                pid = array[1].trim();
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return pid;
}

编码必须是 GBK——tasklist.exe 在中文 Windows 上输出是 GBK 编码,UTF-8 会乱码

5.4 关流流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 1. 查进程是否存在
String pid = StreamManager.getPid();

if (pid != null) {
    // 2. 关流
    StreamManager.closeLiu(pid);
    // 3. 短暂等待 ffmpeg 退出
    Thread.sleep(500);
}

// 4. 推新流
Process process = StreamManager.openLiu(rtsp, rtmp, "1920x1080");

六、前端:Vue + vue-video-player

6.1 安装

1
2
npm install --save vue-video-player
npm install --save videojs-flash

6.2 组件

 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
<template>
  <videoPlayer
    ref="videoPlayer"
    :options="videoOptions"
    class="vjs-custom-skin videoPlayer"
    :playsinline="false"
    @play="onPlayerPlay()"
  />
</template>

<script>
import 'video.js/dist/video-js.css'
import { videoPlayer } from 'vue-video-player'
import 'videojs-flash'

export default {
  components: { videoPlayer },
  data() {
    return {
      videoOptions: {
        playbackRates: [0.7, 1.0, 1.5, 2.0],
        autoplay: false,
        muted: false,
        loop: false,
        preload: 'auto',
        language: 'zh-CN',
        aspectRatio: '16:9',
        techOrder: ['flash', 'html5'],   // 兼容顺序
        sources: [{
          type: 'rtmp/hls',
          src: 'rtmp://127.0.0.1:1935/live/cam001'  // 你的 RTMP 地址
        }],
        poster: '',
        notSupportedMessage: '此视频暂无法播放,请稍后再试',
        controlBar: {
          timeDivider: true,
          durationDisplay: true,
          remainingTimeDisplay: false,
          fullscreenToggle: true
        }
      }
    }
  },
  methods: {
    onPlayerPlay() {
      // 暂停其他视频
      this.$refs.videoPlayer.player.pause()

      // 调后端 API
      this.$http.get('/api/stream/play', {
        params: {
          rtsp: this.videoRtsp,
          rtmp: this.videoRtmp,
          fenbianlv: '1920x1080'
        }
      })
    }
  }
}
</script>

techOrder: ['flash', 'html5'] —— 优先 Flash(RTMP),回退 HTML5(HLS)。但 Chrome 88+ 已经彻底禁用 Flash所以生产请直接用 HLS

七、按需推流:点击播放事件

 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
onPlayerPlay() {
  this.$refs.videoPlayer.player.pause()

  // 1. 查流进程
  this.$http.get('/api/stream/isLiu').then(res => {
    const liuSta = res.data.result

    if (liuSta) {
      // 2. 进程存在 → 取 PID
      this.$http.get('/api/stream/getFFpid').then(res => {
        const pid = res.data.message

        // 3. 关流
        this.$http.post('/api/stream/closeLiu', { pid }).then(() => {

          // 4. 推新流
          this.$http.post('/api/stream/openLiu', {
            rtsp: this.videoRtsp,
            rtmp: this.videoRtmp,
            fenbianlv: '1920x1080'
          })
        })
      })
    } else {
      // 5. 进程不存在 → 直接推
      this.$http.post('/api/stream/openLiu', {
        rtsp: this.videoRtsp,
        rtmp: this.videoRtmp,
        fenbianlv: '1920x1080'
      })
    }
  })
}

八、生产级方案升级

8.1 RTMP → HLS 切片

nginx-rtmp-module 自动把 RTMP 切成 HLS 切片:

  • 浏览器用 http://server:8080/hls/cam001.m3u8 播放
  • 支持原生 video 标签不用 Flash
  • 延迟 5-10 秒HLS 切片固定延迟

8.2 用 ZLMediaKit 替代 nginx-rtmp

  • 项目:https://github.com/ZLMediaKit/ZLMediaKit
  • 支持 RTSP/RTMP/HLS/HTTP-FLV/WebRTC 全协议
  • 延迟可调到 1 秒级
  • 2018-2023 持续维护,生产首选

8.3 用 go2rtc 做"轻量转协议"

  • 项目:https://github.com/alexxit/go2rtc
  • 几乎零配置,Docker 一行起
  • 支持 RTSP / RTMP / WebRTC / MSE / HLS 多协议转发

8.4 浏览器兼容性

协议ChromeEdgeFirefoxSafari
RTMP❌(Flash 88+ 已删)
HLS
HTTP-FLV✅(via MSE)
WebRTC

结论生产请走 HLS(兼容 Safari)或 WebRTC(低延迟)。RTMP 已经是 2010 年代技术。

九、常见 5 个坑

  1. RTSP URL 认证失败——海康 RTSP 默认需要先在摄像头后台开启 RTSP 服务(路径:网络 → 平台接入 → RTSP)
  2. ffmpeg 推流卡顿——-b 4096k 太高,降到 2048k 或 1024k或者 -c:v copy 不转码前提是摄像头 H.264
  3. nginx-rtmp hls 切片生成失败——hls_path 目录nginx 没写权限
  4. videojs-flash 加载失败——Chrome 88+ 不再支持 Flash改用 videojs-http-streaming (VHS) 播 HLS
  5. ffmpeg 进程关不掉——taskkill /F /PID 仍关不掉时,wmic process where "name='ffmpeg.exe'" call terminate

十、总结

  • 老方案 = FFmpeg + nginx-rtmp + video.js / vue-video-player(2018-2020 经典
  • 新方案 = ZLMediaKit / MediaMTX / go2rtc 直接做 RTSP → HLS/WebRTC(2021+ 推荐
  • 按需推流 = 不点就不开 ffmpeg省 CPU 省带宽
  • Java 调 FFmpegProcessBuilder + tasklist /fi "imagename eq ffmpeg.exe" 查 PID + taskkill /F /PID
  • 浏览器兼容RTMP 已死Flash 88+ 被删),走 HLS / WebRTC / HTTP-FLV
  • 延迟排序:WebRTC (~500ms) < HTTP-FLV (~1s) < HLS (~5-10s) < RTMP (~1-2s)

参考资料

使用 Hugo 构建
主题 StackJimmy 设计