声明:本文章内容仅代表个人观点,不能保证完全的正确性,仅供参考!
先上个自己画的图,结合流程图和文字解释,理解起来会更快些
1、视频输出初始化
程序运行时,初始化OBS,视频相关的初始化是再mainWindow中进行的
OBSApp::OBSInit() -> mainWindow->OBSInit()
InitBasicConfig()读取appdata目录下配置文件中Video相关的参数,没有设置的参数使用接口
InitBasicConfigDefaults()接口中加载的默认参数
OBSBasic::RetsetVideo(),重置视频设置
obs_video_info ovi;获取视频设置的参数,包括:帧率,颜色格式,YUV颜色空间,YUV颜色范围,背景及
输出分辨率等
调用 AttemptToResetVideo() -> obs_reset_video(),将当前参数尝试重置给Video
停止当前的video,使用新参数ovi重新初始化video,obs_init_video(ovi)
obs_init_video(struct obs_video_info *ovi)
通过make_video_info函数,将ovi参数设置给video_out_info vi;
调用video_output_open函数启动视频数据输出线程
int video_output_open(video_t **video, struct video_output_info *info)
创建video_output *out对象,拷贝info中的数据到out->info,设置out->frame_time每一帧的时间差
启动线程函数 video_thread,并将out作为参数传入
初始化out->cache,调用video_frame_init将cache中每一帧的内容按照视频格式初始化
完成后,将out对象赋值给obs->video->video
其中线程执行函数video_thread就是视频输出线程,等待信号量video->update_semaphore 被唤醒
执行video_output_cur_frame函数,获取视频缓存中第一帧,从video->inputs中获取输出类型
调用编码器绑定的回调函数input->callback,receive_video(),进行视频数据编码.
而video->update_semaphore 信号量是在所有画面合成完成后被唤醒,后面将介绍是如何唤醒的
video->inputs中保存的是输出类型,包括推流和录像,后面将会说到是如何添加的
启动画面合成线程函数 obs_graphics_thread(),后面单独介绍画面合成线程的流程
至此视频输出的初始化完成,输出线程和画面合成线程已启动
2、obs-x264、obs-qsv11、obs-ffmpeg、rtmp模块加载
obs-x264是软编,obs-qsv11是intel硬编,obs-ffmpeg中包含ffmpeg_aac、ffmpeg_opus、以及nvenc编码
rtmp是推流模块
在OBSInit()函数初始化视频后,将执行加载模块的操作,这里将介绍obs-x264模块是怎么加载并且被调用的,
其他几个模块的加载是类似的;
OBSInit() -> AddExtraModulePaths(),添加加载模块的路径 -> obs_load_all_modules()加载所有模块
void obs_load_all_modules(void)
obs_find_modules()遍历所有模块目录,load_all_callback是对找到的模块执行的回调函数,
find_modules_in_path()在每个目录中查找dll文件,并执行函数process_found_module() -> 执行回调
也就是load_all_callback函数
load_all_callback() -> obs_open_module() -> os_dlopen()获取打开模块的句柄,接着执行的
load_module_exports函数获取模块中的接口地址绑定给obs_module各个函数指针,obs-x264模块只有
一个接口地址obs_module_load,被绑定至mod->load;将模块的一些信息填充,包括模块的名称,路径等
执行obs_init_module(module),该函数的作用就是为了调用刚才绑定的module->load()接口,也就是
obs-x264模块中的obs_module_load函数
obs_module_load() -> 宏obs_register_encoder(&obs_x264_encoder),其中obs_x264_encoder是个全局变量,
再obs-x264.c文件中完成了obs_x264_encoder的初始化,绑定函数接口,id,编码类型,编码方式;
-> obs_register_encoder_s()做一系列的检查 -> 宏REGISTER_OBS_DEF 将obs_x264_encoder添加到
obs->encoder_types
至此obs-x264模块的加载已完成,后面会介绍如何使用x264编码器
3、输出设置(简单模式)
模块加载完成后,将会对输出进行设置,以下是对视频输出设置的说明
OBSInit() -> ResetOutputs() 从配置文件中读取当前的视频设置是否是简单模式,重置outputHandler,并创建
简单输出模式的指针赋值给outputHandler -> CreateSimpleOutputHandler() -> new SimpleOutput -> 构造函数
SimpleOutput::SimpleOutput
获取当前编码类型:软编、硬编(QSV)、硬编(nvenc),调用LoadStreamingPreset_h264根据不同的编码类型
创建不同的编码器,赋值给成员变量h264Streaming指针,该指针后面将会添加到视频输出的编码器中
obs_video_encoder_create,根据编码器id创建编码器 -> 调用create_encoder()函数,根据编码器id和
编码器类型创建,在create_encoder函数中构造obs_encoder *encoder,根据id调用find_encoder()函数,从
obs->encoder_types找到指定的编码器,赋值给encoder->info,构造完成后,h264Streaming指针就是当前选
择的编码器
4、开启推流
OBSBasic::StartStreaming() -> outputHandler->StartStreaming()
SimpleOutput::StartStreaming(obs_service_t *service)
Active()是判断推流、录像、回放缓存是否激活;正常状态下返回false,也就需要执行SetupOututs()函数
SetupOutputs中调用了Update()函数,在Update中如果视频格式不是NV12或者I420,将编码器的首要格式设置
为NV12;更新编码器h264Streaming中的设置参数,如果编码器注册了update接口(软编和qsv有nvenc没有)
将新的参数更新到编码器中,回到SetupOutputs,调用obs_encoer_set_video,设置h264Streaming->media为
obs->video.video,设置timebase_num = fps_den;timebase_den = fps_num;注意这里是把帧率的分子分母反
着赋值给编码器,完成后回到StartStreaming创建推流的对象
调用obs_output_create()函数,根据输出id创建推流对象,与创建编码对象类似,推流对象在加载模块时已
添加到obs->output_types中,获取到的推流输出对象赋值给streamOutput指针
调用obs_output_set_video_encoder()函数,将推流输出streamOutput->video_encoder设置为编码器
创建好的h264Streaming
调用obs_output_start() -> obs_output_actual_start() 回调推流对象output->info.start()回调函数开启
推流,其中start绑定至rtmp_stream_start
static bool rtmp_stream_start(void *data)
创建线程,执行connect_thread()函数,
static void *connect_thread(void *data)
init_connect()初始化推流;调用free_packets清空stream->packets,获取推流设置,赋值到stream
try_connect()连接rtmp服务器,RTMP_Init(&stream->rtmp)初始化rtmp客户端,设置推流服务器地址、
用户名、密码、流地址、音频编码名称(为何没有添加视频编码名称,母鸡)
RTMP_Connect()连接rtmp服务器,RTMP_ConnectStream()连接rtmp流地址
init_send()启动发送函数,reset_semaphore()重置发送信号量,创建推流执行线程send_thread,发送
视频关键数据send_meta_data(),开启推流数据捕获obs_output_begin_data_capture()
其中 send_thread()线程函数:循环等待信号量stream->send_sem 被唤醒,唤醒后 get_next_packet()
取出队列中的第一个已编码数据包,执行 send_packet 函数,调用flv_packet_mux进行flv数据封包,
再调用RTMP_Write()发送数据包,完成视频数据推流.
bool obs_output_begin_data_capture(obs_output_t *output, uint32_t flags)
获取output->info也就是rtmp对象设置的flags,是否包含编码,音视频数据,绑定服务器;开启捕获,
hook_data_capture(),这里是绑定编码完成后的音视频数据到rtmp推流的回调函数,音视频编码完成的
数据回调都是interleave_packets(),start_audio_encoders()添加音频已编码数据捕获,
obs_encoder_start()添加视频已编码数据捕获,调用obs_encoder_start_internal(),将回调函数
interleave_packets,参数param也就是推流输出对象output构造成结构体encoder_callback cb;
static void interleave_packets (void *data, struct encoder_packet *packet)
调用obs_encoder_packet_create_instance(&out, packet);拷贝packet中的数据到局部变量out
中,其中进行malloc的时候,多申请了一个long类型长度的内存,这个pref是这个数据包的引用
计数器;如果音视频数据都收到时,调用apply_interleaved_packet_offset,这个函数是调整
时间补偿或者时间修复的吗?否则调用check_received接口将当前的音频或视频已收到标识设置
为true
was_started = output->received_audio && output->received_video;
......
if (was_started)
apply_interleaved_packet_offset(output, &out);
else
check_received(output, packet);
根据编码时间戳,将当前数据包插入到输出队列中,并将output->highest_audio_ts设置为当前
数据包的编码时间戳
insert_interleaved_packet(output, &out);
set_higher_ts(output, &out);
如果当前是否第一次收到了音频以及视频数据包,调用prune_interleaved_packets(output)对数
据包中的内容进行修剪,修剪规则如下:
先找出第一帧音频和第一帧视频的数据包,以第一帧视频数据包的index为基准,对比两个数据包
的时间戳的差值:
如果音频数据包的时间戳减去视频数据包的时间戳的数值大于每帧视频间隔的时间差,那么需
要删除这个音频数据包时间戳之前的所有音视频数据包
如果没有找到这样的音频数据包,那么就需要找出音视频数据包的时间戳差距最小的那个数
据包的index,如果这个index的值比第一帧视频数据包的index小,那么需要删除这个index
之前的所有数据包,如果比第一帧视频数据包的index大,那么需要删除第一帧视频数据包
之前的所有数据包
通过对第一次发送的音视频数据包的裁剪后,当前的待发送数据包中第一帧音视频数据包的时间戳
的差距最小,以达到首次发送的音视频数据是同步的,调整修正完成后的待发送数据包相关的时间
戳,再次确保发送的第一帧音视频数据包的准确性,并且重新调整待发送数据包的index,调用发送
数据包函数 send_interleaved
如果是后续收到的音视频数据包,则直接调用发送函数 send_interleaved
if (output->received_audio && output->received_video) {
if (!was_started) {
if (prune_interleaved_packets(output)) {
if (initialize_interleaved_packets(output)) {
resort_interleaved_packets(output);
send_interleaved(output);
}
}
} else {
send_interleaved(output);
}
}
static inline void send_interleaved (struct obs_output *output)
确认待发送数据包中的第一个数据包时间戳是合法的
if (!has_higher_opposing_ts(output, &out))
return;
把第一个数据包从队列中移除
da_erase(output->interleaved_packets, 0);
如果是视频数据包的话,在这里统计总的发送帧数
if (out.type == OBS_ENCODER_VIDEO) {
output->total_frames++;
调用output->info.encoded_packet回调函数 rtmp_stream_data,进入rtmp准备发送
static void rtmp_stream_data (void *data, struct encoder_packet *packet)
将数据包的数据拷贝至局部变量new_packet,数据包的引用技术+1,将new_packet添
加到待推流数据块中,视频数据包:add_video_packet,其中视频数据包在添加之前
检查是否有需要丢弃的帧,检查完成后调用add_packet,将数据包追加到
stream->packets队列中,添加成功后,唤醒信号量stream->send_sem,通知线程
send_thread(),执行发送
将cb添加到视频编码器encoder->callbacks队列中,如果添加的是第一个已编码数据推流回调,调用
add_connection(),启动音频数据输出捕获start_raw_video(),video->raw_active的值增加,说明下
raw_active的值,是控制视频数据是否输出的开关,后面在输出视频数据时要用到;调用
video_output_connect()函数,关联视频数据到编码的回调,创建video_input input结构体,将数据编
码的回调函数 receive_video,编码器对象encoder,视频数据信息,填充到input中,并将input添加到
video->inputs队列里,这个队列后面将会用到,其作用是合成后的视频数据调用这个队列中的回调
进行视频数据输出(音频数据添加编码回调跟视频类似,在add_connection时调用audio_output_connect
,构造audio_input input;将其加入到指定混音器mix->inputs中
static void receive_video(void *param, struct video_data *frame)
拷贝视频数据到encoder_frame enc_frame,调用do_encode()执行编码,在do_encode函数中,
初始化待完成的数据包encoder_packet pkt;调用编码器绑定的编码函数,此处举例x264编码
回调obs_x264_encode(),编码完成后调用send_packet,如果当前时视频帧的第一帧,需要单独
调用send_first_video_packet函数,将视频的SEI信息添加到视频数据中,调用之前绑定的推流
回调函数 interleave_packets(),进行视频数据发送
5、视频画面生成
在初始化视频时,启动了一个线程函数obs_graphics_thread(),所有画面源的合成,画面显示以及视频输出都在
这个函数里触发,说白了这里就时画面生成和输出的源头
void *obs_graphics_thread(void *param)
循环处理画面,会根据设置的视频帧数,每隔固定时间处理一次画面
tick_sources(),没有深入研究具体是什么内容
output_frame():输出当前视频帧
static inline void output_frame(bool raw_active)
调用render_video(),渲染视频数据,在开启推流和录像功能时,调用render_output_texture(),渲染输
出帧,并保存在video->convert_textures和video->output_textures中,再调用stage_output_texture
将画面保存到video->copy_surfaces
调用download_frme,从video->copy_surfaces中拷贝出当前视频帧数据到video_data *frame,这样就
拿到了需要输出的视频画面;
将frame传入output_video_data(),在该函数中,调用video_output_lock_frame()函数,拷贝
input->cache[last_add]给output_frame,需要注意的是,这个拷贝是将cache[]中的指针地址拷贝
过来了,通过格式转换函数例如copy_rgb_frame,将input_frame中的数据内容拷贝到output_frame,
实际上也就是将视频内容拷贝到了input->cache[last_add]中,再调用video_output_unlock_frame()函
数,唤醒信号量video->update_semaphore,通知线程video_thread视频输出数据已就绪,执行数据输出
、编码、rtmp推流
调用render_displays()将当前视频画面显示在窗口中,
sleep直到下一帧视频数据时间戳