# android_ffmpeg_opengl_ndk **Repository Path**: jsjxsy/android_ffmpeg_opengl_ndk ## Basic Information - **Project Name**: android_ffmpeg_opengl_ndk - **Description**: ffmpeg+opengl+ndk实现Android平台的播放器 - **Primary Language**: Unknown - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2024-08-09 - **Last Updated**: 2024-12-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: ffmpeg, Opengl, NDK ## README ## TestPlayer #### 实现效果如下,代码仅供参考学习,要稳定建议使用ijkplayer - 可下载看看 [演示apk](https://github.com/DMings/TestPlayer/blob/master/demosrc/app-debug.apk) ![视频seek旋转](https://github.com/DMings/TestPlayer/blob/master/demosrc/seek_rotation.gif) ![视频切换](https://github.com/DMings/TestPlayer/blob/master/demosrc/change_play.gif) #### 以下为实现的大概流程,主要分三条线程 ##### 主AVPacket取包分发线程 avformat_open_input //打开文件输入 avformat_find_stream_info // 获取音频/视频流信息 初始化音频/视频信息并创建相应的线程,进入循环解码 av_seek_frame // 先判断是否需要seek avformat_flush //seek后需要清除缓存 av_read_frame // 读取一个AVPacket 获取AVPacket类型,av_packet_clone复制一份并放进队列进行分发,分发给 视频解码线程或者音频解码线程进行消耗 ##### Audio解帧播放线程 av_find_best_stream // 获取音频类型下最好的流 avcodec_find_decoder // 获取解码器 avcodec_alloc_context3 // 分配流的context内存 avcodec_parameters_to_context // 设置流context参数 avcodec_open2 // 打开 创建opensl 同步方式创建,输入数据方式为设置回调 RegisterCallback(bqPlayerBufferQueue, slBufferCallback, this); 并设置声道,采样位格式如无符号16位,声音采集频率如44100 av_samples_alloc / av_samples_alloc2 // 根据音频参数分配转换buffer。这里出于降低音频的延迟,采用双缓冲音频buffer,意味着一个数组数据在播放的同时在准备另外一个数组数据,两数组交替切换,互不干扰 swr_alloc_set_opts // 设置转换参数 swr_init // 初始化音频转换器 创建解码audio线程 AttachCurrentThread // 连接JNIEnv环境,用于native线程中更新java层显示时间 启动循环,从音频AVPacket获取一个包 avcodec_send_packet //从包中解码,一个包可能包含多帧 avcodec_receive_frame // 解码包,得到帧 AVFrame,该帧包含音频数据,通常是PCM 如果是seek过的,先校准时间,防止后面的延时计算错误 判断是否需要seek,是就跳过下面 更新java层显示的播放时间 swr_convert // 转换音频buffer,由于AVFrame中的数据不一定是opensl中设置的格式,这里需要转换一下 等待opensl播放结束回调,回调到达后设置音频数据继续播放。(此处音频并没有同步播放的时间戳,而是将音频的PTS作为播放的时间。这是因为我在测试的时候swr_convert转换不了音频采样率, 实现不了音频通过改变采样率来达到同步播放时间戳的效果,我认为引入SoundTouch可以解决,这里就没实现了) 循环结束,此时为音频播放结束或者遇到错误结束,释放资源,停止opensl 播放回调 ##### video解帧显示线程 创建OpenGL环境,设置显示大小,大小变化跟随 surface的回调而改变(视频切换的时候并不需要改变环境,切换视频只需要改变纹理尺寸) void surface_created(Surface surface); void surface_changed(Surface surface, int width, int height); void surface_destroyed(); 视频初始化 av_find_best_stream // 获取视频类型下最好的流 avcodec_find_decoder // 获取解码器 avcodec_alloc_context3 // 分配流的context内存 avcodec_parameters_to_context // 设置流context参数 avcodec_open2 // 打开 sws_getContext // 获取视频转换swscontext av_image_alloc // 根据视频参数分配转换buffer,该buffer用于传递给GL纹理 创建解码video线程 AttachCurrentThread // 连接JNIEnv环境,用于native线程中更新java层显示时间 设置GL纹理尺寸,数据buffer 启动循环,从视频AVPacket队列获取一个包 avcodec_send_packet //从包中解码,一个包可能包含多帧 avcodec_receive_frame // 解码包,得到帧 AVFrame,该帧包含视频数据,通常是YUV 如果是seek过的,先校准时间,防止后面的延时计算错误 判断是否需要seek,如果是seek,后面就不必执行了,跳过这帧显示 sws_scale // 把AVFrame中的数据转换为RGBA buffer 如果有音频的情况下,应该由音频线程更新java时间(用音频时间显示), 如果没有音频流,就让视频更新java层显示时间(用视频时间显示) 视频同步音频(如果音频存在),音频不存在就直接计算视频延时,进行sleep操作 RGBA buffer转gl纹理并绘制显示 循环结束,此时为视频播放结束或者遇到错误结束,释放资源,绘制黑色背景 同步算法 ``` uint Video::synchronize_video(double pkt_duration) { // us uint wanted_delay = 0; double diff_ms; double duration; bool is_seeking; AVRational rational = av_stream->r_frame_rate; int video_base = (int) 1000.0 * rational.den / rational.num; if (video_base) { duration = video_base; } else { duration = pkt_duration; } duration = duration < 200 ? duration : 200; pthread_mutex_lock(&seek_mutex); is_seeking = audio_seeking; pthread_mutex_unlock(&seek_mutex); if (!is_seeking) { if (has_audio) { diff_ms = get_video_pts_clock() - get_audio_clock(); } else { diff_ms = get_video_pts_clock() - get_master_clock(); } } else { // 在seek的时候,音频是不可靠的,音频也在变,这时候尽量丢弃,反正都是没用的 diff_ms = duration * 0.3; } if (!has_audio && fabs(diff_ms) >= duration * 2) { // 当只有视频的时候,已经明显失去了同步,校准一下 double time = av_gettime_relative() / 1000.0; set_master_clock(time - get_video_pts_clock()); return (uint) (duration * 0.7); } if (diff_ms < 0) { // 视频落后时间 if (fabs(diff_ms) < duration * 0.1) { // 如果差距比较少就不管了 wanted_delay = 0; } else { wanted_delay = (uint) (duration * 0.1); // 减少时间,尽量追上 } } else { // 视频超过时间 if (fabs(diff_ms) < duration * 0.15) { // 如果差距比较少就不管了 wanted_delay = 0; } else { // 增加时间,尽量等待时间追上 if (diff_ms < duration * 0.8) { wanted_delay = (uint) (diff_ms * 0.8); } else { wanted_delay = (uint) (duration * 0.8); // 最大不超过,不然卡顿非常明显 } } } return wanted_delay; } ```