FFmpeg使用流程文述
1,去官网上下载代码,交叉编译成so文件和头文件配置进项目
2,每个视频容器都会有音频视频两个轨道,需要FFmpeg分别拿到音频索引和视频索引
3,读取视频需要在子线程中进行,采用生产消费的模式,并需要队列
4,当我们的生产者读取到视频数据包的时候,什么都不管,将它放置在消费者的队列中
5,消费者也叫视频解码线程,要用一个死循环,也就是解码器,不断的去解码AVPacket,解码成原始画面
6,我们假设这个解码后的数据叫做AVFrame
7, 音频解码器也是一样的,需要单独一份, 当读取到音频文件放在音频解码器,整体架构就是这样,一共三个线程
8,初始化FFmpeg时需要总上下文,还要对视频和音频进行初始化,视频处理也需要两个东西,一个是视频上下文,一个是视频解码器,音频也是一样的,需要上下文和解码器
9,总上下文是总文件信息,视频上下文是视频流相关信息
10,新建一个surface
11, 在native中初始化总上下文
//初始化总上下文
AVFormatContext avFormatContext = avformat_alloc_context();
//还要初始化ffmpeg的网络播放,如果没有,就不能播放网络视频
avformat_open_network_init();
12, 打开视频文件,首先要将java中传进来的文件路径变为native的路径,因为C语言是面向过程的,所以每次使用都需要把上下文传进去
avformat_open_input(&avFormatContext, url, NULL, NULL);
13, 需要判断视频流文件是否可用
int code = avformat_find_stream_info(arFormatContext, NULL);
//如果小于0则不可用
if (code < 0) {
return;
}
14,需要知道流的个数是多少,并通过对流中的codec_type的比对,识别视频流,音频流,并拿到索引
avFormatContext -> nb_Streams;
//遍历流的个数,找到音频流,视频流索引
for (int i = 0; i < avFormatContext->nb_Streams; i++) {
// 视频流对象,如果是视频
if (avFormatContext -> Stream[i] -> codecpar -> codec_type == AVMEDIA_TYPE_VIDEO) {
// 获取到视频索引
videoIndex = i;
//获取到视频的信息,类似于视频宽高,码流,等级等的信息
AVCodecParamsters *Parameters = avFormatContext->stream[i]->codecpar;
Parameters -> width; //宽度
Parameters -> height; //高度
Parameters -> video_play; //延迟时间
//如果是音频
} else if (avFormatContext -> Stream[i] -> codecpar -> codec_type == AVMEDIA_TYPE_AUDIO) {
// 获取到音频索引
audioIndex = i;
}
}
15,如果是视频,获取到视频解码器
AVCodec *dec = avcodec_find_decoder(paramters->codec_id);
//获取到视频上下文,初始化解码器
AVCodecContext *videoContext = avcodec_alloc_context3(dec);
16, 把读取文件里面的参数信息,设置到新的上下文
avcodec_parameters_to_context(videoContext, parameters);
17, 打开解码器
avcodec_open2(videoContext, dec, 0);
18,开始实例化线程ptread_creat();
pthread_t thread_decode;
pthread_creat(&thread_decode, NULL, decodePacket, NULL);
// 最终创建decodePacket方法,在子线程中用于解码视频文件
19,视频解码器的创建思路也一样,实例化线程并创建方法decodeVideo解码视频数据包
20,在视频文件解码器的decodePacket方法中
while (isStart) {
AVPacket *avPacket = av_packet_alloc();
int ret = av_read_frame(avFormatContext, avPacket); //读取到的是压缩数据
// 如果ret小于0则跳出循环
if (ret < 0) {
//文件末尾
break;
}
}
21,判断音视频
if(avPacket -> stream_index == videoIndex) {
//则是视频包
} else if (avPacket->stream_index == audioIndex) {
//则是音频包
}
22,这时需要导入一个队列,因为C++中不像java中Arraylist队列,因此需要自己写一个,主要有push(),get()方法
23,需要有两个队列,音频队列和视频队列
audioQueue = new EasyQueue;
videoQueue = new EasyQueue;
24,接下来我们回到decodePacket方法中,如果是视频包,我们把它放在视频队列里去;
需要注意的是,不能让我们的队列数量过大,否则会造成内存问题,因此在decodecPacket方法中,要限制videoPacket的大小
if (videoQueue -> size() > 50) {
usleep(100*1000);
}
25,然后我们的视频解码器中开始解码decovideo,在解码器来一个while循环,并从队列中拿到数据
while (isStart) {
AVPacket *videoPacket = av_packet_alloc();
//有数据就会取出来,没数据就会阻塞
videoQueue -> get(videoPacket);
}
26, 数据拿出来就进行解码avcodec_send_packet:
int ret = avcodec_send_packet(videoContext, videoPacket);
if (ret != 0) {
av_packet_free(&videoPacket);
av_free(videoPacket);
videoPacket = NULL;
}
27, send之后就要进行接收avcode_receive_frame
//容器
AVFrame *videoFrame = av_frame_alloc();
avcode_receive_frame(videoContext, videoFrame);
//如果返回值不等于0的话也要把videoFrame释放掉
if (ret != 0) {
av_frame_free(&videoFrame);
av_free(videoFrame);
videoFrame = NULL;
av_packet_free(&videoPacket);
videoPacket = NULL;
}
28, 我们现在就得到了YUV,目前还不能直接渲染在屏幕上,因为surface不支持YUV,而且视频宽高还没有和屏幕宽高等比例压缩,Surface只支持RGB,不支持YUV
29,拿到surfaceView才能进一步拿到surface,然后拿到Native层的NativeWindow,进而拿到ANativeWindow_Buffer缓冲区对象。缓冲区对象有个bits指针,指向surfaceView显示的一个缓冲区,就和字节数组一样,我们将视频的YUV进行转换赋值给surfaceVOew内的值,在SurfaceView里就会有显示
30,先拿到Native对象
ANativeWindow* nativeWindow = ANativeWindow_fromSurface(env, surface);
31,对native进行设值
//缓冲区数据
ANativeWindow_setBufferGeometry(nativeWindow, &windowBuffer, NULL);
32, 我们需要对YUV数据进行转换,转化的话必然有转换器和上下文
//获取转换上下文
SwsContext *swsContext = sws_getContext(width, height, videoContext->pix->fmt, width, height,
AV_PIX_FMT_RGBA, SWS_BICUBIC, NULL);
33,得到上下文后转换使用sws_scale
//初始化容器
AVFrame *rgbFrame = av_frame_alloc();
//目前rgbFrame初始化了,但是里面的容器没有初始化,需要告诉我容器大小
//rgbFrame -> data;
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGBA, width, height, 1);
//知道大小后然后赋值,先实例化容器
//实例化容器
uint8_t *outbuffer = (uint8_t*)av_malloc(numBytes * sizeof(uint8_t));
//进行填充用av_image_fill_arrays对rgbFrame->data赋值outbuffer
av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, outbuffer,
AV_PIX_FMT_RGBA, width, height, 1)
34, 从avFrame转换到rgbFrame相当于直接转换到缓冲区outBuffer
//进行转换
sws_scale(swsContext, videoFrame->data, video->linesize, 0,
videoContext->height, rgbFrame->data, rgbFrame->lineSize);
35,然后将转换好的数据一行一行copy到缓冲区
//outBuffer
ANativeWindow_lock(nativeWindow, &windowBuffer, NULL);
//目的地
uint8_t *dstWindow = static_cast<uint8_t *>(windowBuffer, bits);
for (int i = 0; i < height; ++i) {
memcpy(dstWindow, + i * windowBuffer.stride * 4, outbuffer + i * rgbFrame -> linesize[0]);
}
36,适配宽高
//在java层通过反射拿到视频宽高
//通过native层反射给java传值
jclass david_player env->GetObjectClass(instance);
jmethodID onSizeChange = env -> GteMethodID(david_player, "onSizeChange", "(II)V");
37, 这时候Java层的onSizeChange方法得到调用
//先计算视频宽高比
float ratio = width/(float)height;
//再拿到屏幕的宽
int ScreenWidth =surfaceView.getConetext().getResource().getDisplayMetrics().widthPixels;
//让视频的宽等于屏幕的宽
int videoWidth = screenWidth;
//再通过宽高比得到高
int videoHright = (int)(screenwidth/radio)
38,我们再给它LayoutParams,将宽高作为参数没进去
Relativelayout.LayputParams lp = new Relativelayout.LayoutParamas(videoWidth, videoHeight);
//再把设置好的布局信息再次设置给SurfaceView
//调整surfaceview控件的大小
SurfaceView.setLayoutParams(lp);
视频解码基本到这就结束了,接下来开始音频解码
音频解码
1,在decodeAudio方法中我们获取到decodePacket传来的audioPacket和视频解码一样也是用
int ret = avcodec_send_packet(audioContext, audioPacket);
//如果ret不等于0就释放掉
... ...
//再用audioFrame当容器去接收解码后的audioFrame
AVFrame *audioFrame = av_frame_alloc();
2,这个解码后的音频数据同样不能直接播放,还要进行转换
//在java层新建方法 需要参数采样频率和通道数
creatTrack(int sampleRateInitz ,int nb_channels);
3,在这个方法内使用AudioTrack
AudioTrack audio = new AudioTrack();
//传入采样频率,buffer ,size 等参数
4,最后再用audioTrack.play()
就可以播放了
5,在java层新建个方法
playTrack(byte[] buffer, int lenth) {
if (audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
//播放声音
audioTrack.write(buffer, 0, lenth);
}
}
需要从native层将buffer传进来
6,现在需要做两件事,在native初始化时回调creatTrack,在解码后回调playTrack
7, 需要在native层通过反射得到creatTrack,传入几个参数
8,初始化一个音频转换上下文
SwrContext *swrAudioContext = swr_alloc();
// 音频转换上下文,也是输入和输出,设置转换器上下文
swr_alloc_set_opts(swrAudioContext, swrAudioContext, out_format, out_sample_rate,
audioContext->channel_layout,
audioContext->sample_fmt,
audioContext->sample_rate, 0, NULL);
//进行初始化
swr_init(swrAudioContext);
9,来个audioFrame作为音频容器
AudioFrame audioFrame = av_Frame_alloc();
10, 视频需要YUV的转换,而音频转换是不需要的
int ret = avcodec_recive_frame(videoContext, audioFrame);
11, 当ret > 0时进行转换
if (ret > 0) {
//frame->data 数据源
//outbuffer -> 目的地
//转换
swr_conrert(swrContext, &outbuffer, 44100*2
(const uint8_t **)(frame -> data), frame->nb_samples);
}
12,获取转换之后的个数
//转换个数
int size = av_samples_get_buffer_size(NULL, out_channer_nb, frame->nb_samples,
AV_SAMPLE_FMT_S16, 1);
13, 回调应用层,因为是子线程回调主线程,所以刚才的反射方法已经不行了,需要写一个动态注册JNI_load获取到一个jvm,就可以拿到主线程
14,根据jvm创造一个env,然后把env挂靠到jvm里面
-JavaVm *javaVm = NULL;
JNIEnv *jniEnv;
if (javaVM->AttachCurrenThread(&jniEnv, 0) != JNI_OK) {
continue;
}
//这样就拥有了访问主线程的能力
15,将这个对象mInstance变成全局
jobject mInstance = env -> NewGlobalRef(instance);
16,这时候就能回调应用层playTrack方法了
jniEnv->CallVoidMethod(mInstance, playTrack, byteArrays, size);
17, 建一个java数组的容器
jbyteArray byteArrays = jniEnv -> NewByteArray(size);
18, 把byteArrays塞进去
jniEnv -> setByteArrayRegion(byteArrays, 0, size,
reinteroret_cast<const jbyte *>countbuffer);
19, 调用完后再释放掉,音频解码流程至此基本上就结束了