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

EasyPlayerPro基于FFMPEG实现播放同时进行录像的功能 之前有博客专门介绍了EasyPlayer的本地录像的功能,简单来说,EasyPlayer是一款RTSP播放器,它将RTSP流里的音视频媒体帧解析出来,并用安卓系统提供的MediaMuxer类进行录像.那EasyPlayerPro可以这样实现吗?答案是不太现实,因为Pro支持绝大多数的流媒体协议,并不单单是RTSP协议,包括hTTP\RTSP\RTMP\HLS\FILE等格式都支持.要将这些数据分别解 阅读详情

前言

在以前的博文中,我们通过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

为 Android 编译并集成 FFmpeg 的尝试与踩坑 前言与环境说明初步目标是使用 FFmpeg 实现 Android 内简单的视频剪辑、添加背景音乐、添加字幕等功能,由于本人初学 Android 开发,能力有限,基础薄弱,无法较为全面地深入学习过程中遇到的问题,文章中可能掺杂有一些知其然而不知其所以然的部分或一些不恰当不精确的个人理解,还请见谅。设备:macOS Big Sur 11.6 (Apple Silicon M1)FFmpeg 版本:4.4开发环境:目前 NDK 在苹果芯片下仍只能使用 Rosetta 2 转译后进行使用。 阅读详情

相关推荐

基于FFmpeg的简单Android视频播放器

1. 模块分割首先对这个视频播放器所采用的一些部件要清楚。这个播放器主要可以拆分为4个部分:1.解码:FFmpeg2.音频输出:OpenSLES3.视频渲染:OpenGLES这些框架都是基于C的api,因此这次我们的主要工作将会集中在NDK部分。而关于NDK的一些知识,之前的博客也有讲过,所以这个工程会是对之前知识的一次综合运用。为了提高可移植性,对关键部件使用接口来规范其API接口。对于播放器内部,播放的音频数据格式为16位PCM,44.1kHz采样率,双声道。

qq_52703909的博客 1298

FFmpeg实现Android音视频播放器【1】

这一步主要是将视频文件中的视频和音频数据进行拆分。首先获取AVFormatContext对象,通过AVFormatContext打开音视频数据源(.mp4文件或者音视频流)

weixin_40194067的博客 1233

解决Android平台移植ffmpeg的一揽子问题

IT行业是一个踩在巨人肩膀上前进的行业,否则做的事情不一定有意义,所以我也是基于havlenapetr移植的ffmpeg基础上做了些改进,他做的主要贡献有:1. 移植了ffmpeg并将与媒体相关的结构体在java层重新进行了封装,方便应用程序在java层直接操作ffmpeg API,如各种媒体格式转码及播放2. 模仿Android的MediaPlayer类实现了ffmpeg的播放接口,如setDataSource(),setDisplay(),start(), stop(),pause()等,缺点是没有实现

moruite的专栏 3万+

android 声音同步 测试,基于FFmpeg和Android的音视频同步播放实现

基于FFmpeg和Android的音视频同步播放实现发布时间:2018-06-23 22:16,浏览次数:575, 标签:FFmpegAndroid前言在以前的博文中,我们通过FFmpeg解码,并基于OpenGL ES完成了视频的渲染,也完成了基于OpenSL ES实现的native音频注入播放。本文将这两部分代码进行合并,并实现音视频的同步播放。实现需求* 基于FFmpeg实现视频解码,并通过O...

weixin_34168167的博客 489

FFmpeg 之音视频解码与音视频同步(二),flutterplugin开发

| 名称 | 机构 | 推出时间 | 使用领域 | | — | — | — | — | | AAC | MPEG | 1997 | 各个领域 | | AC-3 | Dolby | 1992 | 电影 | | MP3 | MPEG | 1993 | 早期普及 | | WMV | 微软 | 1999 | Windows | 音视频解码流程 解封装格式。将输入的按照一定格式封装的音视频数据,分离成为音频流压缩编码数据和视频流压缩编码数据。 解码。将视频和音频的压缩编码数据,解码成为非压缩的视频和音频原

m0_65145113的博客 863

Android中使用FFmpeg进行音视频处理

本文介绍了在Android中使用FFmpeg进行音视频处理的方法。通过调用FFmpeg命令行来实现各种音视频处理操作,如转码、剪辑等。希望本文能帮助你在Android应用开发中使用FFmpeg成功实现音视频处理。在Android应用开发中,有时我们需要对音视频进行处理,比如转码、剪辑、水印添加等操作。而FFmpeg是一个广泛使用的开源多媒体框架,它提供了丰富的功能和强大的性能。本文将介绍如何在Android中调用FFmpeg方法来进行音视频处理。注意:本文仅供参考,实际使用中请遵循FFmpeg的许可协议。

BUG?不存在的! 613

ffmpeg播放器 android,Android使用FFmpeg(六)--ffmpeg实现音视频同步播放

关于准备工作正文依旧依照流程图来逐步实现同步播放: 从流程图可以看出,实现同步播放需要三个线程,一个开启解码的装置得到packet线程,然后分别是播放音频和视频的线程。这篇简书是以音频播放为基准来进行播放,也就是音频一直不停的播放,视频根据音频播放来调整延迟时间。1.开启play线程,在这个线程中,注册组件,得到音视频的解码的装置并将packet压入队列。这里和前面的音视频分开播放并没有多大差别,...

weixin_39725193的博客 289

基于FFmpeg的Android播放器核心实现:解码、渲染与音视频同步实战

在移动端多媒体开发中,视频解码与渲染是核心环节,而音视频同步则直接决定播放体验的流畅度。FFmpeg作为业界领先的跨平台多媒体处理框架,凭借丰富的容器格式支持和强大的编解码能力,成为解决设备兼容性问题的关键工具。然而,在Android平台上深度集成FFmpeg并非易事,开发者需要同时处理好NDK交叉编译、JNI生命周期管理、多线程调度以及音视频同步等多重挑战。本文以一个完整的自研播放器项目为线索,从FFmpeg的裁剪编译方法、JNI层的稳健封装,到解码线程与帧缓冲设计、基于AudioClock的同步策略,再

weixin_42550597的博客 75

Android使用FFmpeg(六)--ffmpeg实现音视频同步播放

