1.利用FFMpeg进行MP3视频转YUV格式,2.ffmpeg解码MP3后用surfaceview播放
1 利用FFMpeg进行MP3视频转YUV格式
理论:
YUV,是一种颜色编码方法
详细看这里
https://blog.csdn.net/junzia/article/details/76315120
为什么需要转yuv格式
现在绝大多数视频解码后播放的格式都是YUV 所以需要做下YUV格式
一个通道.
前面放Y 后面放UV 比例是 4:1:1
视频yuv 音频 是pcm
YUV
- “Y”表示明亮度(Luminance、Luma),“U”和“V”则是色度、浓度(Chrominance、Chroma)。
封装格式: MP4 rmvb等
编码格式: 对应响应的编码 MPEG2,MPEG4,H.264
视频播放一般有两个 流 音频流 视频流, 有时还有个一流 是字幕流
资料
https://blog.csdn.net/leixiaohua1020/article/details/11693997
a) 解协议(http,rtsp,rtmp,mms)
AVIOContext,URLProtocol,URLContext主要存储视音频使用的协议的类型以及状态。URLProtocol存储输入视音频使用的封装格式。每种协议都对应一个URLProtocol结构。(注意:FFMPEG中文件也被当做一种协议“file”)
b) 解封装(flv,avi,rmvb,mp4)
AVFormatContext主要存储视音频封装格式中包含的信息;AVInputFormat存储输入视音频使用的封装格式。每种视音频封装格式都对应一个AVInputFormat 结构。
c) 解码(h264,mpeg2,aac,mp3)
每个AVStream存储一个视频/音频流的相关数据;每个AVStream对应一个AVCodecContext,存储该视频/音频流使用解码方式的相关数据;每个AVCodecContext中对应一个AVCodec,包含该视频/音频对应的解码器。每种解码器都对应一个AVCodec结构。
d) 存数据
视频的话,每个结构一般是存一帧;音频可能有好几帧
解码前数据:AVPacket
解码后数据:AVFrame
我这里用到了 三个Context
- AVFormatContext 解封装上下文 存储视音频封装格式中包含的信息
- AVCodecContext 解码器上下文 存储该视频/音频流使用解码方式的相关数据
- SwsContext 转换器的上下文 存储该视频/音频流转换的相关数据(宽高 视频质量目标格式等)
转化的流程 整个图很不错
来自https://blog.csdn.net/boonya/article/details/79474754
下面开始预先操作
- 把ffmpeg 的so库 还有头文件导入到android studio 工程中去.
- 在native-lib中导入头文件.
extern "C" {
//编码
#include "libavcodec/avcodec.h"
//封装格式处理
#include "libavformat/avformat.h"
//像素处理
#include "libswscale/swscale.h"
}
- 在手机根目录放个mp4视频
视频信息
可以看到:
- 封装格式mp4
- 编码格式avc1格式
大致流程图:
代码
注释都有
#include <jni.h>
#include <string>
#include <jni.h>
#include <string>
#include <android/log.h>
//extern "C" 主要作用就是为了能够正确实现C++代码调用其他C语言代码 加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
extern "C" {
//编码
#include "libavcodec/avcodec.h"
//封装格式处理
#include "libavformat/avformat.h"
//像素处理
#include "libswscale/swscale.h"
}
#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"jnilib",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"jnilib",FORMAT,##__VA_ARGS__);
JNICALL extern "C"
JNIEXPORT void JNICALL
Java_androidrn_ffmpegdemo_MainActivity_openVideo(JNIEnv *env, jobject instance, jstring inputStr_,
jstring outStr_) {
const char *inputStr = env->GetStringUTFChars(inputStr_, 0);
const char *outStr = env->GetStringUTFChars(outStr_, 0);
//无论编码还是解码 都要调用这个 注册各大组件
av_register_all();
//获取AVFormatContext 比特率 时长 文件路径 流的信息(nustream) 都封装在这里面
AVFormatContext *pContext = avformat_alloc_context();
//AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options
//上下文 文件名 打开文件格式 获取信息(AVDictionary) 凡是AVDictionary字典 都是获取视频文件信息
if (avformat_open_input(&pContext, inputStr, NULL, NULL) < 0) {
LOGE("打开失败");
return;
}
//给nb_streams填充信息 avformat_find_stream_info 函数可以读取一部分视音频数据并且获得一些相关的信息
if (avformat_find_stream_info(pContext, NULL) < 0) {
LOGE("获取信息失败");
return;
}
//找到视频流
int video_stream_ids = -1;
for (int i = 0; i < pContext->nb_streams; ++i) {
LOGE("循环 %d", i);
//如果填充的视频流信息 -> 编解码,解码器 -> 流的类型 == 视频的类型
//codec 每一个流 对应的解码上下文 codec_type 流的类型
if (pContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_ids = i;
}
}
// 获取到解码器上下文
AVCodecContext *pCodecCtx = pContext->streams[video_stream_ids]->codec;
//解码器
AVCodec *pCodex = avcodec_find_decoder(pCodecCtx->codec_id);
//打开解码器 为什么avcodec_open2 版本升级的原因
if (avcodec_open2(pCodecCtx, pCodex, NULL) < 0) {
LOGE("解码失败");
return;
}
//得到解封装 读取 解封每一帧 读取每一帧的压缩数据
//初始化avpacket 分配内存 FFMpeg 没有自动分配内存 必须手动分匹配手动释放 不过有分配函数
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
//初始化packet 每一个包是一个完整的数据帧,来暂存解复用之后、解码之前的媒体数据(一个音/视频帧、一个字幕包等)及附加信息(解码时间戳、显示时间戳、时长等)
av_init_packet(packet);
//初始化AVFrame
AVFrame *frame = av_frame_alloc();
//目的数据 YUV的frame 声明yuvFrame
AVFrame *yuvFrame = av_frame_alloc();
//给yuv 缓冲区初始化
//avpicture_get_size 计算给定宽度和高度的图片的字节大小 如果以给定的图片格式存储。
// uint8_t 8位无符号整型数(int)
uint8_t *out_buffer = (uint8_t *) av_malloc(
avpicture_get_size((AV_PIX_FMT_YUV420P), pCodecCtx->width, pCodecCtx->height));
//填充YUM缓冲区
int re = avpicture_fill(reinterpret_cast<AVPicture *>(yuvFrame), out_buffer, AV_PIX_FMT_YUV420P,
pCodecCtx->width, pCodecCtx->height);
LOGE("宽 %d 高 %d",pCodecCtx->width,pCodecCtx->height);
//获取转化的上下文 参数 通过解码器的上下文获取到 pCodecCtx->pix_fmt 是mp4的上下文
SwsContext *swsContext = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL);
int frame_count = 0;
//目的转化成YUV 写到一个文件里面去 声明文件
FILE *fp_yuv = fopen(outStr, "wb");
//AVFormatContext *s, AVPacket *pkt 上下文,avpacket 是数据包的意思
//packet 入参
int got_frame;
//解码每一帧图片
while (av_read_frame(pContext, packet) >= 0) {
// 解封装
//参数
//AVCodecContext *avctx, 解码器的上下文
// AVFrame *picture, 已经解封装的frame,是直接能在手机上播放的frame
// int *got_picture_ptr, 入参出参对象, 可以根据出参判断是否解析完成
// const AVPacket *avpkt 压缩的packet数据
//把packet压缩的数据 解压 赋给frame
avcodec_decode_video2(pCodecCtx, frame, &got_frame, packet);
//判断是否读完
if (got_frame > 0) {
LOGE("解码第 %d 个 frame ",frame_count++);
// 获取到了frame数据 视频像素的数据
// 目的转化为yuv格式 sws_scale 函数
sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
frame->linesize, 0, frame->height, yuvFrame->data, yuvFrame->linesize);
//得到所有的大小 宽度乘以高度 解释: 在R G B 中有多少像素 就是宽度乘以高度, 在YUV中 有多少像素是由Y决定的 如果只有Y 那么只有亮度 就是黑白的
int y_size = pCodecCtx->width * pCodecCtx->height;
//Y亮度信息
fwrite(yuvFrame->data[0], 1, y_size, fp_yuv);
//色度
fwrite(yuvFrame->data[1], 1, y_size / 4, fp_yuv);
//浓度
fwrite(yuvFrame->data[2], 1, y_size / 4, fp_yuv);
}
//释放
av_free_packet(packet);
}
fclose(fp_yuv);
av_frame_free(&yuvFrame);
av_frame_free(&frame);
avcodec_close(pCodecCtx);
avformat_free_context(pContext);
env->ReleaseStringUTFChars(inputStr_, inputStr);
env->ReleaseStringUTFChars(outStr_, outStr);
}
结果
- sws_getContext 函数解析
// 初始化sws_scale
struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags,
SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param);
参数int srcW, int srcH, enum AVPixelFormat srcFormat定义输入图像信息(寬、高、颜色空间(像素格式))
参数int dstW, int dstH, enum AVPixelFormat dstFormat定义输出图像信息寬、高、颜色空间(像素格式))。
参数int flags选择缩放算法(只有当输入输出图像大小不同时有效)
参数SwsFilter *srcFilter, SwsFilter *dstFilter分别定义输入/输出图像滤波器信息,如果不做前后图像滤波,输入NULL
参数const double *param定义特定缩放算法需要的参数(?),默认为NULL
函数返回SwsContext结构体,定义了基本变换信息。
如果是对一个序列的所有帧做相同的处理,函数sws_getContext只需要调用一次就可以了。
sws_getContext(w, h, YV12, w, h, NV12, 0, NULL, NULL, NULL); // YV12->NV12 色彩空间转换
sws_getContext(w, h, YV12, w/2, h/2, YV12, 0, NULL, NULL, NULL); // YV12图像缩小到原图1/4
sws_getContext(w, h, YV12, 2w, 2h, YN12, 0, NULL, NULL, NULL); // YV12图像放大到原图4倍,并转换为NV12结构
sws_scale
缩放SRCPLACE中的图像切片并将其缩放
需要注意的是第四个参数srcSliceY 这个代表的是第一列要处理的位置,如果要从头开始处理,直接填0即可
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY,int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
*
-C以前创建的缩放上下文
* SWSYGETCONTRONTHECT()
*@ PARAM SRCPLACE包含数组指向平面的指针
*源切片
*@ PARAM SrcReST包含每个平面的步幅的数组
*源图像
*@ PARAM SRCLSICY在切片的源图像中的位置
*进程,即数字(从开始计数)
*0)在切片的第一行的图像中
*@ PARAM SRCLSICH源数组的高度,即
*切片中的行
*@ PARAM DST包含指向平面的指针的数组
*目的地形象
*@ PARAM-DSSTRIDE包含每个平面的步长的数组
*目的地形象
*@返回输出条的高度
利用surfaceview播放
理论
解码部分和上面的代码基本上差不多.解码出来会得到了rgbframe (和yuvframe一样 不过格式不是YUV格式,因为surfaceview不能识别YUV,只能识别RGB.).
主要问题是解码后如何给surfaceview播放. ? c++ 里面是利用 ANativeWindow 播放. ANativeWindow 播放需要一个缓冲区buffer.
这个buffer 可以通过rgbframe 一行一行的写入缓冲区(ANativeWindow的缓冲区).
导入头文件:
include
#include <jni.h>
#include <string>
#include <jni.h>
#include <string>
#include <android/log.h>
//extern "C" 主要作用就是为了能够正确实现C++代码调用其他C语言代码 加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
extern "C" {
//编码
#include "libavcodec/avcodec.h"
//封装格式处理
#include "libavformat/avformat.h"
//像素处理
#include "libswscale/swscale.h"
//用于android 绘制图像的
#include <android/native_window_jni.h>
#include <unistd.h>
}
#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"jnilib",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"jnilib",FORMAT,##__VA_ARGS__);
extern "C"
JNIEXPORT void JNICALL
Java_androidrn_ffmpegdemo_MyVideoView_render(JNIEnv *env, jobject instance, jstring input_,
jobject surface) {
const char *input = env->GetStringUTFChars(input_, 0);
// TODO
//无论编码还是解码 都要调用这个 注册各大组件
av_register_all();
//获取AVFormatContext 比特率 时长 文件路径 流的信息(nustream) 都封装在这里面
AVFormatContext *pContext = avformat_alloc_context();
//AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options
//上下文 文件名 打开文件格式 获取信息(AVDictionary) 凡是AVDictionary字典 都是获取视频文件信息
if (avformat_open_input(&pContext, input, NULL, NULL) < 0) {
LOGE("打开失败");
return;
}
//给nbstram填充信息
if (avformat_find_stream_info(pContext, NULL) < 0) {
LOGE("获取信息失败");
return;
}
//找到视频流
int video_stream_ids = -1;
for (int i = 0; i < pContext->nb_streams; ++i) {
LOGE("循环 %d", i);
//如果填充的视频流信息 -> 编解码,解码器 -> 流的类型 == 视频的类型
//codec 每一个流 对应的解码上下文 codec_type 流的类型
if (pContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_ids = i;
break;
}
}
// 获取到解码器上下文 获取视频编解码器
AVCodecContext *pCodecCtx = pContext->streams[video_stream_ids]->codec;
//解码器
AVCodec *pCodex = avcodec_find_decoder(pCodecCtx->codec_id);
//打开解码器 为什么avcodec_open2 版本升级的原因
if (avcodec_open2(pCodecCtx, pCodex, NULL) < 0) {
LOGE("解码失败");
return;
}
//得到解封装 读取 解封每一帧 读取每一帧的压缩数据
//初始化avpacket 分配内存 FFMpeg 没有自动分配内存 必须手动分匹配手动释放 不过有分配函数
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
//初始化packet 每一个包是一个完整的数据帧,来暂存解复用之后、解码之前的媒体数据(一个音/视频帧、一个字幕包等)及附加信息(解码时间戳、显示时间戳、时长等)
// av_init_packet(packet);
//初始化AVFrame
AVFrame *frame = av_frame_alloc();
//目的数据 RGB的frame 声明RGBFrame
AVFrame *rgbFrame = av_frame_alloc();
//给RGB 缓冲区初始化 分配内存
//avpicture_get_size 计算给定宽度和高度的图片的字节大小 如果以给定的图片格式存储。
// uint8_t 8位无符号整型数(int)
uint8_t *out_buffer = (uint8_t *) av_malloc(
avpicture_get_size((AV_PIX_FMT_RGBA), pCodecCtx->width, pCodecCtx->height));
LOGE("render 宽 %d render 高 %d", pCodecCtx->width, pCodecCtx->height);
// //填充rgb缓冲区
int re = avpicture_fill(reinterpret_cast<AVPicture *>(rgbFrame), out_buffer, AV_PIX_FMT_RGBA,
pCodecCtx->width, pCodecCtx->height);
LOGE("申请YUM内存%d ", re);
//获取转化的上下文 参数 通过解码器的上下文获取到 pCodecCtx->pix_fmt 是mp4的上下文 SWS_BICUBIC 是级别 越往下效率越高清晰度越低
//AV_PIX_FMT_RGBA 这个是需要转化的格式
SwsContext *swsContext = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGBA,
SWS_BICUBIC, NULL, NULL, NULL);
//AVFormatContext *s, AVPacket *pkt 上下文,avpacket 是数据包的意思
//packet 入参
int got_frame;
int length = 0;
//获取ANtiviewindow ANativeWindow_fromSurface
ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
//声明buffer 视频的缓冲区 rgbFrame 的缓冲区
ANativeWindow_Buffer outBuffer;
int frame_count = 0;
//解码每一帧图片
while (av_read_frame(pContext, packet) >= 0) {
// 解封装
//参数
//AVCodecContext *avctx, 解码器的上下文
// AVFrame *picture, 已经解封装的frame,是直接能在手机上播放的frame
// int *got_picture_ptr, 入参出参对象, 可以根据出参判断是否解析完成
// const AVPacket *avpkt 压缩的packet数据
if (packet->stream_index == video_stream_ids) {
//把packet压缩的数据 解压 赋给默认frame
length = avcodec_decode_video2(pCodecCtx, frame, &got_frame, packet);
LOGE(" 获得长度 %d ", length);
//判断是否读完
if (got_frame) {
LOGE("解码第 %d 个 frame ", frame_count++);
//绘制之前 需要配置一些信息. 宽高 输出的格式 可以修改 pCodecCtx->width 和height
ANativeWindow_setBuffersGeometry(nativeWindow, pCodecCtx->width, pCodecCtx->height,
WINDOW_FORMAT_RGBA_8888);//AV_PIX_FMT_RGBA 这个也是上面转化的格式
//因为surfaceview 只支持RGB所以 这里不转成YUV, 电视机 机顶盒都是YUV
//TODO buffer很重要
//先锁定当前的window 和surfaceview 类似 需要先锁定 , 第一个参数就是上面获取的ANtiviewindow, 第二个参数是buffer 这个buffer非常重要他是需要绘制的缓冲区.
ANativeWindow_lock(nativeWindow, &outBuffer, NULL);
// 调用转化方式,目的转化为RGB格式 sws_scale 函数
sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
frame->linesize, 0, pCodecCtx->height, rgbFrame->data,
rgbFrame->linesize);
//获取window 被输出的画面的首地址 window首地址
uint8_t *dst = static_cast<uint8_t *>(outBuffer.bits);
//拿到一行有多少字节 为什么需要×4 rgba
int destStride = outBuffer.stride * 4;//这里就是window的一行数据 字节
//获取rgbframe(像素数据)的 首地址
uint8_t *src = rgbFrame->data[0];
//实际内存一行的数量
int srcStride = rgbFrame->linesize[0];
LOGE("实际内存第一行一行的数量 = %d", srcStride);
//把rgbframe拷贝到缓冲区 到解码的高度
for (int i = 0; i < pCodecCtx->height; ++i) {
//dest 目的地址 src 源地址 n 数量
//void *memcpy(void *dest, const void *src, size_t n);
//从源src所指的内存地址的起始位置 开始拷贝n个字节 到目标dest 所指的内存地址的起始位置中
//window首地址 变化的 真正拷贝的字节数 srcStride
memcpy(dst + i * destStride,
reinterpret_cast<const void *>(src + i * srcStride),
srcStride);
}//这个循环完成就完成拷贝了.
LOGE("解锁画布 睡眠16毫秒 ");
//解锁画布
ANativeWindow_unlockAndPost(nativeWindow);
//画面绘制完之后 画面停止16毫秒 usleep在#include <unistd.h> 中
usleep(1000 * 16);
}
}
//释放
av_free_packet(packet);
}
ANativeWindow_release(nativeWindow);
av_frame_free(&rgbFrame);
av_frame_free(&frame);
avcodec_close(pCodecCtx);
avformat_free_context(pContext);
env->ReleaseStringUTFChars(input_, input);
}
结果: