写点什么

Java 版流媒体编解码和图像处理 (JavaCPP+FFmpeg)

用户头像
极客good
关注
发布于: 刚刚
  • 接下来是整个程序最重要的方法 openMediaAndSaveImage,该方法是整个程序的主体,负责将打开流媒体、解码、转格式、保存、释放等五个步骤串起来,外部只要调用这个方法就能完成整个功能:


/**


  • 打开流媒体,取一帧,转为 YUVJ420P,再保存为 jpg 文件

  • @param url

  • @param out_file

  • @throws IOException


*/


public void openMediaAndSaveImage(String url,String out_file) throws IOException {


log.info("正在打开流媒体 [{}]", url);


// 打开指定流媒体,进行解封装,得到解封装上下文


AVFormatContext pFormatCtx = getFormatContext(url);


if (null==pFormatCtx) {


log.error("获取解封装上下文失败");


return;


}


// 控制台打印流媒体信息


av_dump_format(pFormatCtx, 0, url, 0);


// 流媒体解封装后有一个保存了所有流的数组,videoStreamIndex 表示视频流在数组中的位置


int videoStreamIndex = getVideoStreamIndex(pFormatCtx);


// 找不到视频流就直接返回


if (videoStreamIndex<0) {


log.error("没有找到视频流");


return;


}


log.info("视频流在流数组中的第[{}]个流是视频流(从 0 开始)", videoStreamIndex);


// 得到解码上下文,已经完成了初始化


AVCodecContext pCodecCtx = getCodecContext(pFormatCtx, videoStreamIndex);


if (null==pCodecCtx) {


log.error("生成解码上下文失败");


return;


}


// 从视频流中解码一帧


AVFrame pFrame = getSingleFrame(pCodecCtx,pFormatCtx, videoStreamIndex);


if (null==pFrame) {


log.error("从视频流中取帧失败");


return;


}


// 将 YUV420P 图像转成 YUVJ420P


// 转换后的图片的 AVFrame,及其对应的数据指针,都放在 frameData 对象中


FrameData frameData = YUV420PToYUVJ420P(pCodecCtx, pFrame);


if (null==frameData) {


log.info("YUV420P 格式转成 YUVJ420P 格式失败");


return;


}


// 持久化存储


saveImg(frameData.avFrame,out_file);


// 按顺序释放


release(true, null, null, pCodecCtx, pFormatCtx, frameData.buffer, frameData.avFrame, pFrame);


log.info("操作成功");


}


  • 现在整体逻辑已经清楚了,再来看里面 openMediaAndSaveImage 里面调用的那些方法的源码,先看打开流媒体的 getFormatContext:


/**


  • 生成解封装上下文

  • @param url

  • @return


*/


private AVFormatContext getFormatContext(String url) {


// 解封装上下文


AVFormatContext pFormatCtx = new avformat.AVFormatContext(null);


// 打开流媒体


if (avformat_open_input(pFormatCtx, url, null, null) != 0) {


log.error("打开媒体失败");


return null;


}


// 读取流媒体数据,以获得流的信息


if (avformat_find_stream_info(pFormatCtx, (PointerPointer<Pointer>) null) < 0) {


log.error("获得媒体流信息失败");


return null;


}


return pFormatCtx;


}


  • 流媒体解封装后有一个保存了所有流的数组,getVideoStreamIndex 方法会找到视频流在数组中的位置:


/**


  • 流媒体解封装后得到多个流组成的数组,该方法找到视频流咋数组中的位置

  • @param pFormatCtx

  • @return


*/


private static int getVideoStreamIndex(AVFormatContext pFormatCtx) {


int videoStream = -1;


// 解封装后有多个流,找出视频流是第几个


for (int i = 0; i < pFormatCtx.nb_streams(); i++) {


if (pFormatCtx.streams(i).codec().codec_type() == AVMEDIA_TYPE_VIDEO) {


videoStream = i;


break;


}


}


return videoStream;


}


  • 解封装之后就是解码,getCodecContext 方法得到解码上下文对象:


