java网络摄像头播放、截图、rtsp转m3u8

10 篇文章 0 订阅
2 篇文章 1 订阅
该博客介绍了如何使用JavaCV库进行摄像头的播放、截图以及RTSP流转换为HLS流的操作。通过引入javacv-platform依赖,实现了网络摄像头视频播放、截图功能,并详细展示了转换RTSP到m3u8的代码实现,包括关键参数配置。最后,给出了使用HTML和Vue播放转换后HLS流的示例。
摘要由CSDN通过智能技术生成

1. 依赖库

		<!-- https://mvnrepository.com/artifact/org.bytedeco/javacv-platform -->
		<dependency>
		    <groupId>org.bytedeco</groupId>
		    <artifactId>javacv-platform</artifactId>
		    <version>1.5.7</version>
		</dependency>

2. 网络摄像头视频播放

入参:

String rtspURL = "rtsp://admin:risun8768@192.168.10.200:554/Streaming/Channels/601";

CameraUtil.play(rtspURL);

	/** 摄像头播放 */
	public static void play(String rtspURL) throws IOException {
		FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(rtspURL);
		grabber.setOption("rtsp_transport", "tcp"); 
		grabber.setImageWidth(960);
		grabber.setImageHeight(540);
		grabber.start();
		CanvasFrame canvasFrame = new CanvasFrame("摄像机");
		canvasFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		canvasFrame.setAlwaysOnTop(true); 
		OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat(); 
		while (true){ 
			Frame frame =grabber.grabImage();
			Mat mat = converter.convertToMat(frame);
			canvasFrame.showImage(frame); 
		}
	}

 

3. 截图

入参:

String rtspURL = "rtsp://admin:risun8768@192.168.10.200:554/Streaming/Channels/601";

String fileName = "D:/code/java/risun-smartsite/ruoyi-admin/camera_files_path/img_in/2/0.jpg";

CameraUtil.screenshot(rtspURL, fileName);

	/** 摄像头截图 */
	public static void screenshot(String rtspURL, String fileName) throws IOException {
		FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(rtspURL);
		grabber.setOption("rtsp_transport", "tcp"); 
		grabber.setImageWidth(960);
		grabber.setImageHeight(540);
		grabber.start();
		File outPut = new File(fileName); 
		while (true){ 
			Frame frame = grabber.grabImage(); 
			if (frame != null) {
				ImageIO.write(FrameToBufferedImage(frame), "jpg", outPut); 
				grabber.stop();
				grabber.release();
				break; 
			}
		}
	}

