1. 程式人生 > >javacpp-FFmpeg系列之2:通用拉流解碼器,支持視頻拉流解碼並轉換為YUV、BGR24或RGB24等圖像像素數據

javacpp-FFmpeg系列之2:通用拉流解碼器,支持視頻拉流解碼並轉換為YUV、BGR24或RGB24等圖像像素數據

tope sca 封裝 ams 定義 throw tco 如何使用 都是

javacpp-ffmpeg系列:

javacpp-FFmpeg系列之1:視頻拉流解碼成YUVJ420P,並保存為jpg圖片

javacpp-FFmpeg系列之2:通用拉流解碼器,支持視頻拉流解碼並轉換為YUV、BGR24或RGB24等圖像像素數據

javacpp-FFmpeg系列之3: 圖像數據轉換(BGR與BufferdImage互轉,RGB與BufferdImage互轉)

前言:

第一篇中視頻解碼成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見下一章

一、功能設計

第一篇寫的很簡略(實際上是那一大坨代碼,自己實在看不下去了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所支持的多種像素格式轉換

javacpp-FFmpeg系列之2:通用拉流解碼器,支持視頻拉流解碼並轉換為YUV、BGR24或RGB24等圖像像素數據