海康sdk捕獲碼流數據通過JavaCV推成rtmp流的實現思路(PS流轉封裝RTMP)

海康sdk捕獲碼流數據通過JavaCV推成rtmp流的實現思路(PS流轉封裝RTMP)

碼雲(Gitee)主頁:https://gitee.com/banmajio
github主頁:https://github.com/banmajio
個人博客:banmajio’s blog

問題分析

通過海康sdk註冊回調函數,可以捕獲到視頻的碼流數據。但是因爲海康sdk回調的碼流數據是ps封裝的h264的碼流數據,也就是說通過海康sdk可以得到視頻的ps流

轉碼推rtmp

最開始的時候,找到一個demo,是將海康sdk回調函數中將碼流數據的byte[]—>BytePointer—>Mat,然後又通過opencv_imgproc.cvtColor()將yv12的Mat轉爲rgb的Mat,接着將rgb的Mat轉爲了Frame幀,最後通過FFmpegFrameRecorder.record(Frame)將幀推送到rtmp地址上。雖然這種方式可以實現我們的需求,但是帶來的問題是,cpu佔用率極高。大概推三路cpu就佔滿了。查看JavaCV源碼發現FFmpegFrameRecorder.record(Frame)方法會對幀進行編解碼的動作,然後將Frame轉換爲AVPacket。CPU高的原因也正是消耗在了編解碼的地方。

	this.bPointer = new BytePointer(frameBean.getBuffer().length);
	this.yv12Mat = new Mat(height + height / 2, width, CV_8UC1);
	this.rgbMat = new Mat(height, width, CV_8UC3);
	if (this.converter == null) {
		this.converter = new ToIplImage();
	}
	if (this.matConverter == null) {
		this.matConverter = new ToMat();
	}
	// 圖像轉碼-----↓
	// 填充指針
	this.bPointer.put(frameBean.getBuffer());
	// mat填充
	this.yv12Mat.data(this.bPointer);
	// 轉碼opencv實現方式
	opencv_imgproc.cvtColor(this.yv12Mat, this.rgbMat, opencv_imgproc.COLOR_YUV2BGR_YV12);
	// 轉換爲幀
	this.matFrame = this.matConverter.convert(this.rgbMat);
	// 圖像轉碼-----↑
	try {
		this.recorder.record(this.matFrame);
	} catch (Exception e) {
		e.printStackTrace();
	}

PS流轉封裝

後來在一個技術交流羣內瞭解到,Javacv是可以將PS流轉封裝爲flv格式推到rtmp的。具體的實現思路就是通過Java的管道流,將sdk回調函數中獲得的碼流數據寫入PipedOutputStream中,然後將對應的PipedInputStream當做參數傳入到FFmpegFrameGrabber的構造方法中。其餘的操作和拉rtsp流推rtmp流大體類似。可以參考JavaCV轉封裝rtsp到rtmp(無需轉碼,低資源消耗)
其中要注意的幾點就是:
1.管道流PipedInputStream,PipedOutputStream不可以在同一線程下使用否則會造成死鎖。
2.管道流是一種阻塞流,PipedOutputStream.write(byte[])會將數據放到PipedInputStream的緩衝區中,當PipedInputStream將這部分數據read()出去後,PipedOutputStream纔會繼續write。這個緩衝區的大小默認值時1024。也可以自己手動通過下面的這種方式指定緩衝區大小。

	PipedInputStream inputStream = new PipedInputStream(5120);

3.管道流PipedInputStream,PipedOutputStream成對出現,需要將兩者建立連接才能正常工作。建立連接有以下兩種方式:

	//第一種方式
	PipedInputStream inputStream = new PipedInputStream();
	PipedOutputStream outputStream = new PipedOutputStream(inputStream);

	//第二種方式
	PipedInputStream inputStream = new PipedInputStream();
	PipedOutputStream outputStream = new PipedOutputStream();
	inputStream.connect(outputStream);

4.推流方式和rtsp推流方式幾乎相同

			grabber = new FFmpegFrameGrabber(inputStream, 0);
			grabber.setOption("stimeout", "2000000");
			grabber.setVideoOption("vcodec", "copy");
			grabber.setFormat("mpeg");
			grabber.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
			grabber.setVideoCodec(avcodec.AV_CODEC_ID_H264);
//			grabber.setAudioStream(Integer.MAX_VALUE);
			grabber.setFrameRate(25);
			grabber.setImageWidth(1280);
			grabber.setImageHeight(720);
			logger.debug("grabber start");
			grabber.start();
			logger.debug("grabber end");

			this.recorder = new FFmpegFrameRecorder(rtmp, grabber.getImageWidth(), grabber.getImageHeight(), 0);
			this.recorder.setInterleaved(true);
			this.recorder.setVideoOptions(this.videoOption);
			// 設置比特率
			this.recorder.setVideoBitrate(bitrate);
			// h264編/解碼器
			this.recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
			// 封裝flv格式
			this.recorder.setFormat("flv");
			this.recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
			// 視頻幀率(保證視頻質量的情況下最低25,低於25會出現閃屏)
			this.recorder.setFrameRate(grabber.getFrameRate());
			// 關鍵幀間隔,一般與幀率相同或者是視頻幀率的兩倍
			this.recorder.setGopSize((int) grabber.getFrameRate() * 2);
			AVFormatContext fc = null;
			fc = grabber.getFormatContext();
			this.recorder.start(fc);
			logger.debug("hcsdk " + rtmp + "開始推流");
			// 清空探測時留下的緩存
//			grabber.flush();

			AVPacket pkt = null;
			long dts = 0;
			long pts = 0;

			for (int no_frame_index = 0; no_frame_index < 5 || err_index < 5;) {
				if (interrupt) {
					break;
				}
				pkt = grabber.grabPacket();
				if (pkt == null || pkt.size() <= 0 || pkt.data() == null) {
					// 空包記錄次數跳過
					no_frame_index++;
					err_index++;
					continue;
				}
				// 獲取到的pkt的dts,pts異常,將此包丟棄掉。
				if (pkt.dts() == avutil.AV_NOPTS_VALUE && pkt.pts() == avutil.AV_NOPTS_VALUE || pkt.pts() < dts) {
					logger.debug("異常pkt   當前pts: " + pkt.pts() + "  dts: " + pkt.dts() + "  上一包的pts: " + pts + " dts: "
							+ dts);
					err_index++;
					av_packet_unref(pkt);
					continue;
				}
				// 記錄上一pkt的dts,pts
				dts = pkt.dts();
				pts = pkt.pts();
				// 推數據包
				err_index += (recorder.recordPacket(pkt) ? 0 : 1);
				// 將緩存空間的引用計數-1,並將Packet中的其他字段設爲初始值。如果引用計數爲0,自動的釋放緩存空間。
				av_packet_unref(pkt);
			}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章