4. rtsp转m3u8

	/** 转m3u8 */
	public void push(String input, String output)
			throws org.bytedeco.javacv.FrameGrabber.Exception, org.bytedeco.javacv.FrameRecorder.Exception {
		FFmpegFrameGrabber grabber = null;// 采集器
		FFmpegFrameRecorder recorder = null;// 解码器
		int bitrate = 2500000;// 比特率
		double framerate;// 帧率
		int err_index = 0;// 推流过程中出现错误的次数
		int timebase;// 时钟基
		long dts = 0, pts = 0;// pkt的dts、pts时间戳
		try {
			// 开启ffmpeg日志级别;QUIET是屏蔽所有,可选INFO、DEBUG、ERROR等
			avutil.av_log_set_level(avutil.AV_LOG_INFO);
			FFmpegLogCallback.set();
			grabber = new FFmpegFrameGrabber(input);
			grabber.start();
			// 异常的framerate,强制使用25帧
			if (grabber.getFrameRate() > 0 && grabber.getFrameRate() < 100) {
				framerate = grabber.getFrameRate();
			} else {
				framerate = 25.0;
			}
			bitrate = grabber.getVideoBitrate();// 获取到的比特率 0
			recorder = new FFmpegFrameRecorder(output, grabber.getImageWidth(), grabber.getImageHeight(), 0);
			// 设置比特率
			recorder.setVideoBitrate(bitrate);
			// h264编/解码器
			recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
			// 设置音频编码
			recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
			// 视频帧率(保证视频质量的情况下最低25,低于25会出现闪屏)
			recorder.setFrameRate(framerate);
			// 关键帧间隔,一般与帧率相同或者是视频帧率的两倍
			recorder.setGopSize((int) framerate);
			// 解码器格式
			recorder.setFormat("hls");
			// 单个切片时长,单位是s,默认为2s
			recorder.setOption("hls_time", "60");
			// HLS播放的列表长度,0标识不做限制
			recorder.setOption("hls_list_size", "0");
			// 设置切片的ts文件序号起始值,默认从0开始,可以通过此项更改
			recorder.setOption("start_number", "120");
//			recorder.setOption("hls_segment_type","mpegts");
			// 自动删除切片,如果切片数量大于hls_list_size的数量,则会开始自动删除之前的ts切片,只保 留hls_list_size个数量的切片
			// recorder.setOption("hls_flags","delete_segments");
			// ts切片自动删除阈值,默认值为1,表示早于hls_list_size+1的切片将被删除
			// recorder.setOption("hls_delete_threshold","1");
			/*
			 * hls的切片类型: 'mpegts':以MPEG-2传输流格式输出ts切片文件,可以与所有HLS版本兼容。 'fmp4':以Fragmented
			 * MP4(简称:fmp4)格式输出切片文件,类似于MPEG-DASH,fmp4文件可用于HLS version 7和更高版本。
			 */
//			recorder.setOption("hls_segment_type","mpegts");

			AVFormatContext fc = null;
			fc = grabber.getFormatContext();
			recorder.start(fc);
			// 清空探测时留下的缓存
//			grabber.flush();

			AVPacket pkt = null;
			for (int no_pkt_index = 0; no_pkt_index < 5 && err_index < 5;) {
				pkt = grabber.grabPacket();
				if (pkt == null || pkt.size() <= 0 || pkt.data() == null) {
					Thread.sleep(1);
					no_pkt_index++;
					continue;
				}
				// 获取到的pkt的dts,pts异常,将此包丢弃掉。
				if (pkt.dts() == avutil.AV_NOPTS_VALUE && pkt.pts() == avutil.AV_NOPTS_VALUE || pkt.pts() < dts) {
					err_index++;
					
					av_packet_unref(pkt);
					continue;
				}
				// 矫正dts,pts
				pkt.pts(pts);
				pkt.dts(dts);
				err_index += (recorder.recordPacket(pkt) ? 0 : 1);
				// pts,dts累加
				timebase = grabber.getFormatContext().streams(pkt.stream_index()).time_base().den();

				pts += (timebase / (int) framerate);
				dts += (timebase / (int) framerate);
			}
		} catch (Exception e) {
			grabber.stop();
			grabber.close();
			if (recorder != null) {
				recorder.stop();
				recorder.release();
			}
		} finally {
			grabber.stop();
			grabber.close();
			if (recorder != null) {
				recorder.stop();
				recorder.release();
			}
		}
	}

转换输出如下: 

 

用nginx或其它web服务代理这个目录,在浏览器访问结果如下(注意跨域): 

 写个简单的测试网页:

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>video.js</title>
    <link href="https://unpkg.com/video.js@6.11.0/dist/video-js.min.css" rel="stylesheet">
    <script src="https://unpkg.com/video.js@6.11.0/dist/video.min.js"></script>
    <script src="https://unpkg.com/videojs-flash/dist/videojs-flash.js"></script>
    <script src="https://unpkg.com/videojs-contrib-hls/dist/videojs-contrib-hls.js"></script>
  </head>
  <body>
    <div>
        <p>测试</p>
        <video id="my-player1" autoplay class="video-js" controls>
            <source src="http://192.168.1.132:8765/img/m3u8/0.m3u8" type="application/x-mpegURL">
            <p class="vjs-no-js">
                 not support
            </p>
        </video>
        <script type="text/javascript">
            var player = videojs('my-player1',{
                width:704,
                heigh:576
                });
        </script>
    </div>
  </body>
</html>

效果如下:

 VUE播放参考:

vue video播放m3u8源icon-default.png?t=M85Bhttps://blog.csdn.net/zj850324/article/details/126470971?spm=1001.2014.3001.5502

附上完整工具类代码:

package com.ruoyi.ai.tools;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;

import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
// import org.bytedeco.javacpp.opencv_core;
import org.bytedeco.javacv.CanvasFrame;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FFmpegLogCallback;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.opencv_core.Mat;
import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;

/**
 * @author created by Jerry
 * @date 2022年9月8日---上午10:49:18
 * 摄像头数据处理工具
 */
