基于FFmpeg和Android的音视频同步播放实现

本文介绍了如何使用FFmpeg解码视频,并结合OpenGLES渲染,同时利用OpenSLES实现音频的PCM注入播放,最终实现音视频同步播放的效果。文章详细探讨了同步原理及其实现方法。

前言

在以前的博文中,我们通过FFmpeg解码,并基于OpenGL ES完成了视频的渲染,也完成了基于OpenSL ES实现的native音频注入播放。 本文将这两部分代码进行合并,并实现音视频的同步播放。

实现需求

  1. 基于FFmpeg实现视频解码,并通过OpenGL ES进行渲染;
  2. 基于OpenSL ES进行PCM注入播放;
  3. 播放时进行音视频同步;

关于音视频同步原理

本文不打算详细介绍音视频同步的基本原理,网上关于这部分的资源很多。简单的来说,是音视频在编码时,在音频和视频PES包中,打入时间戳信息(PTS),那么在终端解码时,由于音频解码和视频解码的速度可能不一致,如果不进行同步操作,可能声音和画面就不同步了,造成画面超前或者滞后于声音。
一般来说,声音的播放时固定采样率的,所以声音的播放本身是平滑的,因此我们往往基于音频播放的基准(PTS)来进行视频同步,让视频画面的播放速度来匹配音频的解码速度。

音频同步实现

由于参考了前述博文视频和音频播放,所以原理部分不再阐述,重点描述同步相关的代码实现。关注如下两个函数:

double get_audio_clock() {
    double pts;
    int hw_buf_size, bytes_per_sec, n;

    pts = audio_clock;

    bytes_per_sec = 0;
    n = global_context.acodec_ctx->channels * 2;
    bytes_per_sec = global_context.acodec_ctx->sample_rate * n;
    hw_buf_size = last_enqueue_buffer_size
            - (double(av_gettime() - last_enqueue_buffer_time) / 1000000.0)
                    * bytes_per_sec;
    if (bytes_per_sec) {
        pts -= (double) hw_buf_size / bytes_per_sec;
    }

    //LOGV2("get_audio_clock:pts is %ld", pts);
    return pts;
}
// this callback ha

ndler is called every time a buffer finishes playing
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
    SLresult result;

    //LOGV2("bqPlayerCallback...");

    if (bq != bqPlayerBufferQueue) {
        LOGV2("bqPlayerCallback : not the same player object.");
        return;
    }

    int decoded_size = audio_decode_frame(decoded_audio_buf,
            sizeof(decoded_audio_buf));
    if (decoded_size > 0) {
        result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue,
                decoded_audio_buf, decoded_size);
        last_enqueue_buffer_time = av_gettime();
        last_enqueue_buffer_size = decoded_size;
        // the most likely other result is SL_RESULT_BUFFER_INSUFFICIENT,
        // which for this code example would indicate a programming error
        if (SL_RESULT_SUCCESS != result) {
            LOGV2("bqPlayerCallback : bqPlayerBufferQueue Enqueue failure.");
        }
    }
}

我们知道bqPlayerCallback函数是注册给OpenSL ES的接口函数,当解码芯片音频数据消耗完毕时,会调用此函数,我们在这个函数里存储了两个变量,

last_enqueue_buffer_time = av_gettime();
last_enqueue_buffer_size = decoded_size;

其中,last_enqueue_buffer_time保存当前的系统时间,last_enqueue_buffer_size存储注入的数据大小。
接下来,函数get_audio_clock(),是获取音频时钟信息的,返回单位是秒,一般是浮点数,这里重点注意如下代码段,

pts = audio_clock;

    bytes_per_sec = 0;
    n = global_context.acodec_ctx->channels * 2;
    bytes_per_sec = global_context.acodec_ctx->sample_rate * n;
    hw_buf_size = last_enqueue_buffer_size
            - (double(av_gettime() - last_enqueue_buffer_time) / 1000000.0)
                    * bytes_per_sec;
    if (bytes_per_sec) {
        pts -= (double) hw_buf_size / bytes_per_sec;
    }

    return pts;