关于 Android使用FFmpeg(一)--编译ffmpeg Android使用FFmpeg(二)--Android Studio配置ffmpeg Android使用FFmpeg(三)--ffmpeg实现视频播放 Android使用FFmpeg(四)--ffmpeg实现音频播放(使用Au...

测试0901-1 936

【PyBind11+anaconda+opencv+windows11+cmake+wsl+vscode】从入门到跑通。构建基于opencv c++的python package

如何安装请详见上文 【PyBind11+anaconda+opencv+windows11+cmake+wsl+vscode】从入门到跑通。构建基于opencv c++的python接口_子韵如初的博客-优快云博客w不需要安装啥x11 、 xming直接mobxterm可以搞定点击右上角的X server打开可视化服务器然后 apt install x11-apps -yxclock 搞定从mobxterm可以看到display的端口X11显示可视化的原理是就是通过X协议,类似于http对linux于w

weixin_43953700的博客 3万+

Word多级标题出现黑块的解决思路(word 数字必须介于1-1638之间)

遇到多级目录出现黑块问题,看到一篇博客完美解决,在此记录。

AI_Optimization的博客 3716

Android FFmpeg系列——4 子线程播放音视频

利用工作闲余时间,终于实现在子线程播放音视频! 上一接学习了在 C 使用多线程,接着就是利用 C 多线程同时播放音视频(暂时还不同步)。 不多说,直接上码。 代码 // C 层播放器结构体 typedef struct _Player { // Env JavaVM *java_vm; // Java 实例 jobject instance; jobject...

JohanMan的博客 1914

20220601超简单百度地图街景图片爬取+绿视率计算

真的不难咯。

weixin_39676904的博客 1万+

android 端基于 FFmpeg 实现音频剪切、拼接、转码、混音、编解码;视频剪切、水印、截图、转码、编解码、转 Gif 动图;音视频合成与分离;音视频解码、同步与播放;FFmpeg 本地推流、H

FFmpegAndroid 项目地址:xufuji456/FFmpegAndroid  简介:android 端基于 FFmpeg 实现音频剪切、拼接、转码、混音、编解码;视频剪切、水印、截图、转码、编解码、转 Gif 动图;音视频合成与分离;音视频解码、同步与播放;FFmpeg 本地推流、H264 与 RTMP 实时推流直播;OpenGL+GPUImage 实时滤镜;FFmpeg 滤镜:素描...

未来很长,但我会努力的走下去。 1092

基于ESP32的AI 古琴:基于音频指纹的民族乐器智能校准系统设计与实现

硬件选型兼顾实用性与低成本,接线简单易实现;基于 MFCC 的音频指纹算法,能有效识别古琴弦的音高特征;人机交互友好,新手可快速上手。

weixin_42300449的博客 307

基于STM32F103C8T6的智能蓝牙手环系统设计与实现

摘要:本文设计并实现了一款基于STM32F103C8T6单片机的智能蓝牙手环系统。该系统集成了ADXL345加速度计、DS18B20温度传感器、plusesennor心率模块和BT06蓝牙模块,实现了心率(±2bpm)、体温(±0.5℃)、步数(±3%)等健康数据的实时监测与显示。通过OLED屏提供人机交互界面,支持蓝牙APP远程控制,具备自动步数清零、异常提醒等功能。测试表明系统各项功能均达到设计要求,响应时间0.1-0.5秒,心率检测准确率98%,体温检测准确率99%,功耗50mA(工作)/10mA(待

3254968421的博客 205

Android音视频学习系列(七) — 从0~1开发一款Android端播放器(支持多协议网络拉流本地文件)

现在一个 APP 玩的花样是越来越多了几乎都离不开音频、视频、图片等数据显示,该篇就介绍其中的音视频播放,音视频播放可以用已经成熟开源的播放器,(推荐一个不错的播放器开源项目如果用已开源的播放器就没有太大的学习意义了,该篇文章会介绍基于 FFmpeg 4.2.2 、Librtmp 库从 0~1 开发一款 Android 播放器的流程和实例代码编写。

金戈鐡馬 1585

STM32串口编程基础知识讲解

本篇文章给大家介绍一下串口的基础编程知识,这些知识是属于串口的理论知识,在开始学习编写代码的时候必须要掌握这些基本的知识点。本篇文章讲解了串口的基本概念知识,大家在学习编写代码时将其掌握才能更好的理解代码。

m0_49476241的博客 1208

Android使用ffmpeg+opengl+opensles实现播放器

终于到了第5章,前面已经学了音频和视频的渲染知识,这一章讲的是视频播放器的实现,记录一下看源码的一些要点。 初始化 1、同步模块整体初始化 初始化一些参数,minBufferedDuration为0.5f,maxBufferedDuration为1.0f。在调用initMeta()之后变成0.5和0.8,最大的音视频偏差设置为了0.05。 bool AVSynchronizer::in...

JabamiYu的博客 2944

redhawk:Low Power Analysis

1.rush current与switch cell 在standby状态下为了控制leakage power我们选择power gating的设计方式,使用power switch cell关闭block/power domain的电源。 power switch的基本介绍可见: 低功耗设计-Power Switch power switch的table中有四种状态,对应block工作、待机和上电,下电。low power analysis主要分析上电的过程。 上电过程可以看做mos电容

m0_61544122的博客 3206

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

上一篇: 基于FFmpeg和OpenSL ES的Android音频播放器实现
下一篇: 移植ffplay媒体播放器和sdl2到Android平台
ericbars
博客等级 码龄10年 120粉丝 30原创
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值