- 学习烧录1.3,0, 0.5 Done
- 学习文件管理,测试SD卡支持程度 1.4, 0,0.5 Done
- 实战SDK,编译app的hello word和自己的镜像1.1,0,1.5 Done
- 实现对自己的APP进行编译和打包1.2,1.1,1 临时不需要
进行图像处理库的移植和测试2.2, 1.2, 2Done
这里重点记录图像处理思路和视频保存。
作为录视频的触发条件,临时设定为双保险:入侵检测(即动态捕获)和人形检测(跑yolo5)的混合模式。
不同于我们的家用pc有充足的资源甚至能软编码,对于这款开发板我们只能通过官方的视频媒体处理接口( Rockchip Media Process Interface,简称 RKMPI )进行开发,这样可以使用硬解码功能。
比较重要的一点就是文件的储存和录制,我需要进行学习,补齐一些unix系统编程的知识。
虽然人家SDK 注释几乎没用,文档也遮遮掩掩的,但是依然可以用作学习的材料。让我们一起研究RKipc自带的储存录制模块。这是有必要的,因为到了后期即使我们不需要直接调用RKipc的储存函数,我们自己手搓可能也能要借鉴里面的一些技术。
//以下内容特别感谢Windows Copilot
文件目录“SDK_PATH/project/app/rkipc/rkipc/common/storage/storage.c”
我们在fork()这篇文章中对进程有了些许的理解,(可恶啊,卡内基梅隆那个shell实验还没做,我真的太废物了,无能狂怒qwq)我们可以使用fork()函数来创建一个新的进程。现在我们可以继续探索线程。
首先回想一下进程 processes,每一个进程都有自己pid,也就是身份证,子进程和父进程可以看作是复制的关系,我们还讨论了一些变量和文件输出的问题。由此可见,如果你需要管理不同的进程,我们需要搞到它们的ID,储存起来,然后进行资源和逻辑的访问控制,另外我们可以使用信号来解决管理问题,比如kill函数。啥,你问我具体咋做?可以搜索一下怎样实现一个简单的多任务shell。我要是会这个我就把卡内基梅隆的shellLab做完了,,,。
对于线程 threads, Linux Kernel Development 这本书提及“Microsoft Windows or Sun Solaris sometimes call threads lightweight processes”. 而在Linux当中, To Linux, threads are simply a manner of sharing resources between processes (which are already quite lightweight)。由此可见对于Linux来说,不需要将线程作为一个和进程结构不同的新方式。
对于这个问题, Copilot也回答“ 在 Linux 中,线程和进程的主要区别在于它们共享的资源。具体来说,线程共享以下资源:
信号处理:线程共享信号处理器,这意味着信号可以被任何一个线程处理。
虚拟地址空间:所有线程共享同一个虚拟地址空间,这意味着它们可以访问相同的全局变量和堆内存。
文件描述符:线程共享打开的文件描述符,这使得它们可以同时读写相同的文件。
Linux 内核使用相同的调度器来调度进程和线程。线程只是共享资源的进程,因此它们在调度时没有特殊处理。调度器根据优先级、时间片等因素来决定哪个线程或进程应该运行。
由于线程共享资源,因此需要使用同步机制来防止数据竞争和不一致性。常用的同步机制包括互斥锁(pthread_mutex_t)、条件变量(pthread_cond_t)等。”
以及我们为什么需要线程:
在摄像头程序中,通常会涉及到多个线程来处理不同的任务,例如:
- 一个线程负责从摄像头获取视频流。
- 一个线程负责处理视频数据(如编码、存储)。
- 另一个线程可能负责网络传输或显示视频。
在本文的讨论中,我们不讨论目前我的工程的具体执行流程,比如入口在哪里多少进程多少线程,仅分析storage.c这个程序
RK官方代码风格没得说,先扫一眼对应头文件storage.h的情况
采用json的方式?有点死说实话
“cJSON.h”的相关配置
//#include "cJSON.h" #include "common.h" #include "rkmuxer.h" #define RKIPC_MAX_FORMAT_ID_LEN 8 #define RKIPC_MAX_VOLUME_LEN 11 #define RKIPC_MAX_FILE_PATH_LEN 256 #define JSON_KEY_FOLDER_NAME "FolderName" #define JSON_KEY_FILE_NUMBER "FileNumber" #define JSON_KEY_TOTAL_SIZE "total_size" #define JSON_KEY_TOTAL_SPACE "total_space" #define JSON_KEY_FILE_ARRAY "FileArray" #define JSON_KEY_FILE_NAME "FileName" #define JSON_KEY_MODIFY_TIME "ModifyTime" #define JSON_KEY_FILE_SIZE "FileSize" #define JSON_KEY_FILE_SPACE "FileSpace"
然后是一个有意思的宏
/* Pointer Check */
#define RKIPC_CHECK_POINTER(p, errcode)
do {
if (!(p)) {
LOG_DEBUG("pointer[%s] is NULL", #p);
return errcode;
}
} while (0)
前面几个相当于配置没啥意思,但是有些结构体是有锁或者pthread_t的,最后一个就是pthread_t record_thread_id;
结构体定义区
typedef enum {
DISK_UNMOUNTED = 0,
DISK_NOT_FORMATTED,
DISK_FORMAT_ERR,
DISK_SCANNING,
DISK_MOUNTED,
DISK_MOUNT_BUTT,
} rkipc_mount_status;
typedef enum {
LIST_ASCENDING = 0,
LIST_DESCENDING,
LIST_BUTT,
} rkipc_sort_type;
typedef enum {
SORT_MODIFY_TIME = 0,
SORT_FILE_NAME,
SORT_BUTT,
} rkipc_sort_condition;
typedef enum {
MSG_DEV_ADD = 1,
MSG_DEV_REMOVE = 2,
MSG_DEV_CHANGED = 3,
} rkipc_enum_msg;
typedef struct {
char folder_path[RKIPC_MAX_FILE_PATH_LEN];
rkipc_sort_condition sort_cond;
bool num_limit;
int limit;
} rkipc_str_folder_attr;
typedef struct {
char dev_path[RKIPC_MAX_FILE_PATH_LEN];
char mount_path[RKIPC_MAX_FILE_PATH_LEN];
int free_size_del_min;
int free_size_del_max;
int auto_delete;
int folder_num;
char format_id[RKIPC_MAX_FORMAT_ID_LEN];
char volume[RKIPC_MAX_VOLUME_LEN];
int check_format_id;
rkipc_str_folder_attr *folder_attr;
} rkipc_str_dev_attr;
typedef struct {
char filename[RKIPC_MAX_FILE_PATH_LEN];
long size;
long long time;
void *thumb;
} rkipc_fileinfo;
typedef struct {
char path[RKIPC_MAX_FILE_PATH_LEN];
int file_num;
rkipc_fileinfo *file;
} rkipc_filelist;
typedef struct {
int list_num;
rkipc_filelist *list;
} rkipc_filelist_array;
typedef struct _rkipc_str_file {
struct _rkipc_str_file *next;
char filename[RKIPC_MAX_FILE_PATH_LEN];
time_t time;
off_t size;
off_t space;
mode_t mode;
} rkipc_str_file;
typedef struct {
char cpath[RKIPC_MAX_FILE_PATH_LEN * 2];
rkipc_sort_condition sort_cond;
int wd;
int file_num;
off_t total_size;
off_t total_space;
pthread_mutex_t mutex;
rkipc_str_file *file_list_first;
rkipc_str_file *file_list_last;
} rkipc_str_folder;
typedef struct {
char dev_path[RKIPC_MAX_FILE_PATH_LEN];
char dev_type[MAX_TYPE_NMSG_LEN];
char dev_attr_1[MAX_ATTR_LEN];
rkipc_mount_status mount_status;
pthread_t file_scan_tid;
int folder_num;
int total_size;
int free_size;
int fsck_quit;
rkipc_str_folder *folder;
} rkipc_str_dev_sta;
typedef struct _rkipc_tmsg_element {
struct _rkipc_tmsg_element *next;
int msg;
char *data;
int data_len;
} rkipc_tmsg_element;
typedef struct {
rkipc_tmsg_element *first;
rkipc_tmsg_element *last;
int num;
int quit;
pthread_mutex_t mutex;
pthread_cond_t not_empty;
rkipc_reg_msg_cb rec_msg_cb;
pthread_t rec_tid;
void *handle_path;
} rkipc_tmsg_buffer;
typedef struct {
rkipc_tmsg_buffer msg_hd;
pthread_t event_listener_tid;
int event_listener_run;
rkipc_str_dev_sta dev_sta;
rkipc_str_dev_attr dev_attr;
} rkipc_storage_handle;
typedef struct rk_storage_muxer_struct_ {
int id;
char file_name[256 * 2];
char record_path[256];
const char *file_format;
int file_duration;
int g_record_run_;
void *g_storage_signal;
pthread_t record_thread_id;
VideoParam g_video_param;
AudioParam g_audio_param;
} rk_storage_muxer_struct;
然后是函数原型区,看名称知功能
函数原型区
现在我们来看c文件,开头准备一些重要的变量,不过看变量猜功能
#define STORAGE_NUM 3
static int record_flag[STORAGE_NUM] = {-1}; // 3 storage thread I guess?
static void *g_sd_phandle = NULL;
static void *g_file_scan_signal = NULL;
static rkipc_str_dev_attr g_sd_dev_attr;
static pthread_mutex_t g_rkmuxer_mutex = PTHREAD_MUTEX_INITIALIZER; // mutex, or lock init? { { 0, 0, 0, 0, 0, 0, { 0, 0 } } } a crazy way to init structure
static int g_storage_record_flag[3]; // only for recording thread
static int rk_storage_muxer_init_by_id(int id);
static int rk_storage_muxer_deinit_by_id(int id);
然后就看不懂了,问问copilot这段代码问问有没有比较简单的例程
static pthread_mutex_t g_rkmuxer_mutex = PTHREAD_MUTEX_INITIALIZER; //互斥锁通常用于保护共享资源,防止多个线程同时访问和修改。 static int g_storage_record_flag[3]; // only for recording thread, 这个数组的注释表明它是专门为记录线程使用的,这暗示了存在一个或多个记录线程。
继续解释:通常,多线程程序会包含以下部分:
线程创建: 使用 pthread_create 函数来创建新线程。例如:
pthread_t thread_id;
pthread_create(&thread_id, NULL, thread_function, NULL);
线程函数: 定义线程执行的函数。例如:
void *thread_function(void *arg) {
// 线程执行的代码
return NULL;
}
线程同步: 使用互斥锁、条件变量等机制来同步线程。例如:
pthread_mutex_lock(&g_rkmuxer_mutex);
// 访问共享资源
pthread_mutex_unlock(&g_rkmuxer_mutex);
还有一个例程
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_mutex_t lock;
void *thread_function(void *arg) {
pthread_mutex_lock(&lock);
printf("Thread %d is running\n", *(int *)arg);
pthread_mutex_unlock(&lock);
return NULL;
}
int main() {
pthread_t threads[3];
int thread_args[3] = {1, 2, 3};
pthread_mutex_init(&lock, NULL);
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &thread_args[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_mutex_destroy(&lock);
return 0;
}
//基本功,函数指针的用法
#include <stdio.h>
// 定义一个函数指针类型
typedef int (*device_operation)(void);
// 定义具体的设备操作函数
int device_init(void) {
printf("Device initialized\n");
return 0;
}
int device_read(void) {
printf("Device read\n");
return 0;
}
int device_write(void) {
printf("Device write\n");
return 0;
}
int main() {
// 定义一个函数指针并赋值
device_operation op = device_init;
op(); // 调用初始化函数
op = device_read;
op(); // 调用读函数
op = device_write;
op(); // 调用写函数
return 0;
}
其实还是不很懂,但是至少学会怎么用了,先看看我们感兴趣的录制功能的实现。
函数不算长,让我们看看做了什么
int rk_storage_record_start() {
// only main stream
LOG_INFO("start\n");
time_t t = time(NULL);
struct tm tm = *localtime(&t);
snprintf(rk_storage_muxer_group[0].file_name, 512, "%s/%d%02d%02d%02d%02d%02d.%s",
rk_storage_muxer_group[0].record_path, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, rk_storage_muxer_group[0].file_format);
LOG_INFO("file_name is %s\n", rk_storage_muxer_group[0].file_name);
pthread_mutex_lock(&g_rkmuxer_mutex);
rk_storage_muxer_group[0].g_record_run_ = 0;
rkmuxer_deinit(0);
rkmuxer_init(0, NULL, rk_storage_muxer_group[0].file_name,
&rk_storage_muxer_group[0].g_video_param,
&rk_storage_muxer_group[0].g_audio_param);
rk_storage_muxer_group[0].g_record_run_ = 1;
pthread_mutex_unlock(&g_rkmuxer_mutex);
LOG_INFO("end\n");
return 0;
}
前几行包括snprintf是将基本的信息比如文件名和目录和文件格式储存,这样我们就完成了第一个rk_storage_muxer_group的配置,紧接着pthread_mutex_lock(&g_rkmuxer_mutex);这行代码申请了一个互斥锁g_rkmuxer_mutex,这是一个全局的互斥锁,初学把它当一个tag就可以,另外我们还可以简单认为pthread_mutex_lock(&g_rkmuxer_mutex); 和 pthread_mutex_unlock(&g_rkmuxer_mutex); 之间的所有代码都会受到互斥锁的保护,防止自身被其他线程强制打断或者打扰,如果不清楚怎样打扰,可以看一下前文创建和加入一个线程的例程。也就是不但保护了我们配置好的部分,还能保证rk_storage_muxer_group[0].g_record_run_ = 0; 和rkmuxer_init(0, NULL, rk_storage_muxer_group[0].file_name,&rk_storage_muxer_group[0].g_video_param,&rk_storage_muxer_group[0].g_audio_param);和rk_storage_muxer_group[0].g_record_run_ = 1;这三个写入语句不会因为线程的调度切换或者加入不同线程造成的修改,某种程度让这个代码块完全成块,不可再分。
所以启动函数的逻辑就很简单了,把时间文件目录之类的放到配置里面,设置记录运行的标识变量,停用rkmuxer,再初始化一个,再设置运行状态标识。啥?那rkmuxer_init和rkmuxer_deinit具体咋样?嗯,RK很不要脸的没开源,,,只给了一个没有备注的头文件,文档也没有
MUXER.H
#ifndef __RKMUXER_H__
#define __RKMUXER_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
typedef struct {
unsigned int width;
unsigned int height;
unsigned int vir_width;
unsigned int vir_height;
unsigned int data_size;
unsigned char *data;
} ThumbParam;
typedef struct StVideoParam {
char format[24]; // NV12
char codec[24]; // H.264, H.265
int width;
int height;
int bit_rate;
int profile; // h264 profile
int level; // h264 level
int frame_rate_den;
int frame_rate_num;
int qp_min;
int qp_max;
int vir_width;
int vir_height;
ThumbParam thumb;
} VideoParam;
typedef struct StAudioParam {
char format[24]; // S16,S32
char codec[24]; // MP2
int channels;
int sample_rate;
int frame_size;
} AudioParam;
int rkmuxer_init(int id, char *format, const char *file_path, VideoParam *video_param,
AudioParam *audio_param);
int rkmuxer_deinit(int id);
int rkmuxer_write_video_frame(int id, unsigned char *buffer, unsigned int buffer_size,
int64_t present_time, int key_frame);
int rkmuxer_write_audio_frame(int id, unsigned char *buffer, unsigned int buffer_size,
int64_t present_time);
long long int rkmuxer_get_thumb_pos(int id);
#ifdef __cplusplus
}
#endif
#endif
因为这个c语言文件其他函数基本上是对muxer.c里面函数的二次封装,比如写视频帧等等,不再研究,后续有时间会分析storage.c其他的一些技巧和好玩的东西。
得出结论,学了很多东西,但是官方没东西,还是没用,下一期我会尝试使用其他的方式进行(原始视频流的封装),比如手搓一个H264转存flv或者使用ffmpeg来进行视频文件的保存(感谢这个老哥的问题,某种程度也是分享)。
最后几张截图表达一下自己的感受


嗯,,,,有些郁闷其实qwq
一些补充材料或者引用,感谢知识的共享,我们得以进步乃至机械飞升[雾]:
The Linux Implementation of Threads | Linux Kernel Process Management | InformIT
一文搞定Linux进程和线程(详细图解)_linux 启动独立线程-CSDN博客
Linux Process vs. Thread | Baeldung on Linux
multithreading – Do Linux kernel processes multithread? – Stack Overflow
Are Linux kernel threads really kernel processes? – Unix & Linux Stack Exchange
linux 下 进程和线程的区别(baidu 面试)_linux的进程与线程-CSDN博客
Linux——模拟实现一个简单的shell(带重定向) – tp_16b – 博客园 (cnblogs.com)
Views: 41
