各个方法分析
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;
}