public class CameraUtil {
	/** 摄像头截图 */
	public static void screenshot(String rtspURL, String fileName) throws IOException {
		FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(rtspURL);
		grabber.setOption("rtsp_transport", "tcp"); 
		grabber.setImageWidth(960);
		grabber.setImageHeight(540);
		grabber.start();
		File outPut = new File(fileName); 
		while (true){ 
			Frame frame = grabber.grabImage(); 
			if (frame != null) {
				ImageIO.write(FrameToBufferedImage(frame), "jpg", outPut); 
				grabber.stop();
				grabber.release();
				break; 
			}
		}
	}
	/** 摄像头播放 */
	public static void play(String rtspURL) throws IOException {
		FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(rtspURL);
		grabber.setOption("rtsp_transport", "tcp"); 
		grabber.setImageWidth(960);
		grabber.setImageHeight(540);
		grabber.start();
		CanvasFrame canvasFrame = new CanvasFrame("摄像机");
		canvasFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		canvasFrame.setAlwaysOnTop(true); 
		OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat(); 
		while (true){ 
			Frame frame =grabber.grabImage();
			Mat mat = converter.convertToMat(frame);
			canvasFrame.showImage(frame); 
		}
	}

	/**
	 * 创建BufferedImage对象
	 */
	public static BufferedImage FrameToBufferedImage(Frame frame) {
		Java2DFrameConverter converter = new Java2DFrameConverter();
		BufferedImage bufferedImage = converter.getBufferedImage(frame);
//		bufferedImage=rotateClockwise90(bufferedImage);
		return bufferedImage;
	}

	/**
	 * 处理图片,将图片旋转90度。
	 */
	public static BufferedImage rotateClockwise90(BufferedImage bi) {
		int width = bi.getWidth();
		int height = bi.getHeight();
		BufferedImage bufferedImage = new BufferedImage(height, width, bi.getType());
		for (int i = 0; i < width; i++)
			for (int j = 0; j < height; j++)
				bufferedImage.setRGB(j, i, bi.getRGB(i, j));
		return bufferedImage;
	}
	
	/** 转m3u8 */
	public void push(String input, String output)
			throws org.bytedeco.javacv.FrameGrabber.Exception, org.bytedeco.javacv.FrameRecorder.Exception {
		FFmpegFrameGrabber grabber = null;// 采集器
		FFmpegFrameRecorder recorder = null;// 解码器
		int bitrate = 2500000;// 比特率
		double framerate;// 帧率
		int err_index = 0;// 推流过程中出现错误的次数
		int timebase;// 时钟基
		long dts = 0, pts = 0;// pkt的dts、pts时间戳
		try {
			// 开启ffmpeg日志级别;QUIET是屏蔽所有,可选INFO、DEBUG、ERROR等
			avutil.av_log_set_level(avutil.AV_LOG_INFO);
			FFmpegLogCallback.set();
			grabber = new FFmpegFrameGrabber(input);
			grabber.start();
			// 异常的framerate,强制使用25帧
			if (grabber.getFrameRate() > 0 && grabber.getFrameRate() < 100) {
				framerate = grabber.getFrameRate();
			} else {
				framerate = 25.0;
			}
			bitrate = grabber.getVideoBitrate();// 获取到的比特率 0
			recorder = new FFmpegFrameRecorder(output, grabber.getImageWidth(), grabber.getImageHeight(), 0);
			// 设置比特率
			recorder.setVideoBitrate(bitrate);
			// h264编/解码器
			recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
			// 设置音频编码
			recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
			// 视频帧率(保证视频质量的情况下最低25,低于25会出现闪屏)
			recorder.setFrameRate(framerate);
			// 关键帧间隔,一般与帧率相同或者是视频帧率的两倍
			recorder.setGopSize((int) framerate);
			// 解码器格式
			recorder.setFormat("hls");
			// 单个切片时长,单位是s,默认为2s
			recorder.setOption("hls_time", "60");
			// HLS播放的列表长度,0标识不做限制
			recorder.setOption("hls_list_size", "0");
			// 设置切片的ts文件序号起始值,默认从0开始,可以通过此项更改
			recorder.setOption("start_number", "120");
//			recorder.setOption("hls_segment_type","mpegts");
			// 自动删除切片,如果切片数量大于hls_list_size的数量,则会开始自动删除之前的ts切片,只保 留hls_list_size个数量的切片
			// recorder.setOption("hls_flags","delete_segments");
			// ts切片自动删除阈值,默认值为1,表示早于hls_list_size+1的切片将被删除
			// recorder.setOption("hls_delete_threshold","1");
			/*
			 * hls的切片类型: 'mpegts':以MPEG-2传输流格式输出ts切片文件,可以与所有HLS版本兼容。 'fmp4':以Fragmented
			 * MP4(简称:fmp4)格式输出切片文件,类似于MPEG-DASH,fmp4文件可用于HLS version 7和更高版本。
			 */
//			recorder.setOption("hls_segment_type","mpegts");

			AVFormatContext fc = null;
			fc = grabber.getFormatContext();
			recorder.start(fc);
			// 清空探测时留下的缓存
//			grabber.flush();

			AVPacket pkt = null;
			for (int no_pkt_index = 0; no_pkt_index < 5 && err_index < 5;) {
				pkt = grabber.grabPacket();
				if (pkt == null || pkt.size() <= 0 || pkt.data() == null) {
					Thread.sleep(1);
					no_pkt_index++;
					continue;
				}
				// 获取到的pkt的dts,pts异常,将此包丢弃掉。
				if (pkt.dts() == avutil.AV_NOPTS_VALUE && pkt.pts() == avutil.AV_NOPTS_VALUE || pkt.pts() < dts) {
					err_index++;
					
					av_packet_unref(pkt);
					continue;
				}
				// 矫正dts,pts
				pkt.pts(pts);
				pkt.dts(dts);
				err_index += (recorder.recordPacket(pkt) ? 0 : 1);
				// pts,dts累加
				timebase = grabber.getFormatContext().streams(pkt.stream_index()).time_base().den();

				pts += (timebase / (int) framerate);
				dts += (timebase / (int) framerate);
			}
		} catch (Exception e) {
			grabber.stop();
			grabber.close();
			if (recorder != null) {
				recorder.stop();
				recorder.release();
			}
		} finally {
			grabber.stop();
			grabber.close();
			if (recorder != null) {
				recorder.stop();
				recorder.release();
			}
		}
	}
	
