Skip to content

meiliqin/GoldenVideoClip

Folders and files

NameName
Last commit message
Last commit date

Latest commit

author
梅立琴
Feb 5, 2021
a1a5e2a · Feb 5, 2021

History

2 Commits
Jan 28, 2021
Jan 28, 2021
Jan 28, 2021
Jan 28, 2021
Jan 28, 2021
Jan 28, 2021
Jan 28, 2021
Jan 28, 2021
Jan 28, 2021
Feb 5, 2021
Jan 28, 2021
Jan 28, 2021

Repository files navigation

基于 trinity 进行二次开发,目前除trinity已有的剪辑功能外,做了以下工作:

  • 抽帧预览
  • 逐帧seek,播放进度拖拽
  • 添加文字贴纸功能 sticker.cc
  • 文字的移动,旋转,缩放,调时间轴,删除和添加 video_editor.cc image_process.cc
  • 支持素材的裁剪,旋转,调速
  • 做音视频同步 player.cc
  • 做音频变速
  • 做素材转场 transition.cc

项目结构

  • extra 共有头文件及so资源包
  • app 为demo迭代
  • core cpp/java库核心代码,绝大部分是c++代码

需求

整体需求大致分为 3 部分

  1. 包含解码相关但不限于,视频源加载/信息获取/帧预览/进度条管理
  2. 包含编码相关但不限于,素材管理,以json组织传输;视频合成
  3. feature 功能的开发,面向时间线叠加渲染
    1. 调整素材

    2. 文字贴纸

    3. 特效

    4. 美颜滤镜

    5. 音乐出来

DEMO 目录

  1. common 存放共有逻辑,比如 json 共有结构,共有 view 等
  2. input 视频加载,开放帧获取,信息获取,进度条等,对应需求 "1"
  3. output 视频输出,对应需求 "2"
  4. feature,各功能点代码严格分开
    1. effect 特效所有代码
    2. filter 滤镜
    3. music 音乐处理
    4. subtitle 字幕
    5. .... 自行添加

部分源码解读:

软硬解在jni层的不同处理?

  1. 可从上下文判断软硬模式
  2. 软解视频使用 YuvRender 渲染器,
  3. 硬解则基于 OpenGl封装 FrameBuffer从缓存管道中读取
  4. 上述2/3 获取到纹理之后交给 ImageProcess 处理器处理,改处理器会读取所有 feature 特征,针对每个特征进行画帧
  5. 最后在把纹理绘制到屏幕上。

Editor 扮演什么角色?

  1. 包含播放功能,同时用户管理feature提供的资源,把每一个媒体当作clip进行加载
  2. 每一个 feature 通过业务封装成 Json 格式传递给 jni 层,对应Jni中 editor_resource_
  3. editor_resource_ 把每一个配置 json 写入保存到文件中

Clip 扮演什么角色 ?

  1. 我们支持多个媒体资源拼接插入,每一个媒体资源(图片/视频)对应 java/c++ 层的同名 MediaClip
  2. 目前仅仅支持 JPEG 格式/ PNG 格式/mp4 格式

JNI层使用什么手段通讯?

  1. 类 Android 层的 Handler 机制,使用 Message+MessageQueue 队列实现,可查看 message 包
  2. MessageNode为节点信息,next指针支持链表接口,Message 为真正消息,类Android Message
  3. handler#postMessage 为发送信息,handler#handleMessage 为处理信息
  4. messageQueue 包含进队出队中止等实现

关于缓存队列的管理 Frame 帧数据 FramePool 帧缓存池,用于缓存 Frame数据,记录个数

  1. PacketPool *packet_pool;
  2. FramePool *audio_frame_pool;保存所有已解码音频帧
  3. FramePool *video_frame_pool; 保存所有已解码视频帧 FrameQueue,帧队列
  4. FrameQueue *audio_frame_queue; 存放大于当前播放位置的已解码音频帧
  5. FrameQueue *video_frame_queue; 存放大于当前播放位置的已解码视频帧 PacketQueue,压缩包队列
  6. PacketQueue *video_packet_queue;
  7. PacketQueue* audio_packet_queue;

关于解码线程 音频解码 audio_decode_thread 视频硬解 video_decode_hw_thread 视频软解 video_decode_sw_thread

