基于 trinity 进行二次开发,目前除trinity已有的剪辑功能外,做了以下工作:
- 抽帧预览
- 逐帧seek,播放进度拖拽
- 添加文字贴纸功能 sticker.cc
- 文字的移动,旋转,缩放,调时间轴,删除和添加 video_editor.cc image_process.cc
- 支持素材的裁剪,旋转,调速
- 做音视频同步 player.cc
- 做音频变速
- 做素材转场 transition.cc
- extra 共有头文件及so资源包
- app 为demo迭代
- core cpp/java库核心代码,绝大部分是c++代码
整体需求大致分为 3 部分
- 包含解码相关但不限于,视频源加载/信息获取/帧预览/进度条管理
- 包含编码相关但不限于,素材管理,以json组织传输;视频合成
- feature 功能的开发,面向时间线叠加渲染
-
调整素材
-
文字贴纸
-
特效
-
美颜滤镜
-
音乐出来
-
- common 存放共有逻辑,比如 json 共有结构,共有 view 等
- input 视频加载,开放帧获取,信息获取,进度条等,对应需求 "1"
- output 视频输出,对应需求 "2"
- feature,各功能点代码严格分开
- effect 特效所有代码
- filter 滤镜
- music 音乐处理
- subtitle 字幕
- .... 自行添加
软硬解在jni层的不同处理?
- 可从上下文判断软硬模式
- 软解视频使用 YuvRender 渲染器,
- 硬解则基于 OpenGl封装 FrameBuffer从缓存管道中读取
- 上述2/3 获取到纹理之后交给 ImageProcess 处理器处理,改处理器会读取所有 feature 特征,针对每个特征进行画帧
- 最后在把纹理绘制到屏幕上。
Editor 扮演什么角色?
- 包含播放功能,同时用户管理feature提供的资源,把每一个媒体当作clip进行加载
- 每一个 feature 通过业务封装成 Json 格式传递给 jni 层,对应Jni中 editor_resource_
- editor_resource_ 把每一个配置 json 写入保存到文件中
Clip 扮演什么角色 ?
- 我们支持多个媒体资源拼接插入,每一个媒体资源(图片/视频)对应 java/c++ 层的同名 MediaClip
- 目前仅仅支持 JPEG 格式/ PNG 格式/mp4 格式
JNI层使用什么手段通讯?
- 类 Android 层的 Handler 机制,使用 Message+MessageQueue 队列实现,可查看 message 包
- MessageNode为节点信息,next指针支持链表接口,Message 为真正消息,类Android Message
- handler#postMessage 为发送信息,handler#handleMessage 为处理信息
- messageQueue 包含进队出队中止等实现
关于缓存队列的管理 Frame 帧数据 FramePool 帧缓存池,用于缓存 Frame数据,记录个数
- PacketPool *packet_pool;
- FramePool *audio_frame_pool;保存所有已解码音频帧
- FramePool *video_frame_pool; 保存所有已解码视频帧 FrameQueue,帧队列
- FrameQueue *audio_frame_queue; 存放大于当前播放位置的已解码音频帧
- FrameQueue *video_frame_queue; 存放大于当前播放位置的已解码视频帧 PacketQueue,压缩包队列
- PacketQueue *video_packet_queue;
- PacketQueue* audio_packet_queue;
关于解码线程 音频解码 audio_decode_thread 视频硬解 video_decode_hw_thread 视频软解 video_decode_sw_thread
播放流程 av_play_play
- avformat_alloc_context 获取 AVFormatContext 上下文
- av_find_best_stream 获取视频 steam 流
- av_find_best_stream 获取音频stream 流
- 如果存在音频,则 avcodec_open2 初始化音频解码器
- 如果存在视频,则获取 AVCodecParameters 判断解码器类型,初始化解码器(软/硬)
- 设置音频视频的 pack 队列信息,包括 buffer 时间,queue 时长,空队列回调,满队列回调
- 启动读取 steam 流线程 read_thread
- 启动视频/音频解码线程 video_decode_sw_thread/video_decode_hw_thread/audio_decode_thread
- 设置播放状态
音频解码做了什么 audio_decode_thread
- 获取音频 filter 上下文
- 从 audio_frame_pool 获取音频帧
- 当不是中止状态时进入循环
- 如果是暂停态,则休眠 100 ms
- 接收音频解码器返回的帧数据,保存在音频帧中
- 如果成功获取到音频解码帧,如果小于 seek_to 则在 audio_frame_pool 丢掉,否则则进入 audio_frame_queue 队列。跳过
- 如果读取失败或结束了,则从 audio_packet_queue 获取数据。
- 如果 audio_packet_queue 没有数据 且 文件已经到头了,则清除上下文,跳过
- 如果有数据且等于 flush_packet,清空 audio_frame_queue,刷新buffer,跳过
- 把 packet 发送给解码器解码,释放 packet
视频软解做了什么 video_decode_sw_thread
- 获取播放上下文
- 从 video_frame_pool 获取视频帧
- 当不是中止状态时进入循环
- 如果只播放音频,则把视频 video_packet_queue 都丢掉,保证音视频同步,跳过
- 接收视频解码器返回的帧数据,保存在视频帧中
- 如果成功获取到视频解码帧,则进 video_frame_queue 队列,休眠 2000 微妙, video_frame_pool index 往后移动一位
- 如果读取失败或结束了,则从 video_packet_queue 获取数据。
- 如果 video_packet_queue 没有数据 且 文件已经到头了,则清除上下文,跳过
- 如果有数据且等于 flush_packet,清空 video_packet_queue,刷新buffer,跳过
- 把 packet 发送给解码器解码,释放 packet
视频硬解做了什么 video_decode_hw_thread
- 获取播放上下文
- 从 video_frame_pool 获取视频帧
- 当不是中止状态时进入循环
- 如果只播放音频,则把视频 video_packet_queue 都丢掉,保证音视频同步,跳过
- 接收视频解码器返回的帧数据,保存在视频帧中
- 如果已经读取到尾部,则跳过
- 如果已经读区到数据
- 如果支持精准 seek模式,当前帧时间小于 seek_to 时间,则释放 buffer,否则 video_frame_queue 进队之后只显示一针,状态调整为 SEEK_COMPLETE
- 如果不是精准 seek模式,当 seek_to 大于当前帧时间是,每隔 100 ms 显示一帧,video_frame_queue 进队之后,状态调整为 SEEK_COMPLETE
- 如果成功获取到视频解码帧,则进 video_frame_queue 队列,休眠 2000 微妙, video_frame_pool index 往后移动一位
- 如果读取失败或结束了,则从 video_packet_queue 获取数据。
- 如果 video_packet_queue 没有数据 且 文件已经到头了,则清除上下文,跳过
- 如果有数据且是 seek 场景,清空 video_packet_queue,刷新buffer,跳过
- 把 packet 发送给解码器解码,释放 packet
- 获取 video_packet_queue 数据
- 如果文件已经到尾部了,则设置 end_of_stream = true;否则则休眠 10 ms,跳过
- 如果有数据且等于 flush_packet,则跳过
- 把 packet 发送给解码器解码,释放 packet
异步读文件数据 read_stream_thread
- 获取播放上下文
- 当不是中止状态时进入循环
- 如果当前 seeking == 1,清除音视频队列信息及缓存,把解码器 seek ,设置 seek == 2,利用 av_seek_frame seek 到 seek_to 位置
- 如果 audio_packet_queue + video_packet_queue 大小大于 buffers ,则跳过
- 从 packet_pool 获取一个 packet 数据
- 从 packet 中读取 frame 数据
- 如果成功读取,包含视频流,则放入 video_packet_queue ,包含音频流,则 放入 audio_packet_queue
- 如果数据流不可用,则清空 packet_pool
- 如果文件结束了,则设置 eof = true
音视频同步算法逻辑
/**
* 音视频同步逻辑
* diff 为当前队列最后面的帧的pts减去主时钟
* 根据 RFC-1359 国际规范,音频和视频的时间戳差值在 -100ms ~ +25ms 中间用户无法感知
* 所以根据这个差值来实现音视频同步。
* min 为同步阈值
* max为异常阈值
*/
int64_t diff = videoPts - masterClock;
int64_t min = 25000;
int64_t max = 1000000;
if(diff > -min && diff < min){
startAudioRender();
CreateRenderFrameBuffer();
RenderFrameBuffer();
clock_set(av_play_context_->video_clock, videoPts);
LOGI("DrawVideoFrame, 正常播放[-25ms,25ms]");
}else if(diff >= min && diff < max){
LOGI("DrawVideoFrame, 视频帧提前[ %lld ms ],通过 sleep [ %lld ms]来等待主时钟",diff / 1000,(diff - min) / 1000);
usleep(static_cast<useconds_t>(diff - min));
startAudioRender();
CreateRenderFrameBuffer();
RenderFrameBuffer();
clock_set(av_play_context_->video_clock, videoPts - (diff - min));
}else{
if(diff > 0){
LOGI("DrawVideoFrame, 视频帧提前超过[ 100 ms ],丢帧");
}else{
LOGI("DrawVideoFrame, 视频帧滞后超过[ 25 ms ],丢帧");
}
ReleaseVideoFrame();
}