各个方法分析
conference_file_play(): 调用放视频接口
- 新建对象conference_file_node_t *fnode,用于指向待播放的文件信息。
- fnode指向当前conference
- fnode属性设置:layer_id=-1, type = NODE_TYPE_FILE,new_fnode = 1;
- 将fnode赋给conference->fnode中(conference的fnode为空的,所以就直接赋值了,如果已经有了其他文件在播放,则直接附加在现有的fnode链表后面)
conference_video_fnode_check():用于将fnode绑定到layer,不断循环检查fnode(音频和视频文件的播放,都会触发这个方法,所以调用非常频繁)
- 如果fnode是音频文件,或者视频文件无法正常读取,则返回
- 调用conference_video_canvas_set_fnode_layer(): 将fnode与具体的conference layer绑定
conference_video_canvas_set_fnode_layer具体实现如下:
- 将canvas->layout_floor_id(断点后,发现值为0)对应的layer,与fnode绑定,
- layer->fnode = fnode;
- 将fnode的layer_id设置为才获取到的layerId
- 指定fnode对应的画布,fnode->canvas_id = canvas->canvas_id;
- 如果前面获取到的layer绑定了会议成员,则调用conference_video_detach_video_layer(),解除会议成员member与layer的绑定关系。
conference_video_detach_video_layer中会调用conference_video_release_canvas方法,这个方法是干嘛的? conference_video_release_canvas(): 这个函数不是直接内存释放canvas,只是释放了画布的锁。 主要是要执行这个代码:switch_mutex_unlock(canvas->conference->canvas_mutex);
- 没有找到可以给fnode绑定的layer,则还是继续读取视频帧,读出来就丢弃掉
conference_video_patch_fnode():用于从fnode读取一帧视频图像,保存到layer->img中
- 检查fnode是否已经绑定了layer,没有则返回(因为此处需要读取视频帧到layer上,layer都没有,还读个毛线)
- 获取fnode绑定的layer对象,
mcu_layer_t *layer = &canvas->layers[fnode->layer_id]; - 读取一帧视频图像,
switch_status_t status = switch_core_file_read_video(&fnode->fh, &file_frame, SVR_FLUSH); - 检查fnode是否被锁定(会被谁锁定?被其他member?)
- 锁定:
- 将读取的视频帧赋给overlay_img
layer->overlay_img = file_frame.img;
- 将读取的视频帧赋给overlay_img
- 没锁定:
- 检查当前视频格式是否为I420,不是的话,就转为I420
- 将视频帧里面的图像数据赋给layer,layer->cur_img = file_frame.img;(断点查看过,file_frame里面除了img有数据,其他的属性都是空的)
- 将被赋给新图片数据的layer打上tagged标记,
layer->tagged = 1,该标记用于后面对该新加的img尺寸进行加工,以便符合播放的尺寸要求。
conference_video_scale_and_patch(): 对layer上面的img进行裁剪,并贴到canvas->img上。
- 检查layer是否存在manual_border,存在的话,则将图像填充为边框色
switch_img_fill(IMG, x_pos, y_pos, img_w, img_h, &layer->canvas->border_color); - 检查layer是否存在logo_img,存在的话,裁剪后,则将其贴到layer->img中
- 检查layer是否存在banner_img,存在的话,裁剪后,将其贴到layer->img中
- 检查layer是否存在overlay_img,存在的话,也处理之后,贴上去(怎么处理的,暂时没时间弄明白)
- 将layer->img贴到layer->canvas->img上
conference_video_write_canvas_image_to_codec_group():将帧进行H264编码,并保存到各个会议成员的帧缓存队列中
- 将图片帧进行编码,
encode_status = switch_core_codec_encode_video(&codec_set->codec, frame);- 此处会根据编码,分别调用VP8或者H264的编码器,进行编码,编码后的数据,仍然保存在frame中
- 遍历每个会议成员:
- 尝试从会议成员的帧缓存中获取一个空闲帧dupframe,然后将帧数据复制过去到dupframe中
(switch_frame_buffer_dup(imember->fb, frame, &dupframe) - 将赋值过的空闲帧压入会议成员的帧缓存队列中,
switch_frame_buffer_trypush(imember->fb, dupframe)
- 尝试从会议成员的帧缓存中获取一个空闲帧dupframe,然后将帧数据复制过去到dupframe中
conference_video_pop_next_image():读取会议成员的视频
主要是通过下面的代码,来读取会议成员的视频图片帧。switch_queue_trypop(member->video_queue, &pop)
收集流:from视频文件
函数:conference_video_muxing_thread_run
视频文件切换流程
判断视频文件是否播放结束
文件:mod_conference.c,509行
如果文件已经播放到了末尾,只要不是循环播放,则将文件置为播放结束,fnode->done++。
播放结束,切换到下一个文件
文件:mod_conference.c,723行
如果检查到视频文件播放结束,则关闭当前的视频文件,然后读取下一个fnode指向的视频。
723: if (conference->fnode && conference->fnode->done) {conference_file_node_t *fnode;switch_memory_pool_t *pool;switch_mutex_lock(conference->file_mutex);if (conference->fnode->type != NODE_TYPE_SPEECH) {conference_file_close(conference, conference->fnode);}if (conference->canvases[0] && conference->fnode->layer_id > -1 ) {conference_video_canvas_del_fnode_layer(conference, conference->fnode);}fnode = conference->fnode;conference->fnode = conference->fnode->next;if (conference->fnode) {conference_video_fnode_check(conference->fnode, -1);}pool = fnode->pool;fnode = NULL;switch_core_destroy_memory_pool(&pool);switch_mutex_unlock(conference->file_mutex);}
读取视频文件帧
裁剪图像帧
对layer上面的img进行裁剪,并贴到canvas->img上。
编码图像帧,待发
对视频帧进行编码,并保存到各个会议成员的帧缓存队列中
4077: write_img = canvas->img;4150: write_frame.img = write_img;4174 if (min_members && conference_utils_test_flag(conference, CFLAG_MINIMIZE_VIDEO_ENCODING)) {//将图片帧数据,使用各个视频编码进行编码,并保存到各个会议成员的帧缓存队列中for (i = 0; canvas->write_codecs[i] && switch_core_codec_ready(&canvas->write_codecs[i]->codec) && i < MAX_MUX_CODECS; i++) {canvas->write_codecs[i]->frame.img = write_img;conference_video_write_canvas_image_to_codec_group(conference, canvas, canvas->write_codecs[i], i,timestamp, need_refresh, send_keyframe, need_reset);}}
收集流:from客户端
这个在收到客户端送来的视频帧之后的回调函数。
从代码中可以看出,如果是mux混屏模式,则处理后,直接扔到member->video_queue中,等待后续处理。
也就是说,在mux模式下,都是统一通过conference_video_muxing_write_thread_run来发送帧,而不会主动调用switch_core_session_write_video_frame来发送视频帧的。
if (conference_utils_test_flag(member->conference, CFLAG_VIDEO_MUXING)) {switch_image_t *img_copy = NULL;int canvas_id = member->canvas_id;if (frame->img && (((member->video_layer_id > -1) && canvas_id > -1) || member->canvas) &&conference_utils_member_test_flag(member, MFLAG_CAN_BE_SEEN) &&!conference_utils_member_test_flag(member, MFLAG_HOLD) &&switch_queue_size(member->video_queue) < (int)member->conference->video_fps.fps &&!member->conference->canvases[canvas_id]->playing_video_file) {//对收到的视频进行处理:翻转、旋转、镜像if (conference_utils_member_test_flag(member, MFLAG_FLIP_VIDEO) ||conference_utils_member_test_flag(member, MFLAG_ROTATE_VIDEO) || conference_utils_member_test_flag(member, MFLAG_MIRROR_VIDEO)) {if (conference_utils_member_test_flag(member, MFLAG_ROTATE_VIDEO)) {if (member->flip_count++ > (int)(member->conference->video_fps.fps / 2)) {member->flip += 90;if (member->flip > 270) {member->flip = 0;}member->flip_count = 0;}switch_img_rotate_copy(frame->img, &img_copy, member->flip);} else if (conference_utils_member_test_flag(member, MFLAG_MIRROR_VIDEO)) {switch_img_mirror(frame->img, &img_copy);} else {switch_img_rotate_copy(frame->img, &img_copy, member->flip);}} else {switch_img_copy(frame->img, &img_copy);}//将收到的帧压入视频队列中,等待后续处理if (switch_queue_trypush(member->video_queue, img_copy) != SWITCH_STATUS_SUCCESS) {switch_img_free(&img_copy);}}switch_thread_rwlock_unlock(member->conference->rwlock);return SWITCH_STATUS_SUCCESS;}...
发送流
每个会议成员都会启动线程,执行函数:conference_video_muxing_write_thread_run。
该函数用于读取帧缓存队列,然后发送视频帧。
2294: pop_status = switch_frame_buffer_pop(member->fb, &pop);...2309: if ((switch_size_t)pop != 1) {frame = (switch_frame_t *) pop;if (switch_test_flag(frame, SFF_ENCODED)) {switch_core_session_write_encoded_video_frame(member->session, frame, 0, 0);} else {switch_core_session_write_video_frame(member->session, frame, SWITCH_IO_FLAG_NONE, 0);}...}
待整理
mod_conference.c入会时:
2486 switch_core_session_set_video_read_callback(session, conference_video_thread_callback, (void *)&member);
//读取客户端视频后的回调:
conference_video_thread_callback:
读取之后,放到member->video_queue中
QA
1. 为什么在单步调试的时候,单步到不相干的代码,客户端却收到了黑屏图像?
这个是发送视频是一个独立的线程,只负责从队列中取出内容然后直接发送出去。
我们单步停住时,每执行一步,那个发送视频线程也执行一步。
有可能单步时,已经将黑屏图像压入队列中,然后执行到其他地方,发送视频线程才来得及取出该黑屏图像并发送出去。
如何将异步送流修改为同步送流,以便定位问题?
文件:conference_video.c
if (switch_frame_buffer_dup(imember->fb, frame, &dupframe) == SWITCH_STATUS_SUCCESS) {//原先的代码//if (switch_frame_buffer_trypush(imember->fb, dupframe) != SWITCH_STATUS_SUCCESS) {// switch_frame_buffer_free(imember->fb, &dupframe);//}//新的代码switch_core_session_write_encoded_video_frame(imember->session, dupframe, 0, 0);dupframe = NULL;}