播放流程 av_play_play

  1. avformat_alloc_context 获取 AVFormatContext 上下文
  2. av_find_best_stream 获取视频 steam 流
  3. av_find_best_stream 获取音频stream 流
  4. 如果存在音频,则 avcodec_open2 初始化音频解码器
  5. 如果存在视频,则获取 AVCodecParameters 判断解码器类型,初始化解码器(软/硬)
  6. 设置音频视频的 pack 队列信息,包括 buffer 时间,queue 时长,空队列回调,满队列回调
  7. 启动读取 steam 流线程 read_thread
  8. 启动视频/音频解码线程 video_decode_sw_thread/video_decode_hw_thread/audio_decode_thread
  9. 设置播放状态

音频解码做了什么 audio_decode_thread

  1. 获取音频 filter 上下文
  2. 从 audio_frame_pool 获取音频帧
  3. 当不是中止状态时进入循环
    1. 如果是暂停态,则休眠 100 ms
    2. 接收音频解码器返回的帧数据,保存在音频帧中
    3. 如果成功获取到音频解码帧,如果小于 seek_to 则在 audio_frame_pool 丢掉,否则则进入 audio_frame_queue 队列。跳过
    4. 如果读取失败或结束了,则从 audio_packet_queue 获取数据。
      1. 如果 audio_packet_queue 没有数据 且 文件已经到头了,则清除上下文,跳过
      2. 如果有数据且等于 flush_packet,清空 audio_frame_queue,刷新buffer,跳过
      3. 把 packet 发送给解码器解码,释放 packet

视频软解做了什么 video_decode_sw_thread

  1. 获取播放上下文
  2. 从 video_frame_pool 获取视频帧
  3. 当不是中止状态时进入循环
    1. 如果只播放音频,则把视频 video_packet_queue 都丢掉,保证音视频同步,跳过
    2. 接收视频解码器返回的帧数据,保存在视频帧中
    3. 如果成功获取到视频解码帧,则进 video_frame_queue 队列,休眠 2000 微妙, video_frame_pool index 往后移动一位
    4. 如果读取失败或结束了,则从 video_packet_queue 获取数据。
      1. 如果 video_packet_queue 没有数据 且 文件已经到头了,则清除上下文,跳过
      2. 如果有数据且等于 flush_packet,清空 video_packet_queue,刷新buffer,跳过
      3. 把 packet 发送给解码器解码,释放 packet

视频硬解做了什么 video_decode_hw_thread

  1. 获取播放上下文
  2. 从 video_frame_pool 获取视频帧
  3. 当不是中止状态时进入循环
    1. 如果只播放音频,则把视频 video_packet_queue 都丢掉,保证音视频同步,跳过
    2. 接收视频解码器返回的帧数据,保存在视频帧中
    3. 如果已经读取到尾部,则跳过
    4. 如果已经读区到数据
      1. 如果支持精准 seek模式,当前帧时间小于 seek_to 时间,则释放 buffer,否则 video_frame_queue 进队之后只显示一针,状态调整为 SEEK_COMPLETE
      2. 如果不是精准 seek模式,当 seek_to 大于当前帧时间是,每隔 100 ms 显示一帧,video_frame_queue 进队之后,状态调整为 SEEK_COMPLETE
    5. 如果成功获取到视频解码帧,则进 video_frame_queue 队列,休眠 2000 微妙, video_frame_pool index 往后移动一位
    6. 如果读取失败或结束了,则从 video_packet_queue 获取数据。
      1. 如果 video_packet_queue 没有数据 且 文件已经到头了,则清除上下文,跳过
      2. 如果有数据且是 seek 场景,清空 video_packet_queue,刷新buffer,跳过
      3. 把 packet 发送给解码器解码,释放 packet
    7. 获取 video_packet_queue 数据
      1. 如果文件已经到尾部了,则设置 end_of_stream = true;否则则休眠 10 ms,跳过
      2. 如果有数据且等于 flush_packet,则跳过
      3. 把 packet 发送给解码器解码,释放 packet

异步读文件数据 read_stream_thread

  1. 获取播放上下文
  2. 当不是中止状态时进入循环
    1. 如果当前 seeking == 1,清除音视频队列信息及缓存,把解码器 seek ,设置 seek == 2,利用 av_seek_frame seek 到 seek_to 位置
    2. 如果 audio_packet_queue + video_packet_queue 大小大于 buffers ,则跳过
    3. 从 packet_pool 获取一个 packet 数据
    4. 从 packet 中读取 frame 数据
      1. 如果成功读取,包含视频流,则放入 video_packet_queue ,包含音频流,则 放入 audio_packet_queue
      2. 如果数据流不可用,则清空 packet_pool
      3. 如果文件结束了,则设置 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();
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published