音视频重回顾之基于ai初步实现一个播放器,并思考架构和细节
目前,在ai帮助的前提下,已经实现了一个播放器的基础功能,对相关实现过程中的细节进行梳理。
目前,在ai帮助的前提下,已经实现了一个播放器的基础功能,对相关实现过程中的细节进行梳理。
1.简述
通过项目实践,在ai的帮助下,把自己已有的一些理论知识用项目的形式进行展现。
实现一个支持播放本地文件的视频播放器功能,支持音视频同步(这是最核心基础),播放/暂停,音量控制,进度条显示及控制,倍速控制,osd信息显示等基本功能。
本次主要使用sdl3(SDL3_ttf)+ffmpeg6进行开发。

**支持的功能:**视频能正常播放的前提下,有播放/暂停控制,音量控制,倍速控制,seek控制,osd字母显示,键盘事件/鼠标控制等逻辑。
有遗留暂时未修复的bug:倍速时sonic偶现崩溃(在调用sonic库时,原始frame和sonic库进行计算时的问题),seek的稳定运行还得参考ffplay从数据结构上进行优化处理(seek后要刷新packet,重置clock,并清空frame)。
2.反思
基于ai,在自己的引导下一步步完成视频播放基本功能,并没有遇到过阻塞无法解决的问题。
在实践的过程中发现,需要自己在宏观上把控项目实现的架构,以及所有的问题都是细节交互的问题,尤其这种带有ui界面的,需要做时间戳音视频同步的,都是在多个线程之间细节的把控。
在音视频已经很成熟的当下,所有的接口都已经拿来即可用,最核心的功能是进行音视频的同步处理,需要注意数据结构的设计以及同步策略选择,后面的暂停,倍速,seek等都是建立在该核心功能的基础上的。
===》初步实现后进行参考和反思,发现遇到的问题,应该在数据结构设计层次进行处理,问题会变得简单。
3.架构整理
在ai的帮助下完成第一遍的初步功能实现,就整个实现过程架构和细节进行回顾整理。
3.1 首先梳理以ai为导向,自己的实现过程
这里sdl和ffmpeg的接口已经足够成熟,相关解码及播放的逻辑都无阻碍。
第一次探索,自己没有过多设计数据结构和架构,所遇到的问题都是细节性控制小问题。
核心复杂的问题就是 音频时钟的获取和视频的同步处理,以及seek的操作。

3.2 环境配置最是麻烦,贴一下配置,方便后期回顾。
基于vs2022环境实现,依赖的lib如下:

依赖的对应头文件如下:

这里暂时没有新增播放列表,以及选择文件入口,所以在项目–》配置—》调试—》命令参数中设置死对应的执行默认参数。
3.3 功能实现后反思
在初步实现各种功能基本正常的功能后,发现内部逻辑也没有过多复杂,实际上就是多个线程,配合多个消息队列进行交互控制的逻辑。
自己遇到的同步,seek时的一些问题,核心还是数据结构设计层次提升了问题处理的复杂度,可以在frame队列设计上,以及时钟clock设计时从数据结构层次让问题更简单。

考虑整体逻辑:

3.4 关注ffplay中视频同步,seek处理逻辑
参考ffplay中这段代码实际上是视频同步和seek的关键代码: seek和音视频同步的逻辑,直接在数据结构设计上让问题简单。
主要理解的就是处理seek的架构,在队列中维持了一个serial标志,然后每个视频帧中也携带serial,进行判断是丢弃还是显示。
void FFPlayer::video_refresh(double * remaining_time)
{
Frame *vp = nullptr, *lastvp = nullptr;
// 目前我们先是只有队列里面有视频帧可以播放,就先播放出来
// 判断有没有视频画面
if (video_st) {
retry:
//队列中没有是哦就判断停止喽
if (frame_queue_nb_remaining(&pictq) == 0) {
// nothing to do, no picture to display in the queue
video_no_data = 1; // 没有数据可读
if(eof == 1) {
check_play_finish();
}
} else {
video_no_data = 0; // 有数据可读
double last_duration, duration, delay;
/* dequeue the picture */
lastvp = frame_queue_peek_last(&pictq);//上一帧
screenshot(lastvp->frame); //判断做截屏的处理
vp = frame_queue_peek(&pictq); //当前帧
//处理 seek 或丢弃旧帧
if (vp->serial != videoq.serial) {
frame_queue_next(&pictq);
goto retry;
}
//如果是 seek 后第一帧,重置 frame_timer
//为了避免 seek 后第一帧 diff 巨大导致跳帧。
if (lastvp->serial != vp->serial) {
frame_timer = av_gettime_relative() / 1000000.0;
}
//暂停状态下直接显示 不更新 frame_timer,不丢帧,只显示当前帧
if (paused) {
goto display;
}
//计算上一帧到当前帧的理论间隔
/* compute nominal last_duration */
last_duration = vp_duration(lastvp, vp); //当前帧 pts - 上一帧 pts
// LOG(INFO) << "last_duration ......" << last_duration;
delay = compute_target_delay(last_duration); //根据音频时钟 diff 调整 delay
// LOG(INFO) << "delay ......" << delay;
double time = av_gettime_relative() / 1000000.0;
//判断是否需要等待 如果“还没到显示时间” → 等待 否则 → 显示下一帧
if (time < frame_timer + delay) {
// LOG(INFO) << "(frame_timer + delay) - time " << frame_timer + delay - time;
*remaining_time = FFMIN( frame_timer + delay - time, *remaining_time);
goto display;
}
//更新 frame_timer 如果系统时间跳变太大(比如卡顿) → 重置 frame_timer
frame_timer += delay;
if (delay > 0 && time - frame_timer > AV_SYNC_THRESHOLD_MAX) {
frame_timer = time;
}
//更新视频时钟 用于音视频同步
SDL_LockMutex(pictq.mutex);
if (!std::isnan(vp->pts)) {
update_video_pts(vp->pts, vp->pos, vp->serial);
}
SDL_UnlockMutex(pictq.mutex);
// LOG(INFO) << "debug " << __LINE__;
//判断是否需要丢帧(视频落后音频)
//视频落后音频太多且 framedrop 开启 → 丢掉当前帧,显示下一帧
if (frame_queue_nb_remaining(&pictq) > 1) {
Frame *nextvp = frame_queue_peek_next(&pictq);
duration = vp_duration(vp, nextvp);
if (!step && (framedrop > 0 || (framedrop && get_master_sync_type() != AV_SYNC_VIDEO_MASTER))
&& time > frame_timer + duration) {
frame_drops_late++;
// LOG(INFO) << "frame_drops_late " << frame_drops_late;
frame_queue_next(&pictq);
goto retry;
}
}
//正常显示当前帧
frame_queue_next(&pictq);
force_refresh = 1;
// LOG(INFO) << "debug " << __LINE__;
// if (step && !paused)
// stream_toggle_pause(is);
}
display:
/* display picture */
if (force_refresh && pictq.rindex_shown) {
if(vp) {
if(video_refresh_callback_) {
video_refresh_callback_(vp);
}
}
}
}
force_refresh = 0;
}
音频的核心处理逻辑是通过回调函数实现的,主要就是维持一个音频缓冲区,实现向sdl缓冲区拷贝动作 (音频缓冲区 audio_buf + index + size)控制缓冲区数据安全可靠得向sdl缓冲区中拷贝。
除此之外,涉及音频帧得重采样,多通道处理,倍速处理。
更多推荐


所有评论(0)