上一篇中FFmpeg解封装中在TinaFFmpeg中的prepare方法里把解码器上下文AVCodecContext交给VideoChannel,AudioChannel后,解码工作就交给它们来处理了,这节我们来看它们是如何处理的。
解码入口
prepare完成后会调用Java中TinaPlayer的onPrepare的方法,然后回调 PlayActivity的start方法,然后进入native层的start方法:
LOGE("native prepare流程准备完毕"); // 准备完了 通知java 你随时可以开始播放 callHelper->onPrepare(THREAD_CHILD);//prepare完成后会调用Java中TinaPlayer的onPrepare的方法,然后回调 PlayActivity的start方法,然后进入native层的start方法
//TinaPlayerpublic void onPrepare(){ if (null != listener){ listener.onPrepare(); } }//PlayActivitytinaPlayer.setOnPrepareListener(new TinaPlayer.OnPrepareListener() { @Override public void onPrepare() { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText("开始播放").show(); } }); //调用native_start tinaPlayer.start(); } });//TinaPlayerpublic void start(){ native_start();}
native层:调用ffmpeg中的start方法,然后分别调用videoChannel、audioChannel的play()方法
extern "C"JNIEXPORT void JNICALLJava_tina_com_player_TinaPlayer_native_1start(JNIEnv *env, jobject instance) { ffmpeg->start();}//TinaFFmpegvoid TinaFFmpeg::start() { //重新开线程 isPlaying = 1; if (audioChannel) { //设置为工作状态 audioChannel->play(); } if (videoChannel) { //设置为工作状态 videoChannel->setAudioChannel(audioChannel); videoChannel->play(); } pthread_create(&pid_play, 0, play, this);}
兵马未动,粮草先行。把解码需要的数据源packet放入到SafeQueue的同步队列中去:
void *play(void *args) { TinaFFmpeg *fFmpeg = static_cast<TinaFFmpeg *>(args); fFmpeg->_start(); return 0;}void TinaFFmpeg::_start() { int ret; //1.读取媒体数据包(音频、视频数据) while (isPlaying) { //读取文件的时候没有网络请求,一下子读完了,可能导致oom //特别是读本地文件的时候 一下子就读完了 if (audioChannel && audioChannel->packets.size() > 100) { //10ms av_usleep(1000 * 10); continue; } if (videoChannel && videoChannel->packets.size() > 100) { av_usleep(1000 * 10); continue; } AVPacket *avPacket = av_packet_alloc(); ret = av_read_frame(formatContext, avPacket); //等于0成功,其它失败 if (ret == 0) { if (audioChannel && avPacket->stream_index == audioChannel->id) { //todo 音频 //在audioChannel中执行 解码工作 audioChannel->packets.push(avPacket); } else if (videoChannel && avPacket->stream_index == videoChannel->id) { //在videoChannel中执行 解码工作 videoChannel->packets.push(avPacket); } } else if (ret == AVERROR_EOF) { //读取完成,但是可能还没有播放完成 if (audioChannel->packets.empty() && audioChannel->frames.empty() && videoChannel->packets.empty() && videoChannel->frames.empty()){ av_packet_free(&avPacket);//这里不释放会有内存泄漏,崩溃 break; } } else { av_packet_free(&avPacket); break; } } isPlaying = 0; audioChannel->stop(); videoChannel->stop();}
其实push队列跟audioChannel、videoChannel的play()的过程是同步的,是基于SafeQueue对象packets的生产者与消费者的问题处理,我们先来看视频解码。
视频解码
解码分为decode,render(渲染),创建两个线程,分别在各自的线程中处理。
void VideoChannel::play() { isPlaying = 1; packets.setWork(1); frames.setWork(1); //1.解码 pthread_create(&pid_decode, 0, decode_task, this); //2. 播放 pthread_create(&pid_render, 0, render_task, this);}//解码void VideoChannel::decode() { AVPacket *packet = 0; while (isPlaying) { int ret = packets.pop(packet); if (!isPlaying) { break; } if (!ret) { continue; } //把包丢给解码器 ret = avcodec_send_packet(avCodecContext, packet); releaseAvPacket(&packet); while (ret != 0) { break; } AVFrame *frame = av_frame_alloc(); avcodec_receive_frame(avCodecContext, frame); if (ret == AVERROR(EAGAIN)) { continue; } else if (ret != 0) { break; } //再开一个线程 播放。 frames.push(frame); } releaseAvPacket(&packet);}
decode的过程其实很简单,就是把packet交给 AVCodec处理,然后拿到frame,放入到frames队列中去,而frames就是我们render的数据源,下面来看render:
//渲染void VideoChannel::render() { //目标: RGBA, 没有缩放 swsContext = sws_getContext(avCodecContext->width, avCodecContext->height, avCodecContext->pix_fmt, avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, 0, 0, 0); //每个画面 刷新的间隔 单位:秒 double frame_delays = 1.0 / fps; AVFrame *frame = 0; uint8_t *dst_data[4]; int dst_linesize[4]; av_image_alloc(dst_data, dst_linesize, avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA, 1); while (isPlaying) { int ret = frames.pop(frame); if (!isPlaying) { break; } //src_lines: 每一行存放的 字节长度 sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data), frame->linesize, 0, avCodecContext->height, dst_data, dst_linesize); //获取当前 一个画面播放的时间 ------------- 处理音视频同步(后面再讲)--------------- //把解出来的数据给到 window的Surface,回调出去进行播放callback(dst_data[0], dst_linesize[0], avCodecContext->width, avCodecContext>height); releaseAvFrame(&frame); } av_freep(&dst_data[0]); releaseAvFrame(&frame); isPlaying = 0; sws_freeContext(swsContext); swsContext = 0;}
相关学习资料推荐,点击下方链接免费报名,先码住不迷路~】
音视频免费学习地址:
https://xxetb.xet.tech/s/2cGd0
【免费分享】音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击788280672加群免费领取~
看render的过程其实没有真正的处理渲染,只是将frame的数据data通过回调接口传出去了,TinaFFmpeg调用了这个回调,而这个回调的具体实现在JNI的入口函数里native-lib.cpp中:
//VideoChannelvoid VideoChannel::setRenderFrameCallback(RenderFrameCallback callback) { this->callback = callback;}//TinaFFmpegvoid TinaFFmpeg::setRenderFrameCallback(RenderFrameCallback callback) { this->callback = callback;}extern "C"JNIEXPORT void JNICALLJava_tina_com_player_TinaPlayer_native_1prepare(JNIEnv *env, jobject instance, jstring dataSource_) { .... ffmpeg->setRenderFrameCallback(render); .....}
那么这个render的回调函数究竟是如何处理的呢,如何把datas给到SurfaceView中显示呢?
void render(uint8_t *data, int linesize, int w, int h) { pthread_mutex_lock(&mutex); if (!window) { pthread_mutex_unlock(&mutex); return; } //设置窗口属性 ANativeWindow_setBuffersGeometry(window, w, h, WINDOW_FORMAT_RGBA_8888); ANativeWindow_Buffer window_buffer; if (ANativeWindow_lock(window, &window_buffer, 0)) { ANativeWindow_release(window); window = 0; pthread_mutex_unlock(&mutex); return; } //填充rgb数据给dst_data uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits); //stride : 一行多少个数据 (RGBA) * 4 int dst_linesize = window_buffer.stride * 4; //一行一行拷贝 for (int i = 0; i < window_buffer.height; ++i) { memcpy(dst_data + i * dst_linesize, data + i * linesize, dst_linesize); } ANativeWindow_unlockAndPost(window); pthread_mutex_unlock(&mutex);}
通过memcpy逐行拷贝dst_data中的数据,我们在看一遍render中的传参:
//把解出来的数据给到 window的Surface,回调出去进行播放callback(dst_data[0], dst_linesize[0], avCodecContext->width, avCodecContext->height);
这就把 videoChannel中解压的数据传给了上面的 NativeWindow中的buffer,NativeWindow作为Native层与Java层视频数据展现的承接容器,那具体的数据传输的介质是在TinaPlayer中通过setSurface传过来的SurfaceHolder中的Surface
/** * 画布创建OK*/@Overridepublic void surfaceCreated(SurfaceHolder holder) { native_setSurface(holder.getSurface());}/*** 画布发生变化(横竖屏)*/@Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { native_setSurface(holder.getSurface());}
至此,整个VideoChannal的解码、渲染工作完成,可以在PlayActivity中看到视频了,接下来处理声音:
同样的两个线程,一个解码,一个播放
void *audio_decode(void *args) { AudioChannel *audioChannel = static_cast<AudioChannel *>(args); audioChannel->decode(); return 0;}void *audio_play(void *args) { AudioChannel *audioChannel = static_cast<AudioChannel *>(args); audioChannel->_play(); return 0;}void AudioChannel::play() { packets.setWork(1); frames.setWork(1);...... // 省略代码后续再讲 初始化音视频同步的Context isPlaying = 1; //1. 解码 pthread_create(&pid_audio_decode, 0, audio_decode, this); //2. 播放 pthread_create(&pid_audio_player, 0, audio_play, this);}
解码过程一样:
//音频解码, 跟视频代码一样void AudioChannel::decode() { AVPacket *packet = 0; while (isPlaying) { int ret = packets.pop(packet); if (!isPlaying) { break; } if (!ret) { continue; } //把包丢给解码器 ret = avcodec_send_packet(avCodecContext, packet); releaseAvPacket(&packet); while (ret != 0) { break; } AVFrame *frame = av_frame_alloc(); avcodec_receive_frame(avCodecContext, frame); if (ret == AVERROR(EAGAIN)) { continue; } else if (ret != 0) { break; } //再开一个线程 播放。 frames.push(frame); } releaseAvPacket(&packet);}
decode的工作任务是把frame放入到frames队列中就完成了,下面就是播放声音了。
播放音频
关于OpenSL ES的使用可以进入ndk-sample查看native-audio工程:github.com/googlesampl…
OpenSL ES的开发流程主要有如下7个步骤:
初始化相关的类
作者:cxy107750
链接:
https://juejin.cn/post/6844903703804117006
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 Android播放声音(声音格式PCM),有两种方式,方式一是通过Java SDK中的AudioTrack(AudioRecorder录音),另一种是通过NDK的OpenSL ES来播放,这里的数据在Native层,所以避免JNI跨层的数据调用以及反射等相关处理,这里我们选择OpenSL ES来播放音频,OpenSL ES是Android NDK中的包,专门为嵌入式设备处理音频的库,引入库需要修改CmakeLists.txt文件(FFmpeg只是编解码库,不支持处理输入输出设备)
class AudioChannel : public BaseChannel {public: AudioChannel(int id, AVCodecContext *avCodecContext, AVRational time_base); ~AudioChannel();private: /** * OpenSL ES */ //引擎 SLObjectItf engineObject = 0; //引擎接口 SLEngineItf engineInterface = 0; //混音器 SLObjectItf outputMixObject = 0; //播放器 SLObjectItf bqPlayerObject = 0; //播放器接口 SLPlayItf bqPlayerInterface = 0; //队列结构 SLAndroidSimpleBufferQueueItf bqPlayerBufferQueueInterface = 0;};
创建引擎与接口
SLresult result; // 创建引擎 SLObjectItf engineObject result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); if (SL_RESULT_SUCCESS != result) { return; } // 初始化引擎(init) result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); if (SL_RESULT_SUCCESS != result) { return; } // 获取引擎接口SLEngineItf engineInterfaceresult = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface); if (SL_RESULT_SUCCESS != result) { return; }
设置混音器
result = (*engineInterface)->CreateOutputMix(engineInterface,&outputMixObject, 0, 0, 0); if (SL_RESULT_SUCCESS != result) { return; } // 初始化混音器outputMixObject result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE); if (SL_RESULT_SUCCESS != result) { return; } //3.2 配置音轨(输出) //设置混音器 SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject}; SLDataSink audioSnk = {&outputMix, NULL}; //需要的接口, 操作队列的接口,可以添加混音接口 const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE}; const SLboolean req[1] = {SL_BOOLEAN_TRUE};
创建播放器
//创建buffer缓冲类型的队列 2个队列SLDataLocator_AndroidSimpleBufferQueue android_queue ={SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};//pcm数据格式 //pcm +2(双声道)+ 44100(采样率)+ 16(采样位)+ LEFT|RIGHT(双声道)+ 小端数据SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1, SL_PCMSAMPLEFORMAT_FIXED_16,SL_PCMSAMPLEFORMAT_FIXED_16,SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,SL_BYTEORDER_LITTLEENDIAN};//数据源 将上述配置信息放到这个数据源中 SLDataSource slDataSource = {&android_queue, &pcm};//创建播放器(*engineInterface)->CreateAudioPlayer(engineInterface, &bqPlayerObject, &slDataSource,&audioSnk, 1,ids, req);//初始化播放器(*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
设置播放回调
(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueueInterface); //设置回调(*bqPlayerBufferQueueInterface)->RegisterCallback(bqPlayerBufferQueueInterface, bqPlayerCallback, this);
设置播放状态
(*bqPlayerInterface)->SetPlayState(bqPlayerInterface, SL_PLAYSTATE_PLAYING);
启动激活函数
//6. 手动激活启动回调bqPlayerCallback(bqPlayerBufferQueueInterface, this);
回调接口中处理转码PCM的过程
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) { AudioChannel *audioChannel = static_cast<AudioChannel *>(context); //获取pcm数据 int dataSize = audioChannel->getPcm(); if(dataSize > 0){ (*bq)-> Enqueue(bq, audioChannel->data, dataSize);//这里取 16位数据 }}//获取Pcm 数据int AudioChannel::getPcm() { int data_size = 0; AVFrame *frame; int ret = frames.pop(frame); if (!isPlaying) { if (ret) { releaseAvFrame(&frame); } return data_size; } //48000HZ 8位 =》 44100 16位 //重采样 //假设我们输入了10个数据, swrContext转码器,这一次处理了8个数据 int64_t delays = swr_get_delay(swrContext, frame->sample_rate); //将nb_samples个数据由 sample_rate 采样率转成 44100后,返回多少个数据 // 10 个 48000 = nb个 44100 //AV_ROUND_UP : 向上取整 1.1 = 2 int64_t max_samples = av_rescale_rnd(delays + frame->nb_samples, out_sample_rate, frame->sample_rate, AV_ROUND_UP); //上下文 + 输入缓冲区 + 输出缓冲区能接受的最大数据量 + 输入数据 + 输入数据个数 // 返回每一个声道的数据 int samples = swr_convert(swrContext, &data, max_samples, (const uint8_t **)frame->data, frame->nb_samples); //获得 samples个2字节(16位) * 2声道 data_size = samples * out_samplesize * out_channels; //获取一个frame的一个相对播放时间 //获得播放这段数据的秒速(时间机) clock = frame->pts * av_q2d(time_base); releaseAvFrame(&frame); return data_size;}
播放流程处理完成后,记得释放OpenSL中的对象。
void AudioChannel::stop() { isPlaying = 0; packets.setWork(0); frames.setWork(0); //释放播放器 if(bqPlayerObject){ (*bqPlayerObject)->Destroy(bqPlayerObject); bqPlayerObject = 0; bqPlayerInterface = 0; bqPlayerBufferQueueInterface = 0; } //释放混音器 if(outputMixObject){ (*outputMixObject)->Destroy(outputMixObject); outputMixObject = 0; } //释放引擎 if(engineObject){ (*engineObject)->Destroy(engineObject); engineObject = 0; engineInterface = 0; }}
以上就是音频、视频解码及播放的整个流程,但是会存在的问题是音视频他们在各自的线程里播放,互不干扰,但是从视频源来的视频、音频在某一时间点是相互对应的,而我们的播放端是从队列里拿数据进行播放,队列有同步,等待,阻塞等因素,会造成音视频两者不同步,大概率上甚至说基本是不同步的。所以需要处理二者的同步关系,如何来处理二者的同步呢?下一篇详解.
需要源码的,可前往GitHub获取, 顺便点个star呗。
作者:cxy107750
原文 FFmpeg视频播放(音视频解码) - 掘金