文章目录
前言
obs系列文章入口:https://blog.youkuaiyun.com/qq_33844311/article/details/121479224
上一篇文章分析了obs的视频渲染线程的前世今生,这篇我们还是按照老规矩来分析一下video_thread 视频的编码线程的技术细节。主要还是从线程的创建时机、线程负责的工作、与其他线程之间的配合三个方面来分析obs的视频编码线程的工作细节。
1.视频编码线程的创建时机
通过对obs的vs项目下断点调试,可以发现视频编码线程的创建和视频渲染线程的创建是在同一个函数obs_init_video里面,同样也是在WinMain主线程当中创建。先创建视频编码线程video_thread,紧接着创建视频渲染线程obs_graphics_thread
// 创建video_thread的位置 obs-studio\libobs\media-io\video-io.c
pthread_create(&out->thread, NULL, video_thread, out)
> obs.dll!video_output_open(video_output** video, video_output_info * info) 行 247 C
obs.dll!obs_init_video(obs_video_info * ovi) 行 401 C
obs.dll!obs_reset_video(obs_video_info * ovi) 行 1174 C
obs64.exe!AttemptToResetVideo(obs_video_info * ovi) 行 4315 C++
obs64.exe!OBSBasic::ResetVideo() 行 4429 C++
obs64.exe!OBSBasic::OBSInit() 行 1775 C++
obs64.exe!OBSApp::OBSInit() 行 1474 C++
obs64.exe!run_program(std::basic_fstream<char,std::char_traits<char>> & logFile, int argc, char * * argv) 行 2138 C++
obs64.exe!main(int argc, char * * argv) 行 2839 C++
obs64.exe!WinMain(HINSTANCE__ * __formal, HINSTANCE__ * __formal, char * __formal, int __formal) 行 97 C++
video_output_open函数创建视频编码线程
真正创建视频编码线程的函数,逻辑比较简单不做赘述,看注释即可。
int video_output_open(video_t **video, struct video_output_info *info)
{
struct video_output *out;
if (!valid_video_params(info))
return VIDEO_OUTPUT_INVALIDPARAM;
// 申请 video_output 的内存空间
out = bzalloc(sizeof(struct video_output));
if (!out)
goto fail0;
memcpy(&out->info, info, sizeof(struct video_output_info));
out->frame_time =
util_mul_div64(1000000000ULL, info->fps_den, info->fps_num);
out->initialized = false;
if (pthread_mutex_init_recursive(&out->data_mutex) != 0)
goto fail0;
if (pthread_mutex_init_recursive(&out->input_mutex) != 0)
goto fail1;
if (os_sem_init(&out->update_semaphore, 0) != 0)
goto fail2;
// 真正创建 video_thread 视频编码线程的地方
if (pthread_create(&out->thread, NULL, video_thread, out) != 0)
goto fail3;
// 初始化视频帧缓存队列,数组实现的环形队列
init_cache(out);
out->initialized = true;
// 保存 video_output 到 obs_core -> obs_core_video -> video
*video = out;
return VIDEO_OUTPUT_SUCCESS;
fail3:
os_sem_destroy(out->update_semaphore);
fail2:
pthread_mutex_destroy(&out->input_mutex);
fail1:
pthread_mutex_destroy(&out->data_mutex);
fail0:
video_output_close(out);
return VIDEO_OUTPUT_FAIL;
}
2.视频编码线程的工作内容
当用户开启推流或者录制,视频渲染线程渲染好一帧图像,通过信号通知视频编码线程从缓存队列取出一帧图像,进行编码发送。最终编码后的视频帧、音频帧放到了音视频交织队列,并通知视频发送线程进行录制或者推流。这里视频发送线程可以同时存在1个或者多个,比如推流的同时也开启录制,就会存在两个发送线程分别属于各自的output。如果把视频编码线程当作生产者,视频发送线程当作消费者,则是1对n的生产者消费者编程模型。
了解了视频编码线程大致的工作内容,我们接下来通过分析源码,具体了解一下video_thread是怎么工作的。
static void *video_thread(void *param)
{
struct video_output *video = param;
// 等待信号量 【video_output_unlock_frame函数os_sem_post 通知 video_thread 工作】
while (os_sem_wait(video->update_semaphore) == 0) {
// 检查线程是否退出
if (video->stop)
break;
// 取当前一帧视频进行编码发送
while (!video->stop && !video_output_cur_frame(video)) {
os_atomic_inc_long(&video->total_frames);
}
// 发送的总帧数原子操作+1
os_atomic_inc_long(&video->total_frames);
}
return NULL;
}
video_output_cur_frame 线程真正的工作函数
在video_output_cur_frame 函数里面真正的从视频缓存队列取一帧视频送去编码。具体细节看以下源码注释。
static inline bool video_output_cur_frame(struct video_output *video)
{
struct cached_frame_info *frame_info;
bool complete;
bool skipped;
/* -------------------------------- */
//操作视频帧缓存区需要上锁
//获取缓存的一帧视频信息到 frame_info
pthread_mutex_lock(&video->data_mutex);
frame_info = &video->cache[video->first_added];
pthread_mutex_unlock(&video->data_mutex);
/* -------------------------------- */
pthread_mutex_lock(&video->input_mutex);
for (size_t i = 0; i < video->inputs.num; i++) {
struct video_input *input = video->inputs.array + i;
struct video_data frame = frame_info->frame;
// 推流或者录像如果设置分辨率缩放在scale_video_output函数里面图像的缩放处理
if (scale_video_output(input, &frame))
// callback 绑定的obs-encoder.c 的receive_video函数
// receive_video回调里面做视频的编码
// 具体编码是libx264软便,或者gpu硬件编码,跟绑定编码器有关
input->callback(input->param, &frame);
}
pthread_mutex_unlock(&video->input_mutex);
/* -------------------------------- */
pthread_mutex_lock(&video->data_mutex);
frame_info->frame.timestamp += video->frame_time;
// 循环处理 直到count等于0表示处理完成,等待下次的视频渲染线程的信号通知
complete = --frame_info->count == 0;
skipped = frame_info->skipped > 0;
if (complete) {
// 处理完成更新新添加视频帧的坐标+1,数组实现的环形队列
if (++video->first_added == video->info.cache_size)
video->first_added = 0;
// 更新可缓存视频帧坐标+1,同样是数组实现的环形队列
if (++video->available_frames == video->info.cache_size)
video->last_added = video->first_added;
} else if (skipped) {
// 记录需要跳过的帧的个数
--frame_info->skipped;
os_atomic_inc_long(&video->skipped_frames);
}
pthread_mutex_unlock(&video->data_mutex);
/* -------------------------------- */
return complete;
}
详细解释cached_frame_info的两个字段的含义
frame_info->count的含义表示需要重复输出的帧的次数
- 当视频渲染线程的处理能力足够的情况count永远等于1
- 当视频处理能力不满足设置的帧率情况下,count等于流逝的时间/帧间隔时间 count = (int)((os_gettime_ns() - cur_time) / interval_ns);
frame_info->skipped的含义表示需要跳过的帧的个数
- 当视频编码线程处理能力够的情况下 skipped永远等于0
- 当视频编码线程处理能力不够,视频缓冲区被填满后,需要跳过count个帧
注意以上两个参数的设计保证了在视频数据尽可能的“满足”设置的帧率,当然了推流出去的画面会卡。如果发现和两个值异常,说明推流端存在处理性能瓶颈(采集or编码),需要降低视频编码码率或者视频帧率。性能排查的时候可以关注这两个字段的值。
3.视频编码线程与rtmp输出线程之间的配合
视频编码线程完成编码后会调用注册的obs_output_info->encoded_packet回调函数,对于rtmp推流最终调用的是rtmp_stream_data,把编码后的视频包放到rtmp的音视频包缓存队列,并通知rtmp的发送线程send_thread 打包成flv数据包发送到流媒体服务器。
这里的线程配合比较复杂,涉及到rtmp的推流。将来在rtmp推流线程的博客做详细介绍,这里只介绍视频编码线程的工作。
贴一下函数调用堆栈,具体函数细节请阅读源码。
> obs.dll!os_sem_post(os_sem_data* sem) 行 139 C //发送信号通知send_thread 发送音视频数据
obs-outputs.dll!rtmp_stream_data(void* data, encoder_packet* packet) 行 1462 C
obs.dll!send_interleaved(obs_output* output) 行 1350 C // 取出音视频交织队列的第一个数据包发送给encoded_packet回调
obs.dll!interleave_packets(void* data, encoder_packet* packet) 行 1738 C //把视频包插入到音视频交织队列
obs.dll!send_packet(obs_encoder * encoder, encoder_callback* cb, encoder_packet* packet) 行 896 C
obs.dll!send_off_encoder_packet(obs_encoder* encoder, bool success, bool received, encoder_packet * pkt) 行 954 C
obs.dll!do_encode(obs_encoder* encoder, encoder_frame* frame) 行 989 C // 视频编码函数
obs.dll!receive_video(void* param, video_data* frame) 行 1056 C
obs.dll!video_output_cur_frame(video_output * video) 行 143 C //输出当前一帧图像送去编码发送
obs.dll!video_thread(void* param) 行 188 C //视频编码线程
w32-pthreads.dll!ptw32_threadStart(void * vthreadParms) 行 225 C
4.总结
通过上面的分析,大家应该对obs的视频编码线程有一个大概的了解,对照着源码,多下断点调试分析,很容易理清楚视频渲染线程和视频编码线程之间的配合。视频的编码输出线程驱动着rtmp推流线程工作,回头分析obs的rtmp推流模块再详细分析。视频的录制同推流一样,也是一种output,理解了rtmp的推流流程,理解视频的录制就很简单了。
以上都是个人工作当中对obs-studio开源项目的理解,难免有错误的地方,如果有欢迎指出。
若有帮助幸甚。