上集说到视频架构,这次我们分析一下储存的问题。
Part 1 视频文件的格式和储存。
视频文件储存的方案有很多。因为我们使用H264编码,我们可选的就有MP4,FLV,ts(HLS)等等。
但是我们要从使用场景的角度出发:1. 摄像头需要几乎实时保存数据 2. 码流和时长不确定 3. 尽量顺序保存,能低成本对整个视频的时长等数据进行记录,避免修复视频文件 4. 方便断电后继续录制或者新建文件 5. 想设计简易的webserver进行回放,需要带有快速定位的数据格式。
综合以上要求,封装简单,10s差不多300帧分片,还有m3u8索引的HLS就是我们的选择。不需要像MP4那样修复,难以建立索引,也不需要flv这样时时更新头部的视频数据,不需要担心输出头部信息不足不标准的文件造成各种兼容问题。
Part 2 文件和储存相关的业务
然后是对业务进行功能性的分析,未验证和实现的部分会标注:
- 接收视频帧
- 打包成HLS格式,也就是M3U8和TS文件,该文件格式写入有缓冲
- 文件的自动删除和释放,删除最老的一天的文件夹(删除方式待定)
- 午夜12点自动切换到下一天的文件夹
- 获取磁盘信息,包括sd卡是否存在,容量和剩余空间等
- 断电处理(方式待定)
- 文件数据库
然后我们再分条分析:
- 接收视频帧,我们向rk的例程看齐。在官方rkipc_get_venc_0这个线程中,我们可以看到rk_storage_write_video_frame(0, data, stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS, 0);这样的接口,我们也设计一个函数接受帧数据data,data长度length,是否为关键帧即可。至于为什么知道关键帧,是受到HLS的库的限制。
- 打包成HLS格式。这一点我们直接引入https://github.com/ireader/media-server这个库。等下讨论这个库的使用。
- 文件的自动删除和释放。整体框架我也想了两种实现方式。第一个就是开一个释放空间的线程,开启后就等待,每次写入ts文件后检查容量决定是否唤醒释放空间的线程来删除文件。第一种就是开一个线程,每过一段时间就检查磁盘空间。目前实现的是一种,后面会考虑换成第二种,这样就不会涉及到频繁加锁解锁或者读磁盘浪费性能。
- 文件夹的自动切换可以理解成:每帧判断日期(一年的第几天),如果日期变更,强制保存当前的ts和m3u8文件,创建新日期的文件夹,更新hls的文件名,更新日期。
- 获取磁盘信息。熟悉文件api即可。
- 断电处理。判断文件夹的存在情况,目前在纠结是便利文件夹还是仅依靠数据库,还是两者互相检查。但是视频储存是确定的,在每天的日期文件夹标号,文件夹的数字表示断电次数。比如“/mnt/sdcard/DCIM/2021-07-01/“里面有”/mnt/sdcard/DCIM/2021-07-01/0000/00001.ts”和”/mnt/sdcard/DCIM/2021-07-01/0000/index.m3u8″
- 文件数据库。在DCIM文件夹创建一个index.db文件,记录一共多少天的视频,和每天的热度也就是打开冰箱的次数。在每天的info.db文件记录具体每次开冰箱的时间和开始录制视频的时间。
Part 3 HLS 库的学习和使用
下面补充说一下hls这个库的使用。
在使用这个库之前,先聊一点面向对象的思想。
毕竟现在涉及到了数据结构的层次,为了方便理解对复杂问题的抽象和处理方式,我们需要回顾一下面向对象的思想。
对象说白了就是数据和方法封装到一起。虽然这是c语言,没java/C++那种对象(和我一样)或者类,但是我们有结构体和函数指针。利用结构体,可以很简单的把数据封装到一起,利用函数指针,我们就可以把函数作为一个方法和数据绑定在一起。
对于由ts文件和m3u8文件组成的HLS协议视频或者封装器,我们可以看作一个对象。
然后我们配置安装media-server这个库,仅当前库即可,hls功能不需要前置的sdk,因为我们只需要封装文件,不需要重新编码。编译的过程可以参考冰箱警察7 进度更新的sqlite交叉编译过程,安装可以参考冰箱警察9 通过sh和cmake配置项目的cmake配置过程。为了C/C++插件自动补全的运作,可以在项目根目录新建.vscode文件夹,里面新建c_cpp_properties.json,添加以下内容:
{
"configurations": [
{
"name": "LuckFox-Pico",
"includePath": [
"${BUILDROOT_PATH}/usr/include",
"/workspaces/luckfox-pico/myProject/MD_example/include",
"/workspaces/luckfox-pico/myProject/MD_example/include/**",
"/workspaces/luckfox-pico/myProject/MD_example/3rdparty/**"
],
"defines": [],
"compilerPath": "${SDK_PATH}/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc",
"cStandard": "gnu11",
"cppStandard": "c++20",
"intelliSenseMode": "linux-gcc-arm"
}
],
"version": 4
}
我们再查看flv转hls的例程“3rdparty/media-server/libhls/demo/hls-segmenter-flv.cpp“,学习库的使用方法。
对各个函数的参数分析之后发现实际很简单,核心还是一帧帧处理视频。这里记录的是一些设计。
- 对象化:代码使用相应结构体表示对象,对象的初始化则使用初始化函数。
- 模块设计:flv模块就分了flv_reader和flv_demuxer,hls协议部分则选择把m3u8和ts媒体分离。
- 回调函数:回调函数可以看作实现具体功能的接口,方便在特定区域和步骤内插入我们自己的代码。具体实现往往都是利用函数指针记录函数的入口,存放在结构体当中。
这里我们结合HLS这个需求,多花一些笔墨来分析回调函数。
对于HLS协议,我们需要给文件标号码,获得切片后的文件名,写入缓冲好的ts文件,更新m3u8列表,还需要判断切片是否连续。面对复杂和可能有客制化需求的的情况(比如文件写入的处理,文件名的路径),使用回调函数暴露给用户就是很好的选择,就是我们找到的hls_handler函数。
作为用户,我们还需要了解回调函数的回调时机。比如我们发现示例hls_handler当中有写入ts文件部分,而我们知道ts文件不是时时刻刻都写入的,是ts视频片段在内存缓存足够的帧数再写入的。所以这个回调函数会在ts文件切片结束后自动执行,负责把内存的ts视频写入磁盘或者发送出去和更新m3u8列表。
另外我们注意到hls_media_input(hls, PSI_STREAM_H264, NULL, 0, 0, 0, 0);这个函数被安置在写m3u8前作为视频转换的结束。由此可见hls_media_input传入这些参数可以标记视频文件的结尾,可以直接调用回调函数。
Part 4 更进一步,条件变量的使用
虽然大概率要删除条件变量的部分,但是还想提一下条件变量的经典用法。
while (g_storage_run_ == 1 && SD_card_exist == RK_TRUE)
{
pthread_mutex_lock(&s_free_space_mutex);
while (need_free_disk == RK_FALSE && g_storage_run_ == 1)
{
pthread_cond_wait(&s_free_space_cond, &s_free_space_mutex);
}
pthread_mutex_unlock(&s_free_space_mutex);
//balabala
}
上面的代码可以看出来先使用互斥锁加锁,再申请占用条件变量。
唤醒的时候,先占锁,再发条件信号,再解锁即可。这段代码可以用在反初始化当中,终止带有条件变量的线程。
pthread_mutex_lock(&s_free_space_mutex);
pthread_cond_signal(&s_free_space_cond);
pthread_mutex_unlock(&s_free_space_mutex);
Part 5 关于储存模块设计的补充
为保证代码风格的一致,也为了方便后续升级,我们模仿video.c,除了获得H264帧的接口函数,也设置入口函数对储存进行检测和初始化。以下是当前版本的函数原型。
int storage_init();
int storage_deinit();
int write_frame_2_SD(RK_U8 * data, RK_U32 len, RK_BOOL is_key_frame);
static int hls_handler(void* m3u8, const void* data, size_t bytes, int64_t pts, int64_t dts, int64_t duration);
int folder_create(const char *folder);
static int get_disk_size(char *path, uint32_t *total_size,
uint32_t *free_size, uint64_t *used_size);
int config_hls();
void *space_cleanup_thread(void *arg);
void free_up_disk_notify();
Views: 28