全局变量audio_clock保存的是最后一次解码音频帧的时钟(时间戳)信息,n表示每声道存储的字节数,这里是16bit格式,所以乘以2。bytes_per_sec表示每秒钟消耗的音频字节数,根据采样率和n计算得到。hw_buf_size表示芯片音频缓存区里剩余未解码的音频数据大小,这里依赖保存的变量last_enqueue_buffer_size和last_enqueue_buffer_time计算得到,很好理解,最后就可以计算得到当前的音频时钟pts并返回。

视频同步实现

视频的同步相对复杂一点,首先建立了两个线程,一个负责解码即video_thread线程,一个负责视频渲染即picture_thread线程,解码后的视频帧(即picture)存储在一个队列中,我们定义成,
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];
放在GlobalContext全局上下文中。
视频渲染线程picture_thread中,每一帧视频渲染的时间点,是由timer_delay_ms延时时间决定的,而每一帧视频的timer_delay_ms延时时间的计算主要由如下函数计算得到,

void video_refresh_timer() {
    VideoPicture *vp;
    double actual_delay, delay, sync_threshold, ref_clock, diff;

    if (global_context.pictq_size == 0) {
        schedule_refresh(1);
    } else {
        vp = &global_context.pictq[global_context.pictq_rindex];

        video_current_pts = vp->pts;
        global_context.video_current_pts_time = av_gettime();

        delay = vp->pts - global_context.frame_last_pts;

        if (delay <= 0 || delay >= 1.0) { // 非法值判断
            delay = global_context.frame_last_delay;
        }

        global_context.frame_last_delay = delay;
        global_context.frame_last_pts = vp->pts;

        ref_clock = get_master_clock();
        diff = vp->pts - ref_clock;

        sync_threshold =
                (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
        if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
            if (diff <= -sync_threshold) {
                //av_log(NULL, AV_LOG_ERROR, "video_refresh_timer : skip. \n");
                LOGV("video_refresh_timer : skip. \n");
                delay = 0;
            } else if (diff >= sync_threshold) {
                //av_log(NULL, AV_LOG_ERROR, "video_refresh_timer : repeat. \n");
                LOGV("video_refresh_timer : repeat. \n");
                delay = 2 * delay;
            }
        } else {
            //av_log(NULL, AV_LOG_ERROR,
            //      " video_refresh_timer : diff > 10 , diff = %f, vp->pts = %f , ref_clock = %f\n",
            //      diff, vp->pts, ref_clock);
            LOGV(
                    " video_refresh_timer : diff > 10 , diff = %f, vp->pts = %f , ref_clock = %f\n",
                    diff, vp->pts, ref_clock);
        }

        global_context.frame_timer += delay;

        actual_delay = global_context.frame_timer - (av_gettime() / 1000000.0);
        if (actual_delay < 0.010) {    //每秒100帧的刷新率不存在

            actual_delay = 0.010;
        }
        schedule_refresh((int) (actual_delay * 1000 + 0.5)); //add 0.5 for 进位
        if (vp->pFrame)
            video_display(vp->pFrame);

        if (++global_context.pictq_rindex >= VIDEO_PICTURE_QUEUE_SIZE) {
            global_context.pictq_rindex = 0;
        }

        pthread_mutex_lock(&global_context.pictq_mutex);
        global_context.pictq_size--;
        pthread_cond_signal(&global_context.pictq_cond);
        pthread_mutex_unlock(&global_context.pictq_mutex);
    }
}

其中,get_master_clock()函数获取系统时钟,我们这里一般配置成音频的解码时间戳(PTS),然后计算视频时间戳和参考时间的差值,

ref_clock = get_master_clock();
diff = vp->pts - ref_clock;

以下代码判断差值的大小,如果小于门限值,则不延时,也就是说要迅速播放下一帧,类似于快进,如果大于门限值,则延时加倍,也就是说要慢点播放下一帧,当然可能一帧并不能马上达到音视频之间的同步,但是可以通过多帧的累积,最终使两者同步。