/**


  • 生成解码上下文

  • @param pFormatCtx

  • @param videoStreamIndex

  • @return


*/


private AVCodecContext getCodecContext(AVFormatContext pFormatCtx, int videoStreamIndex) {


//解码器


AVCodec pCodec;


// 得到解码上下文


AVCodecContext pCodecCtx = pFormatCtx.streams(videoStreamIndex).codec();


// 根据解码上下文得到解码器


pCodec = avcodec_find_decoder(pCodecCtx.codec_id());


if (pCodec == null) {


return null;


}


// 用解码器来初始化解码上下文


if (avcodec_open2(pCodecCtx, pCodec, (AVDictionary)null) < 0) {


return null;


}


return pCodecCtx;


}


  • 紧接着从视频流解码取帧解码:


/**


  • 取一帧然后解码

  • @param pCodecCtx

  • @param pFormatCtx

  • @param videoStreamIndex

  • @return


*/


private AVFrame getSingleFrame(AVCodecContext pCodecCtx, AVFormatContext pFormatCtx, int videoStreamIndex) {


// 分配帧对象


AVFrame pFrame = av_frame_alloc();


// frameFinished 用于检查是否有图像


int[] frameFinished = new int[1];


// 是否找到的标志


boolean exists = false;


AVPacket packet = new AVPacket();


try {


// 每一次 while 循环都会读取一个 packet


while (av_read_frame(pFormatCtx, packet) >= 0) {


// 检查 packet 所属的流是不是视频流


if (packet.stream_index() == videoStreamIndex) {


// 将 AVPacket 解码成 AVFrame


avcodec_decode_video2(pCodecCtx, pFrame, frameFinished, packet);// Decode video frame


// 如果有图像就返回


if (frameFinished != null && frameFinished[0] != 0 && !pFrame.isNull()) {


exists = true;


break;


}


}


}


} finally {


// 一定要执行释放操作


av_free_packet(packet);


}


// 找不到就返回空


return exists ? pFrame : null;


}


  • 解码后的图像是 YUV420P 格式,咱们将其转成 YUVJ420P:


/**


  • 将 YUV420P 格式的图像转为 YUVJ420P 格式

  • @param pCodecCtx 解码上下文

  • @param sourceFrame 源数据

  • @return 转换后的帧极其对应的数据指针


*/


private static FrameData YUV420PToYUVJ420P(AVCodecContext pCodecCtx, AVFrame sourceFrame) {


// 分配一个帧对象,保存从 YUV420P 转为 YUVJ420P 的结果


AVFrame pFrameRGB = av_frame_alloc();


if (pFrameRGB == null) {


return null;


}


int width = pCodecCtx.width(), height = pCodecCtx.height();


// 一些参数设定


pFrameRGB.width(width);


pFrameRGB.height(height);


pFrameRGB.format(AV_PIX_FMT_YUVJ420P);


// 计算转为 YUVJ420P 之后的图片字节数


int numBytes = avpicture_get_size(AV_PIX_FMT_YUVJ420P, width, height);


// 分配内存


BytePointer buffer = new BytePointer(av_malloc(numBytes));


// 图片处理工具的初始化操作


SwsContext sws_ctx = sws_getContext(width, height, pCodecCtx.pix_fmt(), width, height, AV_PIX_FMT_YUVJ420P, SWS_BICUBIC, null, null, (DoublePointer) null);


// 将 pFrameRGB 的 data 指针指向刚才分配好的内存(即 buffer)


avpicture_fill(new avcodec.AVPicture(pFrameRGB), buffer, AV_PIX_FMT_YUVJ420P, width, height);


// 转换图像格式,将解压出来的 YUV420P 的图像转换为 YUVJ420P 的图像


sws_scale(sws_ctx, sourceFrame.data(), sourceFrame.linesize(), 0, height, pFrameRGB.data(), pFrameRGB.linesize());


// 及时释放


sws_freeContext(sws_ctx);


// 将 AVFrame 和 BytePointer 打包到 FrameData 中返回,这两个对象都要做显示的释放操作


return new FrameData(pFrameRGB, buffer);


}


  • 然后就是另一个很重要方法 saveImg,里面是典型的编码和输出流程,咱们前面已经了解了打开媒体流解封装解码的操作,现在要看看怎么制作媒体流,包括编码、封装和输出:


/**


  • 将传入的帧以图片的形式保存在指定位置

  • @param pFrame

  • @param out_file

  • @return 小于 0 表示失败


*/


private int saveImg(avutil.AVFrame pFrame, String out_file) {


av_log_set_level(AV_LOG_ERROR);//设置 FFmpeg 日志级别(默认是 debug,设置成 error 可以屏蔽大多数不必要的控制台消息)


AVPacket pkt = null;


AVStream pAVStream = null;


int width = pFrame.width(), height = pFrame.height();


// 分配 AVFormatContext 对象


avformat.AVFormatContext pFormatCtx = avformat_alloc_context();


// 设置输出格式(涉及到封装和容器)


pFormatCtx.oformat(av_guess_format("mjpeg", null, null));


if (pFormatCtx.oformat() == null) {


log.error("输出媒体流的封装格式设置失败");


return -1;


}


try {


// 创建并初始化一个和该 url 相关的 AVIOContext


avformat.AVIOContext pb = new avformat.AVIOContext();


// 打开输出文件


if (avio_open(pb, out_file, AVIO_FLAG_READ_WRITE) < 0) {


log.info("输出文件打开失败");


return -1;


}


// 封装之上是协议,这里将封装上下文和协议上下文关联


pFormatCtx.pb(pb);


// 构建一个新 stream


pAVStream = avformat_new_stream(pFormatCtx, null);


if (pAVStream == null) {


log.error("将新的流放入媒体文件失败");


return -1;


}


int codec_id = pFormatCtx.oformat().video_codec();


// 设置该 stream 的信息


avcodec.AVCodecContext pCodecCtx = pAVStream.codec();


pCodecCtx.codec_id(codec_id);


pCodecCtx.codec_type(AVMEDIA_TYPE_VIDEO);


pCodecCtx.pix_fmt(AV_PIX_FMT_YUVJ420P);


pCodecCtx.width(width);


pCodecCtx.height(height);


pCodecCtx.time_base().num(1);


pCodecCtx.time_base().den(25);


// 打印媒体信息


av_dump_format(pFormatCtx, 0, out_file, 1);


// 查找解码器


avcodec.AVCodec pCodec = avcodec_find_encoder(codec_id);


if (pCodec == null) {


log.info("获取解码器失败");


return -1;


}


// 用解码器来初始化解码上下文


if (avcodec_open2(pCodecCtx, pCodec, (PointerPointer<Pointer>) null) < 0) {


log.error("解码上下文初始化失败");


return -1;


}


// 输出的 Packet


pkt = new avcodec.AVPacket();


// 分配


if (av_new_packet(pkt, width * height * 3) < 0) {


return -1;


}


int[] got_picture = { 0 };


// 把流的头信息写到要输出的媒体文件中


avformat_write_header(pFormatCtx, (PointerPointer<Pointer>) null);


// 把帧的内容进行编码


if (avcodec_encode_video2(pCodecCtx, pkt, pFrame, got_picture)<0) {


log.error("把帧编码为 packet 失败");


return -1;


}


/


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


/ 输出一帧


if ((av_write_frame(pFormatCtx, pkt)) < 0) {


log.error("输出一帧失败");


return -1;


}


// 写文件尾


if (av_write_trailer(pFormatCtx) < 0) {


log.error("写文件尾失败");

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
Java版流媒体编解码和图像处理(JavaCPP+FFmpeg)