在初始化视频时,启动了一个线程函数obs_video_thread(),所有画面源的合成,画面显示以及视频输出都在这个函数里触发。
void *obs_video_thread(void *param) { uint64_t last_time = 0; uint64_t interval = video_output_get_frame_time(obs->video.video); uint64_t frame_time_total_ns = 0; uint64_t fps_total_ns = 0; uint32_t fps_total_frames = 0; obs->video.video_time = os_gettime_ns(); os_set_thread_name("obs-core: graphics thread"); const char *video_thread_name = profile_store_name(obs_get_profiler_name_store(), "arcvideo_director_video_thread(%g"NBSP"ms)", interval / 1000000.); profile_register_root(video_thread_name, interval); while (!video_output_stopped(obs->video.video)) { uint64_t frame_start = os_gettime_ns(); uint64_t frame_time_ns; profile_start(video_thread_name); profile_start(tick_sources_name); last_time = tick_sources(obs->video.video_time, last_time); profile_end(tick_sources_name); profile_start(render_displays_name); render_displays(); profile_end(render_displays_name); profile_start(output_frame_name); output_frame(&obs->video.main_track, obs_get_video_context(), obs_get_video()); for (int i = 0; i < MAX_AUX_TRACK_CHANNELS; i++) { output_frame(&obs->video.aux_track[i], obs_get_aux_video_context(), obs_get_aux_video(i)); } profile_end(output_frame_name); frame_time_ns = os_gettime_ns() - frame_start; profile_end(video_thread_name); profile_reenable_thread(); video_sleep(&obs->video, &obs->video.video_time, interval); frame_time_total_ns += frame_time_ns; fps_total_ns += (obs->video.video_time - last_time); fps_total_frames++; if (fps_total_ns >= 1000000000ULL) { obs->video.video_fps = (double)fps_total_frames / ((double)fps_total_ns / 1000000000.0); obs->video.video_avg_frame_time_ns = frame_time_total_ns / (uint64_t)fps_total_frames; frame_time_total_ns = 0; fps_total_ns = 0; fps_total_frames = 0; } } UNUSED_PARAMETER(param); return NULL; }tick_sources()遍历每个sources;
static uint64_t tick_sources(uint64_t cur_time, uint64_t last_time) { struct obs_core_data *data = &obs->data; struct obs_source *source; uint64_t delta_time; float seconds; if (!last_time) last_time = cur_time - video_output_get_frame_time(obs->video.video); delta_time = cur_time - last_time; seconds = (float)((double)delta_time / 1000000000.0); pthread_mutex_lock(&data->sources_mutex); /* call the tick function of each source */ source = data->first_source; while (source) { obs_source_video_tick(source, seconds); source = (struct obs_source*)source->context.next; } pthread_mutex_unlock(&data->sources_mutex); return cur_time; }调用render_displays()将当前视频画面显示在窗口中;
/* in obs-display.c */ extern void render_display(struct obs_display *display); static inline void render_displays(void) { struct obs_display *display; if (!obs->data.valid) return; gs_enter_context(obs->video.graphics); /* render extra displays/swaps */ pthread_mutex_lock(&obs->data.displays_mutex); display = obs->data.first_display; while (display) { render_display(display); display = display->next; } pthread_mutex_unlock(&obs->data.displays_mutex); gs_leave_context(); }output_frame()输出当前视频帧;
static inline void output_frame(struct obs_video_track* track, struct video_c_t *context, struct video_d_t *data) { struct obs_core_video *video = &obs->video; int cur_texture = track->cur_texture; int prev_texture = cur_texture == 0 ? NUM_TEXTURES-1 : cur_texture-1; struct video_data frame; bool frame_ready; memset(&frame, 0, sizeof(struct video_data)); profile_start(output_frame_gs_context_name); gs_enter_context(video->graphics); profile_start(output_frame_render_video_name); render_video(video, track, cur_texture, prev_texture); profile_end(output_frame_render_video_name); //render_video_ex(video, cur_texture, prev_texture); profile_start(output_frame_download_frame_name); frame_ready = download_frame(video, track, prev_texture, &frame); profile_end(output_frame_download_frame_name); profile_start(output_frame_gs_flush_name); gs_flush(); profile_end(output_frame_gs_flush_name); gs_leave_context(); profile_end(output_frame_gs_context_name); if (frame_ready) { struct obs_vframe_info vframe_info; circlebuf_pop_front(&track->vframe_info_buffer, &vframe_info, sizeof(vframe_info)); frame.timestamp = vframe_info.timestamp; profile_start(output_frame_output_video_data_name); output_video_data(video, context, data, &frame, vframe_info.count, track == &video->main_track || track->is_pgm); profile_end(output_frame_output_video_data_name); } if (++track->cur_texture == NUM_TEXTURES) track->cur_texture = 0; } 调用 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;
这样就拿到了需要输出的视频画面;
static inline void output_video_data(struct obs_core_video *video, struct video_c_t *video_context, struct video_d_t *video_data, struct video_data *input_frame, int count, bool enableFtb) { const struct video_output_info *info; struct video_frame output_frame; bool locked; info = video_output_get_info(video_context); locked = video_output_lock_frame(video_data, &output_frame, count, input_frame->timestamp); if (locked) { /*modified by yshe end*/ if (video->forceblack && enableFtb) { //fill the output_frame to black // memset(output_frame.data[0], 0,output_frame.linesize[0]*info->height*3/2); video_frame_setblack(&output_frame, info->format, info->height); } else { if (video->gpu_conversion) { set_gpu_converted_data(video, &output_frame, input_frame, info); } else if (format_is_yuv(info->format)) { convert_frame(&output_frame, input_frame, info); } else { copy_rgbx_frame(&output_frame, input_frame, info); } } video_output_unlock_frame(video_data); } }将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直到下一帧视频数据时间戳;
在初始化视频时,启动了一个线程函数video_thread(),这个函数一直等待视频帧数据准备就绪的信号唤醒;
static void *video_thread(void *param) { struct video_output *video = param; os_set_thread_name("video-io: video thread"); const char *video_thread_name = profile_store_name(obs_get_profiler_name_store(), "video_thread(%s)", video->context.info.name); while (os_sem_wait(video->data.update_semaphore) == 0) { if (video->context.stop) break; profile_start(video_thread_name); while (!video->context.stop && !video_output_cur_frame(&video->data)) { video->data.total_frames++; } video->data.total_frames++; profile_end(video_thread_name); profile_reenable_thread(); } return NULL; }在输出的一帧视频画面合成后唤醒该信号,在video_output_unlock_frame()里触发该信号
void video_output_unlock_frame(video_d_t *video) { if (!video) return; pthread_mutex_lock(&video->data_mutex); video->available_frames--; os_sem_post(video->update_semaphore); pthread_mutex_unlock(&video->data_mutex); }等到信号后执行video_output_cur_frame函数,获取视频缓存中第一帧,从video->inputs中获取输出类型调用编码器绑定的回调函数input->callback,receive_video(),进行视频数据编码,而video->update_semaphore 信号量是在所有画面合成完成后被唤醒;
video->inputs中保存的是输出类型,包括推流和录像,后面将会说到是如何添加的。
static inline bool video_output_cur_frame(struct video_output_data *video) { struct cached_frame_info *frame_info; bool complete; bool skipped; /* -------------------------------- */ 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; if (scale_video_output(input, &frame)) input->callback(input->param, &frame); } pthread_mutex_unlock(&video->input_mutex); /* -------------------------------- */ pthread_mutex_lock(&video->data_mutex); frame_info->frame.timestamp += video->frame_time; complete = --frame_info->count == 0; skipped = frame_info->skipped > 0; if (complete) { if (++video->first_added == video->cache_size) video->first_added = 0; if (++video->available_frames == video->cache_size) video->last_added = video->first_added; } else if (skipped) { --frame_info->skipped; ++video->skipped_frames; } pthread_mutex_unlock(&video->data_mutex); /* -------------------------------- */ return complete; }在receive_video中调用do_encode()进行编码;
static const char *receive_video_name = "receive_video"; static void receive_video(void *param, struct video_data *frame) { profile_start(receive_video_name); struct obs_encoder *encoder = param; struct obs_encoder *pair = encoder->paired_encoder; struct encoder_frame enc_frame; if (!encoder->first_received && pair) { if (!pair->first_received || pair->first_raw_ts > frame->timestamp) { goto wait_for_audio; } } //memset(&enc_frame, 0, sizeof(struct encoder_frame)); for (size_t i = 0; i < MAX_AV_PLANES; i++) { enc_frame.data[i] = frame->data[i]; enc_frame.linesize[i] = frame->linesize[i]; } if (!encoder->start_ts) encoder->start_ts = frame->timestamp; enc_frame.frames = 1; enc_frame.pts = encoder->cur_pts; do_encode(encoder, &enc_frame); encoder->cur_pts += encoder->timebase_num; wait_for_audio: profile_end(receive_video_name); }在do_encode中根据不同的编码器名称进行编码回调,各个编码器模块在程序开始加载的时候会进行注册,编码完成后调用绑定的编码完成回调。
static const char *do_encode_name = "do_encode"; static inline void do_encode(struct obs_encoder *encoder, struct encoder_frame *frame) { profile_start(do_encode_name); if (!encoder->profile_encoder_encode_name) encoder->profile_encoder_encode_name = profile_store_name(obs_get_profiler_name_store(), "encode(%s)", encoder->context.name); struct encoder_packet pkt = {0}; bool received = false; bool success; pkt.timebase_num = encoder->timebase_num; pkt.timebase_den = encoder->timebase_den; pkt.encoder = encoder; profile_start(encoder->profile_encoder_encode_name); success = encoder->info.encode(encoder->context.data, frame, &pkt, &received); profile_end(encoder->profile_encoder_encode_name); if (!success) { pkt.error_code = 1; pthread_mutex_lock(&encoder->callbacks_mutex); for (size_t i = encoder->callbacks.num; i > 0; i--) { struct encoder_callback *cb; cb = encoder->callbacks.array + (i - 1); send_packet(encoder, cb, &pkt); } pthread_mutex_unlock(&encoder->callbacks_mutex); //full_stop(encoder); blog(LOG_INFO, "Error encoding with encoder '%s'", encoder->context.name); goto error; } if (received) { if (!encoder->first_received) { encoder->offset_usec = packet_dts_usec(&pkt); encoder->first_received = true; } /* we use system time here to ensure sync with other encoders, * you do not want to use relative timestamps here */ pkt.dts_usec = encoder->start_ts / 1000 + packet_dts_usec(&pkt) - encoder->offset_usec; pkt.sys_dts_usec = pkt.dts_usec; pthread_mutex_lock(&encoder->callbacks_mutex); for (size_t i = encoder->callbacks.num; i > 0; i--) { struct encoder_callback *cb; cb = encoder->callbacks.array+(i-1); send_packet(encoder, cb, &pkt); } pthread_mutex_unlock(&encoder->callbacks_mutex); } error: profile_end(do_encode_name); }通过回调得到编码后的数据,然后rtmp对数据封装推流输出,下一篇介绍详细的推流步骤。