if (diff <= -sync_threshold) {
    //av_log(NULL, AV_LOG_ERROR, "video_refresh_timer : skip. \n");
    LOGV("video_refresh_timer : skip. \n");
    delay = 0;
} else if (diff >= sync_threshold) {
    //av_log(NULL, AV_LOG_ERROR, "video_refresh_timer : repeat. \n");
    LOGV("video_refresh_timer : repeat. \n");
    delay = 2 * delay;
}

注意video_refresh_timer()函数中,还有一个actual_delay变量,奇怪的是,delay变量就行了,为何还有个actual_delay变量呢?原来,我们的程序在处理视频数据或者执行时,总要消耗时间,因此实际的delay时间总是要小于上述计算得到的delay值,actual_delay的计算如下,frame_timer是记录的上一次帧显示的系统时间加上了delay的值,av_gettime()是当前的系统时间,两者的差值刚好是下一帧图像显示的延时时间。

actual_delay = global_context.frame_timer - (av_gettime() / 1000000.0);

下面函数完成一帧图像的显示,这和以前博文介绍的实现方法一致。

video_display(vp->pFrame);

几个时间变量

关于代码中出现的几个关于时间的变量,分别解释如下:

pFrame->pkt_pts
这个是码流里存储的PTS值,就是一个计数器,相对于时基的一个计数值

av_q2d(global_context.vstream->time_base)
把分数的时基,转成浮点数(double)型的时间基准,就是最小分辨率时间段吧

pFrame->pkt_pts*av_q2d(global_context.vstream->time_base)
这是把码流里的PTS计数值,转换成时间戳的形式,单位是秒(如0.0001秒)

av_gettime
获取当前系统时间,单位微秒us

vp->pts = pFrame->pkt_pts*av_q2d(global_context.vstream->time_base)
当前要显示的帧的时间戳,单位是秒(如0.0001秒),是 倍数*(1/时基)

video_current_pts
保留最后刷新的帧的vp->pts值

global_context.video_current_pts_time
当前显示帧时的系统时间,单位微秒


global_context.frame_last_pts
看起来和video_current_pts一个意思?最后刷新的帧的vp->pts值


global_context.frame_last_delay
记录最后的延时时间,用于下次PTS非法或者跳变时,采用上一次的值

global_context.frame_timer
用于帧的定时器时间计算,单位微秒

delay = vp->pts - global_context.frame_last_pts

get_master_clock()
返回系统的pts时间戳,是 倍数*(1/时基)

delay = vp->pts - global_context.frame_last_pts
两帧之间的时间戳差值

diff = vp->pts - ref_clock
当前帧和系统时钟之间的差值

global_context.frame_timer
应该是指每一帧视频的显示时刻,单位是秒,记住程序open媒体的时候有一个初始值

遗留问题

  1. 本程序在解码avi等视频格式时,无法播出图像,应该是avcodec_decode_video2解码后的数据是分段的,并不能一次完整解出;
  2. 在我的测试机器上,宏定义VIDEO_PICTURE_QUEUE_SIZE取值不同时,视频可能会出现跳帧卡顿的现象,调整到30帧时,表现最好,具体原因没有查到。

GitHub源码

请参考完整的源码路径:
https://github.com/ericbars/FFmpegAVSync

