热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

IJKPlayerffplay流程详解

ffplay流程图.jpg上图摘自雷霄骅雷神的博客是FFmpeg中ffplay.c的结构流程图ijkplayer的ff_ffplay.c是建立在FFmpeg的ffplay.c的基础

《IJKPlayer ffplay流程详解》 ffplay流程图.jpg

上图摘自 雷霄骅 雷神的博客 是FFmpeg中 ffplay.c的结构流程图

ijkplayer的ff_ffplay.c是建立在FFmpeg的ffplay.c的基础之上的。

其核心:

read_thread线程负责接收服务器的数据
video_thread线程负责video decode
audio_thread负责audio decode
video_refresh_thread线程负责video 的渲染

audio的播放暂时还没有细细的研究 audio_open是关键函数 SDL_OpenAudio():SDL中打开音频设备的函数。注意它是根据SDL_AudioSpec参数打开音频设备。SDL_AudioSpec中的callback字段指定了音频播放的回调函数sdl_audio_callback()。当音频设备需要更多数据的时候,会调用该回调函数。因此该函数是会被反复调用的。

ffp_prepare_async_l 函数开启read_thread和video_refresh_thread线程。
read_thread开启audio_thread和video_thread解码线程

ffp_prepare_async_l函数主要调用 ffpipeline_open_audio_output函数和stream_open函数。

ffpipeline_open_audio_output函数调用SDL_AoutIos_CreateForAudioUnit函数

SDL_Aout *SDL_AoutIos_CreateForAudioUnit()
{
SDL_Aout *aout = SDL_Aout_CreateInternal(sizeof(SDL_Aout_Opaque));
if (!aout)
return NULL;
// SDL_Aout_Opaque *opaque = aout->opaque;
aout->free_l = aout_free_l;
aout->open_audio = aout_open_audio;
aout->pause_audio = aout_pause_audio;
aout->flush_audio = aout_flush_audio;
aout->close_audio = aout_close_audio;
aout->func_set_playback_rate = aout_set_playback_rate;
return aout;
}

SDL_AoutIos_CreateForAudioUnit函数 实现了函数指针的赋值,用于ffplay回调控制audio output的方法。

stream_open函数进行初始化frame队列 packet队列还有音视频同步相关的clock时钟并且同时开启了read_thread线程和video_refresh_thread线程。

video_refresh_thread:这个线程负责 video 的渲染。

read_thread:
这个线程先进行FFmpeg初始化操作,然后获取视频流信息
avformat_open_input():打开媒体。
avformat_find_stream_info():获得媒体信息。这个函数有时候会影响首画的时间,看这个函数的源码发现:这个函数会一直分析视频流的信息,当一直获取不到信息的时候就会一直在这个函数中,一直到它最大的限制,才会出来,会有好几秒的时间。如果网络卡的话,时间会更长。但是我们可以自己给他添加限制,到达我们自己的限制条件的时候就会出来,不会一直在这个函数里面。
ic->probesize这个控制分析视频流的大小,当读的视频流大小达到这个size大小的时候,即使没有获取到信息 也会出来
ic->max_analyze_duration这个事控制视频流信息的大小,同理 当没有获取到信息,读取的视频流的时长达到这个duration的时候 也会出来。
av_dump_format():输出媒体信息到控制台。
stream_component_open():分别打开视频/音频/字幕解码线程。(在下面详细讲解该函数)
av_read_frame():获取一帧压缩编码数据(即一个AVPacket)。
packet_queue_put():根据压缩编码数据类型的不同(视频/音频/字幕),放到不同的PacketQueue中。

stream_component_open该函数:

1.如果是video:

ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);在这个函数中选择硬解码还是软解码,在iOS中实现如下

static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
IJKFF_Pipenode* node = NULL;
IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
if (ffp->videotoolbox) {
node = ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp);
}
if (node == NULL) {
ALOGE("vtb fail!!! switch to ffmpeg decode!!!! \n");
node = ffpipenode_create_video_decoder_from_ffplay(ffp);
ffp->stat.vdec_type = FFP_PROPV_DECODER_AVCODEC;
opaque->is_videotoolbox_open = false;
} else {
ffp->stat.vdec_type = FFP_PROPV_DECODER_VIDEOTOOLBOX;
opaque->is_videotoolbox_open = true;
}
ffp_notify_msg2(ffp, FFP_MSG_VIDEO_DECODER_OPEN, opaque->is_videotoolbox_open);
return node;
}

在android中实现如下:

static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
IJKFF_Pipenode *node = NULL;
if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2)
node = ffpipenode_create_video_decoder_from_android_mediacodec(ffp, pipeline, opaque->weak_vout);
if (!node) {
node = ffpipenode_create_video_decoder_from_ffplay(ffp);
}
return node;
}

选择完解码器,然后开始解码

decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")创建一个解码线程

static int video_thread(void *arg)
{
FFPlayer *ffp = (FFPlayer *)arg;
int ret = 0;
if (ffp->node_vdec) {
ret = ffpipenode_run_sync(ffp->node_vdec);
}
return ret;
}

不管是iOS还是android中:

ffpipenode_create_video_decoder_from_ffplay
ffpipenode_create_video_decoder_from_android_mediacodec
ffpipenode_create_video_decoder_from_ios_videotoolbox
这三个方法都是返回node,并node这个结构体中的函数指针赋值。我们解码的时候 调用的就是node->func_run_sync该方法,使用选择的解码器去解码video。

软解码 该方法如下:

static int func_run_sync(IJKFF_Pipenode *node)
{
IJKFF_Pipenode_Opaque *opaque = node->opaque;
return ffp_video_thread(opaque->ffp);
}

使用videotoolbox 该方法如下:

static int func_run_sync(IJKFF_Pipenode *node)
{
IJKFF_Pipenode_Opaque *opaque = node->opaque;
int ret = videotoolbox_video_thread(opaque);
if (opaque->context) {
dealloc_videotoolbox(opaque->context);
free(opaque->context);
opaque->cOntext= NULL;
}
return ret;
}

使用mediacodec,该方法如下:

static int func_run_sync(IJKFF_Pipenode *node)
{
JNIEnv *env = NULL;
IJKFF_Pipenode_Opaque *opaque = node->opaque;
FFPlayer *ffp = opaque->ffp;
VideoState *is = ffp->is;
Decoder *d = &is->viddec;
PacketQueue *q = d->queue;
int ret = 0;
int dequeue_count = 0;
AVFrame *frame = NULL;
int got_frame = 0;
AVRational tb = is->video_st->time_base;
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
double duration;
double pts;
// int64_t decode_time = 0;
if (!opaque->acodec) {
return ffp_video_thread(ffp);
}
if (JNI_OK != SDL_JNI_SetupThreadEnv(&env)) {
ALOGE("%s: SetupThreadEnv failed\n", __func__);
return -1;
}
frame = av_frame_alloc();
if (!frame)
goto fail;
if (opaque->frame_rotate_degrees == 90 || opaque->frame_rotate_degrees == 270) {
opaque->frame_width = opaque->avctx->height;
opaque->frame_height = opaque->avctx->width;
} else {
opaque->frame_width = opaque->avctx->width;
opaque->frame_height = opaque->avctx->height;
}
opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread");
if (!opaque->enqueue_thread) {
ALOGE("%s: SDL_CreateThreadEx failed\n", __func__);
ret = -1;
goto fail;
}
while (!q->abort_request) {
/*spb add
if(ffp_frame_queue_nb_remaining(&is->pictq) == 2)
ffp_frame_queue_next(&is->pictq);
if (is->videoq.nb_packets == 0)
{
av_log(NULL, AV_LOG_INFO, "spb_log videoq is empty\n");
}
end*/
Frame *vp = ffp_frame_queue_peek_writable(&is->pictq);
int64_t timeUs = opaque->acodec_first_dequeue_output_request ? 0 : AMC_OUTPUT_TIMEOUT_US;
got_frame = 0;
// int64_t start_drain = av_gettime()/1000;
ret = drain_output_buffer(env, node, timeUs, &dequeue_count, frame, &vp->dest_frame, &got_frame);
// int64_t end_drain = av_gettime()/1000;
if (opaque->acodec_first_dequeue_output_request) {
SDL_LockMutex(opaque->acodec_first_dequeue_output_mutex);
opaque->acodec_first_dequeue_output_request = false;
SDL_CondSignal(opaque->acodec_first_dequeue_output_cond);
SDL_UnlockMutex(opaque->acodec_first_dequeue_output_mutex);
}
if (ret != 0) {
ret = -1;
if (got_frame && frame->opaque)
SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);
goto fail;
}
if (got_frame) {
// av_log(NULL, AV_LOG_INFO, "spb_log drain_output_buffer success need time: %lld, time : %lld\n", end_drain - start_drain, end_drain - decode_time);
// decode_time = end_drain;
duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
ret = ffp_queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
if (ret) {
if (frame->opaque)
SDL_VoutAndroid_releaseBufferProxyP(opaque->weak_vout, (SDL_AMediaCodecBufferProxy **)&frame->opaque, false);
}
av_frame_unref(frame);
}
}
fail:
av_frame_free(&frame);
SDL_AMediaCodecFake_abort(opaque->acodec);
if (opaque->n_buf_out) {
free(opaque->amc_buf_out);
opaque->n_buf_out = 0;
opaque->amc_buf_out = NULL;
opaque->off_buf_out = 0;
opaque->last_queued_pts = AV_NOPTS_VALUE;
}
if (opaque->acodec) {
SDL_VoutAndroid_invalidateAllBuffers(opaque->weak_vout);
SDL_LockMutex(opaque->acodec_mutex);
SDL_AMediaCodec_stop(opaque->acodec);
SDL_UnlockMutex(opaque->acodec_mutex);
}
SDL_WaitThread(opaque->enqueue_thread, NULL);
SDL_AMediaCodec_decreaseReferenceP(&opaque->acodec);
ALOGI("MediaCodec: %s: exit: %d", __func__, ret);
return ret;
#if 0
fallback_to_ffplay:
ALOGW("fallback to ffplay decoder\n");
return ffp_video_thread(opaque->ffp);
#endif
}

