碼雲(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);
}