<think>好的,我现在需要分析用户提出的关于蓝牙HCI日志中出现的“BT_HCI btu_free_timer Unable to find expected alarm in hashmap”错误的原因及解决方法。首先,我应该理解这个错误信息的具体含义。 根据错误信息,系统在释放定时器(timer)时,无法在哈希表中找到预期的警报(alarm)。这可能意味着定时器在释放前已经被释放,或者定时器未被正确添加到哈希表中。接下来,我需要结合用户提供的引用内容来分析可能的代码路径。 用户提供的引用[^1]中提到了调用链:bluetoothHCLibInterface的preload方法触发事件HC_EVENT_PRELOAD,进而调用bt_hc_worker_thread,然后通过userial_open打开串口,最终调用vendor接口的userial_open函数。这说明在初始化过程中,涉及到定时器的创建管理。 接下来,可能的原因有几个方面。首先是定时器管理中的竞争条件,比如在多线程环境中,定时器被释放后又被访问,导致哈希表不一致。其次,定时器可能被重复释放,例如在某个回调函数中错误地多次调用释放函数。另外,哈希表本身的操作错误,比如添加或删除时未正确加锁,导致数据不一致。 解决方法方面,需要检查定时器的生命周期管理,确保每次创建后正确添加到哈希表,释放时正确移除。同时,检查多线程同步机制,确保哈希表操作的原子性。另外,查看日志中是否有重复释放的迹象,或者定时器ID冲突的情况。可能还需要使用调试工具跟踪定时器的添加移除过程,确认是否有遗漏或错误。 此外,用户提到的btu_free_timerbtu_free_quick_timer可能涉及不同的定时器类型,需要分别检查它们的处理流程。例如,快速定时器可能有不同的管理机制,或者更短的超时时间导致更频繁的触发释放。 总结来说,这个错误通常与定时器管理的逻辑错误或并发问题有关,需要通过代码审查日志分析来定位具体原因,并采取相应的同步或修正措施。</think>### BT_HCI "btu_free_timer Unable to find expected alarm in hashmap" 原因及解决方法 #### 错误原因分析 1. **定时器生命周期管理异常** 当系统尝试释放已标记为`BTU_FREE_TIMER`的定时器时,哈希表中未找到对应条目。可能原因包括: - 定时器被重复释放(如回调函数中多次调用`btu_free_timer`) - 定时器未被正确添加到哈希表(如初始化时未调用`alarm_set`或`alarm_new`) - 多线程竞争导致哈希表状态不一致(如线程A释放定时器后,线程B仍尝试操作) 2. **蓝牙协议栈与HCI层交互问题** 根据引用[^1]的调用链`bluetoothHCLibInterface->bthc_signal_event->bt_hc_worker_thread`,在HCI预加载阶段可能发生: - 硬件抽象层(HAL)与协议栈的异步通信不同步 - `userial_open`操作未完成时尝试操作定时器 ```c // 示例代码片段(模拟定时器操作) void btu_free_timer(alarm_t* alarm) { if (!hashmap_has_key(timer_map, alarm)) { // 触发错误的位置 LOG_ERROR("Unable to find expected alarm"); } } ``` 3. **内存管理缺陷** - 野指针问题:定时器指针被释放后未置空 - 内存越界:其他模块的内存错误污染了定时器管理区 #### 解决步骤 1. **日志增强分析** - 在`btu_free_timer`前后添加调试日志,记录哈希表状态: ```c LOG_DEBUG("Attempting to free timer:%p, current hashmap size:%d", alarm, hashmap_size(timer_map)); ``` - 检查`btu_start_timer``btu_stop_timer`的调用频率是否匹配 2. **线程同步加固** - 对哈希表操作添加互斥锁: ```c pthread_mutex_lock(&timer_mutex); hashmap_remove(timer_map, alarm); pthread_mutex_unlock(&timer_mutex); ``` 3. **代码审查重点** - 验证`bt_vnd_if->op(BT_VND_OP_USERIAL_OPEN)`的返回状态 - 检查`btu_task`与`bt_hc_worker_thread`的交互时序 - 确认`alarm_new()`是否在所有定时器创建路径中被正确调用 4. **复现与调试** - 使用Valgrind检测内存问题 - 在`userial_vendor_open`失败场景下测试定时器管理 #### 典型修复案例 某厂商在实现`btu_free_quick_timer`时未处理`USERIAL_PORT_1`异常关闭的情况,导致定时器未从哈希表移除。修复方案: ```diff + pthread_mutex_lock(&timer_mutex); hashmap_remove(timer_map, alarm); + pthread_mutex_unlock(&timer_mutex); - userial_vendor_close(); + userial_vendor_close_cleanup(); // 新增清理函数 ```
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值