	public static void main(String[] args) throws IOException {
		String rtspURL = "rtsp://admin:risun8768@192.168.10.200:554/Streaming/Channels/601";
		String fileName = "D:/code/java/risun-smartsite/ruoyi-admin/camera_files_path/img_in/2/0.jpg";
		
		// CameraUtil.screenshot(rtspURL, fileName);
		CameraUtil.play(rtspURL);
		
		// CameraUtil cu = new CameraUtil();
		// cu.push(rtspURL,"D:/nginx 1.7.11.3 Gryphon_2/temp/m3u8/0.m3u8");
	}
}

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
作为一名AI语言模型,我无法进行实战操作。但我可以为您提供相关的思路和技术方案: 1. RTSPRTMP RTSP是实时流传输协议,而RTMP是实时消息传输协议。它们都是用于实时传输音视频流的协议,但是它们的传输方式不同。RTSP是基于TCP的协议,而RTMP是基于TCP或UDP的协议。 要将RTSP换为RTMP流,您需要使用一个换器。其中一个常用的换器是FFmpeg。FFmpeg是一个开源的多媒体框架,可以处理各种音视频格式,并且支持将RTSP换为RTMP流。 以下是使用FFmpeg将RTSP换为RTMP流的命令: ffmpeg -i rtsp://xxx.xxx.xxx.xxx:554/xxx -c copy -f flv rtmp://xxx.xxx.xxx.xxx/live/xxx 其中,rtsp://xxx.xxx.xxx.xxx:554/xxx是RTSP流的地址,rtmp://xxx.xxx.xxx.xxx/live/xxx是RTMP流的地址。 2. RTSPM3U8 M3U8是一种基于HTTP的流协议,它是一种多媒体播放列表,用于指定流媒体文件的播放顺序和位置。要将RTSP换为M3U8流,您需要使用一个换器,如FFmpeg。 以下是使用FFmpeg将RTSP换为M3U8流的命令: ffmpeg -i rtsp://xxx.xxx.xxx.xxx:554/xxx -c:v libx264 -c:a aac -hls_time 10 -hls_list_size 6 -hls_wrap 10 -start_number 1 /var/www/html/live/xxx.m3u8 其中,rtsp://xxx.xxx.xxx.xxx:554/xxx是RTSP流的地址,/var/www/html/live/xxx.m3u8M3U8流的地址。 3. 集成到Java应用程序中 要将上述换器集成到Java应用程序中,您可以使用Java的ProcessBuilder类来执行命令行命令。以下是一个示例代码: String command = "ffmpeg -i rtsp://xxx.xxx.xxx.xxx:554/xxx -c copy -f flv rtmp://xxx.xxx.xxx.xxx/live/xxx"; ProcessBuilder processBuilder = new ProcessBuilder(command.split(" ")); processBuilder.redirectErrorStream(true); Process process = processBuilder.start(); InputStream inputStream = process.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = bufferedReader.readLine()) != null) { System.out.println(line); } process.waitFor(); 要将RTSP换为M3U8,您可以使用类似的代码,只需更改命令即可。 以上是一个基本的思路和技术方案,具体实现还需要根据您的具体需求进行调整。希望能对您有所帮助。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

外码斯迪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值