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, 调用完后再释放掉,音频解码流程至此基本上就结束了