这个方法代码较多,但是我们能看到 其核心就做了两件事
第一件事 opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread"); 创建一个新的线程,在packet queue中不停的取数据 放入硬解码芯片。
第二件事 在下面的while循环中 调用drain_output_buffer这个函数 获取解码后的数据信息。

2.如果是audio:

先执行ret = audio_open(ffp, channel_layout, nb_channels, sample_rate, &is->audio_tgt),audio_open方法。
并没有详细的研究audio的每一步,audio_open也是在根据获取到的流媒体信息进行audio 解码和播放的初始化操作。
其中 wanted_spec.callback = sdl_audio_callback 是有关audio播放的一个函数指针的赋值。

然后

(ret = decoder_start(&is->auddec, audio_thread, ffp, "ff_audio_dec") 开启线程解码

audio解码使用软解,audio数据量小,算法相对简单没有video复杂,解码对CPU资源占用极小。

ijk销毁函数:iOS调用shutdown,android调用release。

这两个函数本质都是要调用ffplay的 ffp_stop_l和ffp_wait_stop_l函数,
ijk的释放都是在ffp_wait_stop_l函数中,这个函数主要通过调用stream_close函数来释放掉ijk。
stream_close会销毁在ijk初始化时候创建的 队列 流 解码器 线程锁和线程条件变量。

未完待续。


推荐阅读
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 本文讨论了如何使用GStreamer来删除H264格式视频文件中的中间部分,而不需要进行重编码。作者提出了使用gst_element_seek(...)函数来实现这个目标的思路,并提到遇到了一个解决不了的BUG。文章还列举了8个解决方案,希望能够得到更好的思路。 ... [详细]
  • RingBuffer,或者说CircularBuffer,是一个长度固定的缓冲区,当从一端插入元素超过指定的最大长度时,缓冲区另一端的元素 ... [详细]
  • 6(自)、交换机之关键字模式
    上一节中的我们的日志系统将所有消息广播给所有消费者,对此我们想做一些改变,例如我们希望将日志消息写入磁盘的程序仅接收严重错误(error),而不存储那些警告(warnning)或者 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 开发笔记:图像识别基于主成分分析算法实现人脸二维码识别
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了图像识别基于主成分分析算法实现人脸二维码识别相关的知识,希望对你有一定的参考价值。 ... [详细]
  • Matlab 中的一些小技巧(2)
    1.Ctrl+D打开子程序  在MATLAB的Editor中,将输入光标放到一个子程序名称中间,然后按Ctrl+D可以打开该子函数的m文件。当然这个子程序要在路径列表中(或在当前工作路径中)。实际上 ... [详细]
  • 时域|波形_语音处理基于matlab GUI音频数据处理含Matlab源码 1734期
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了语音处理基于matlabGUI音频数据处理含Matlab源码1734期相关的知识,希望对你有一定的参考价值。 ... [详细]
  • keras归一化激活函数dropout
    激活函数:1.softmax函数在多分类中常用的激活函数,是基于逻辑回归的,常用在输出一层,将输出压缩在0~1之间,且保证所有元素和为1,表示输入值属于每个输出值的概率大小2、Si ... [详细]
author-avatar
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有