javacpp-FFmpeg系列之2:通用拉流解碼器,支援影片拉流解碼並轉換為YUV、BGR24或RGB24等影像像素數據
- 2019 年 11 月 1 日
- 筆記
版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/eguid_1/article/details/82735524
javacpp-ffmpeg系列:
javacpp-FFmpeg系列之1:影片拉流解碼成YUVJ420P,並保存為jpg圖片 javacpp-FFmpeg系列之2:通用拉流解碼器,支援影片拉流解碼並轉換為YUV、BGR24或RGB24等影像像素數據 javacpp-FFmpeg系列之3: 影像數據轉換(BGR與BufferdImage互轉,RGB與BufferdImage互轉) 補充: javacpp-FFmpeg系列補充:FFmpeg解決avformat_find_stream_info檢索時間過長問題
前言:
第一篇中影片解碼成YUVJ420P影像像素數據(以下簡稱YUV或YUV數據),只是YUV在流媒體協議中用的較多(數據少,節省流量頻寬),在影像處理應用較多的是BGR和RGB像素數據。我們已經獲取到了YUV數據,那麼把YUV轉成BGR或者RGB需要再進行一次轉換,顯然性能上的表現並不是很好,所以本篇會通過編寫一個通用轉換器來介紹如何使用ffmpeg解碼轉出BGR、RGB、YUV等像素數據。
補充:
(1)為什麼暫時沒有用python實現,主要是先熟悉ffmpeg的API,後面會出的
(2)為什麼要轉成BGR、RGB像素數據,因為有了這倆其中一個就可以直接生成java的BufferImage了啊,最重要的是我們本意不是要轉成BufferImage,而是直接編碼成base64的影像數據啊
(3)在線演示demo:https://blog.csdn.net/eguid_1/article/details/82842904
項目維護地址:https://github.com/eguid/easyCV
一、功能設計
第一篇寫的很簡略(實際上是那一大坨程式碼,自己實在看不下去了qaq),直接參考ffmpeg原生C的API,不符合java語言編寫習慣,所以本篇會對上篇程式碼進行一些簡單的封裝復用。
功能上,會支援多種格式的像素數據(BGR、RGB、YUV等等);程式碼上,會對各個流程進行闡述分析。
二、功能實現
(1)初始化
載入ffmpeg的網路庫和編解碼庫,不初始化就沒法用,適合放在靜態塊中進行載入
static { // Register all formats and codecs av_register_all(); avformat_network_init(); }
(2)打開影片流
初始化AVFormatContext,主要就是根據url創建InputStream,並且會根據不同協議(rtmp/rtsp/hls/文件等)嘗試讀取一些文件頭數據(流媒體頭數據)。
補充:FileNotOpenException是繼承自RuntimeException的自定義異常類,只是加個名字方便標識異常而已,下面還會有幾個異常,都是繼承自RuntimeException的自定義異常類,以下不會再提
/** * 打開影片流 * @param url -url * @return * @throws FileNotOpenException */ protected AVFormatContext openInput(String url) throws FileNotOpenException{ AVFormatContext pFormatCtx = new AVFormatContext(null); if(avformat_open_input(pFormatCtx, url, null, null)==0) { return pFormatCtx; } throw new FileNotOpenException("Didn't open video file"); }
(3)檢索流資訊
上一步獲得了AVFormatContext,這一步繼續根據AVFormatContext讀取一部分視音頻數據並且獲得一些相關的資訊
/** * 檢索流資訊 * @param pFormatCtx * @return */ protected AVFormatContext findStreamInfo(AVFormatContext pFormatCtx) throws StreamInfoNotFoundException{ if (avformat_find_stream_info(pFormatCtx, (PointerPointer<?>) null)>= 0) { return pFormatCtx; } throw new StreamInfoNotFoundException("Didn't retrieve stream information"); }
(3)獲取影片幀
上面兩步確定了媒體文件/流的上下文,這一步嘗試讀取一幀影片幀。
分成兩個方法,先獲取影片幀位置,然後根據位置獲取影片幀,當然也可以合成一個方法使用。
/** * 獲取第一幀影片位置 * @param pFormatCtx * @return */ protected int findVideoStreamIndex(AVFormatContext pFormatCtx) { int i = 0, videoStream = -1; for (i = 0; i < pFormatCtx.nb_streams(); i++) { AVStream stream=pFormatCtx.streams(i); AVCodecContext codec=stream.codec(); if (codec.codec_type() == AVMEDIA_TYPE_VIDEO) { videoStream = i; break; } } return videoStream; } /** * 指定影片幀位置獲取對應影片幀 * @param pFormatCtx * @param videoStream * @return */ protected AVCodecContext findVideoStream(AVFormatContext pFormatCtx ,int videoStream)throws StreamNotFoundException { if(videoStream >=0) { // Get a pointer to the codec context for the video stream AVStream stream=pFormatCtx.streams(videoStream); AVCodecContext pCodecCtx = stream.codec(); return pCodecCtx; } throw new StreamNotFoundException("Didn't open video file"); }
(4)查找編解碼器
其實底層調用的還是find_encdec(),遍歷AVCodec鏈表查找有沒有對應的編解碼器,如果沒有找到直接拋出異常,如果已經確定編解碼,也可以指定codec_id
/** * 查找並嘗試打開解碼器 * @return */ protected AVCodecContext findAndOpenCodec(AVCodecContext pCodecCtx) { // Find the decoder for the video stream AVCodec pCodec = avcodec_find_decoder(pCodecCtx.codec_id()); if (pCodec == null) { System.err.println("Codec not found"); throw new CodecNotFoundExpception("Codec not found"); } AVDictionary optionsDict = null; // Open codec if (avcodec_open2(pCodecCtx, pCodec, optionsDict) < 0) { System.err.println("Could not open codec"); throw new CodecNotFoundExpception("Could not open codec"); // Could not open codec } return pCodecCtx; }
(5.1)循環讀取影片幀並解碼成yuv
這個沒什麼好講的了,前面的準備任務做完,就是一直循環讀取影片幀,最後解碼出來的影像幀都是yuv像素數據,這個顯然不是我們想要的,所以要對這裡進行改動
// Allocate video frame AVFrame pFrame = av_frame_alloc(); AVPacket packet = new AVPacket(); // Read frames and save first five frames to disk while (av_read_frame(pFormatCtx, packet) >= 0) { // Is this a packet from the video stream? if (packet.stream_index() == videoStream) { // Decode video frame avcodec_decode_video2(pCodecCtx, pFrame, frameFinished, packet); // Did we get a video frame? if (frameFinished != null&&frameFinished[0] != 0) { //ffmpeg默認解碼出來的是yuv數據 System.err.println(pFrame.data()); } } // Free the packet that was allocated by av_read_frame av_free_packet(packet); }
(5.2)循環讀取影片幀並轉換成RGB或BGR影像像素數據
// Allocate video frame AVFrame pFrame = av_frame_alloc(); //Allocate an AVFrame structure AVFrame pFrameRGB = av_frame_alloc(); width = pCodecCtx.width(); height = pCodecCtx.height(); pFrameRGB.width(width); pFrameRGB.height(height); pFrameRGB.format(fmt); // Determine required buffer size and allocate buffer int numBytes = avpicture_get_size(fmt, width, height); SwsContext sws_ctx = sws_getContext(width, height, pCodecCtx.pix_fmt(), width, height,fmt, SWS_BILINEAR, null, null, (DoublePointer) null); BytePointer buffer = new BytePointer(av_malloc(numBytes)); // Assign appropriate parts of buffer to image planes in pFrameRGB // Note that pFrameRGB is an AVFrame, but AVFrame is a superset // of AVPicture avpicture_fill(new AVPicture(pFrameRGB), buffer, fmt, width, height); AVPacket packet = new AVPacket(); int[] frameFinished = new int[1]; // Read frames and save first five frames to disk while (av_read_frame(pFormatCtx, packet) >= 0) { // Is this a packet from the video stream? if (packet.stream_index() == videoStream) { // Decode video frame avcodec_decode_video2(pCodecCtx, pFrame, frameFinished, packet); // Did we get a video frame? if (frameFinished != null&&frameFinished[0] != 0) { // Convert the image from its native format to BGR sws_scale(sws_ctx, pFrame.data(), pFrame.linesize(), 0, height, pFrameRGB.data(),pFrameRGB.linesize()); //Convert BGR to ByteBuffer //保存RGB或BGR數據 return saveFrame(pFrameRGB, width, height); } } // Free the packet that was allocated by av_read_frame av_free_packet(packet); }
三、完整程式碼
package cc.eguid.cv.corelib.videoimageshot.grabber; import static org.bytedeco.javacpp.avcodec.*; import static org.bytedeco.javacpp.avformat.*; import static org.bytedeco.javacpp.avutil.*; import static org.bytedeco.javacpp.swscale.*; import java.io.IOException; import java.nio.ByteBuffer; import org.bytedeco.javacpp.BytePointer; import org.bytedeco.javacpp.DoublePointer; import org.bytedeco.javacpp.PointerPointer; import cc.eguid.cv.corelib.videoimageshot.exception.CodecNotFoundExpception; import cc.eguid.cv.corelib.videoimageshot.exception.FileNotOpenException; import cc.eguid.cv.corelib.videoimageshot.exception.StreamInfoNotFoundException; import cc.eguid.cv.corelib.videoimageshot.exception.StreamNotFoundException; public abstract class GrabberTmplate { static { // Register all formats and codecs av_register_all(); avformat_network_init(); } //保留,暫不用 protected int width;//寬度 protected int height;//高度 /** * 打開影片流 * @param url -url * @return * @throws FileNotOpenException */ protected AVFormatContext openInput(String url) throws FileNotOpenException{ AVFormatContext pFormatCtx = new AVFormatContext(null); if(avformat_open_input(pFormatCtx, url, null, null)==0) { return pFormatCtx; } throw new FileNotOpenException("Didn't open video file"); } /** * 檢索流資訊 * @param pFormatCtx * @return */ protected AVFormatContext findStreamInfo(AVFormatContext pFormatCtx) throws StreamInfoNotFoundException{ if (avformat_find_stream_info(pFormatCtx, (PointerPointer<?>) null)>= 0) { return pFormatCtx; } throw new StreamInfoNotFoundException("Didn't retrieve stream information"); } /** * 獲取第一幀影片位置 * @param pFormatCtx * @return */ protected int findVideoStreamIndex(AVFormatContext pFormatCtx) { int i = 0, videoStream = -1; for (i = 0; i < pFormatCtx.nb_streams(); i++) { AVStream stream=pFormatCtx.streams(i); AVCodecContext codec=stream.codec(); if (codec.codec_type() == AVMEDIA_TYPE_VIDEO) { videoStream = i; break; } } return videoStream; } /** * 指定影片幀位置獲取對應影片幀 * @param pFormatCtx * @param videoStream * @return */ protected AVCodecContext findVideoStream(AVFormatContext pFormatCtx ,int videoStream)throws StreamNotFoundException { if(videoStream >=0) { // Get a pointer to the codec context for the video stream AVStream stream=pFormatCtx.streams(videoStream); AVCodecContext pCodecCtx = stream.codec(); return pCodecCtx; } throw new StreamNotFoundException("Didn't open video file"); } /** * 查找並嘗試打開解碼器 * @return */ protected AVCodecContext findAndOpenCodec(AVCodecContext pCodecCtx) { // Find the decoder for the video stream AVCodec pCodec = avcodec_find_decoder(pCodecCtx.codec_id()); if (pCodec == null) { System.err.println("Codec not found"); throw new CodecNotFoundExpception("Codec not found"); } AVDictionary optionsDict = null; // Open codec if (avcodec_open2(pCodecCtx, pCodec, optionsDict) < 0) { System.err.println("Could not open codec"); throw new CodecNotFoundExpception("Could not open codec"); // Could not open codec } return pCodecCtx; } /** * 抓取影片幀(默認跳過音頻幀和空幀) * @param url -影片源(rtsp/rtmp/hls/文件等等) * @param fmt - 像素格式,比如AV_PIX_FMT_BGR24 * @return * @throws IOException */ public ByteBuffer grabVideoFrame(String url,int fmt) throws IOException { // Open video file AVFormatContext pFormatCtx=openInput(url); // Retrieve stream information pFormatCtx=findStreamInfo(pFormatCtx); // Dump information about file onto standard error //av_dump_format(pFormatCtx, 0, url, 0); //Find a video stream int videoStream=findVideoStreamIndex(pFormatCtx); AVCodecContext pCodecCtx =findVideoStream(pFormatCtx,videoStream); // Find the decoder for the video stream pCodecCtx= findAndOpenCodec(pCodecCtx); // Allocate video frame AVFrame pFrame = av_frame_alloc(); //Allocate an AVFrame structure AVFrame pFrameRGB = av_frame_alloc(); width = pCodecCtx.width(); height = pCodecCtx.height(); pFrameRGB.width(width); pFrameRGB.height(height); pFrameRGB.format(fmt); // Determine required buffer size and allocate buffer int numBytes = avpicture_get_size(fmt, width, height); SwsContext sws_ctx = sws_getContext(width, height, pCodecCtx.pix_fmt(), width, height,fmt, SWS_BILINEAR, null, null, (DoublePointer) null); BytePointer buffer = new BytePointer(av_malloc(numBytes)); // Assign appropriate parts of buffer to image planes in pFrameRGB // Note that pFrameRGB is an AVFrame, but AVFrame is a superset // of AVPicture avpicture_fill(new AVPicture(pFrameRGB), buffer, fmt, width, height); AVPacket packet = new AVPacket(); int[] frameFinished = new int[1]; try { // Read frames and save first five frames to disk while (av_read_frame(pFormatCtx, packet) >= 0) { // Is this a packet from the video stream? if (packet.stream_index() == videoStream) { // Decode video frame avcodec_decode_video2(pCodecCtx, pFrame, frameFinished, packet); // Did we get a video frame? if (frameFinished != null&&frameFinished[0] != 0) { // Convert the image from its native format to BGR sws_scale(sws_ctx, pFrame.data(), pFrame.linesize(), 0, height, pFrameRGB.data(),pFrameRGB.linesize()); //Convert BGR to ByteBuffer return saveFrame(pFrameRGB, width, height); } } // Free the packet that was allocated by av_read_frame av_free_packet(packet); } return null; }finally { //Don't free buffer // av_free(buffer); av_free(pFrameRGB);// Free the RGB image av_free(pFrame);// Free the YUV frame sws_freeContext(sws_ctx);//Free SwsContext avcodec_close(pCodecCtx);// Close the codec avformat_close_input(pFormatCtx);// Close the video file } } /** * BGR影像幀轉位元組緩衝區(BGR結構) * * @param pFrame * -bgr影像幀 * @param width * -寬度 * @param height * -高度 * @return * @throws IOException */ abstract ByteBuffer saveFrame(AVFrame pFrameRGB, int width, int height); }
四、小結
本章主要詳解ffmpeg拉流解碼的各個流程,可以通過本章程式碼可以輕鬆實現不限於RGB/BGR/YUV的FFmpeg所支援的多種像素格式轉換
支援eguid原創