各个方法分析

conference_file_play(): 调用放视频接口

  1. 新建对象conference_file_node_t *fnode,用于指向待播放的文件信息。
  2. fnode指向当前conference
  3. fnode属性设置:layer_id=-1, type = NODE_TYPE_FILE,new_fnode = 1;
  4. 将fnode赋给conference->fnode中(conference的fnode为空的,所以就直接赋值了,如果已经有了其他文件在播放,则直接附加在现有的fnode链表后面)

conference_video_fnode_check():用于将fnode绑定到layer,不断循环检查fnode(音频和视频文件的播放,都会触发这个方法,所以调用非常频繁)

  1. 如果fnode是音频文件,或者视频文件无法正常读取,则返回
  2. 调用conference_video_canvas_set_fnode_layer(): 将fnode与具体的conference layer绑定

conference_video_canvas_set_fnode_layer具体实现如下:

  1. 将canvas->layout_floor_id(断点后,发现值为0)对应的layer,与fnode绑定,
    1. layer->fnode = fnode;
    2. 将fnode的layer_id设置为才获取到的layerId
  2. 指定fnode对应的画布,fnode->canvas_id = canvas->canvas_id;
  3. 如果前面获取到的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);

  1. 没有找到可以给fnode绑定的layer,则还是继续读取视频帧,读出来就丢弃掉

conference_video_patch_fnode():用于从fnode读取一帧视频图像,保存到layer->img中

  1. 检查fnode是否已经绑定了layer,没有则返回(因为此处需要读取视频帧到layer上,layer都没有,还读个毛线)
  2. 获取fnode绑定的layer对象,mcu_layer_t *layer = &canvas->layers[fnode->layer_id];
  3. 读取一帧视频图像,switch_status_t status = switch_core_file_read_video(&fnode->fh, &file_frame, SVR_FLUSH);
  4. 检查fnode是否被锁定(会被谁锁定?被其他member?)
  5. 锁定:
    1. 将读取的视频帧赋给overlay_img layer->overlay_img = file_frame.img;
  6. 没锁定:
    1. 检查当前视频格式是否为I420,不是的话,就转为I420
    2. 将视频帧里面的图像数据赋给layer,layer->cur_img = file_frame.img;(断点查看过,file_frame里面除了img有数据,其他的属性都是空的)
  7. 将被赋给新图片数据的layer打上tagged标记,layer->tagged = 1,该标记用于后面对该新加的img尺寸进行加工,以便符合播放的尺寸要求。

conference_video_scale_and_patch(): 对layer上面的img进行裁剪,并贴到canvas->img上。

  1. 检查layer是否存在manual_border,存在的话,则将图像填充为边框色 switch_img_fill(IMG, x_pos, y_pos, img_w, img_h, &layer->canvas->border_color);
  2. 检查layer是否存在logo_img,存在的话,裁剪后,则将其贴到layer->img中
  3. 检查layer是否存在banner_img,存在的话,裁剪后,将其贴到layer->img中
  4. 检查layer是否存在overlay_img,存在的话,也处理之后,贴上去(怎么处理的,暂时没时间弄明白)
  5. 将layer->img贴到layer->canvas->img上

conference_video_write_canvas_image_to_codec_group():将帧进行H264编码,并保存到各个会议成员的帧缓存队列中

  1. 将图片帧进行编码, encode_status = switch_core_codec_encode_video(&codec_set->codec, frame);
    1. 此处会根据编码,分别调用VP8或者H264的编码器,进行编码,编码后的数据,仍然保存在frame中
  2. 遍历每个会议成员:
    1. 尝试从会议成员的帧缓存中获取一个空闲帧dupframe,然后将帧数据复制过去到dupframe中 (switch_frame_buffer_dup(imember->fb, frame, &dupframe)
    2. 将赋值过的空闲帧压入会议成员的帧缓存队列中,switch_frame_buffer_trypush(imember->fb, 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++。
image.png

播放结束,切换到下一个文件

文件:mod_conference.c,723行
如果检查到视频文件播放结束,则关闭当前的视频文件,然后读取下一个fnode指向的视频。

  1. 723: if (conference->fnode && conference->fnode->done) {
  2. conference_file_node_t *fnode;
  3. switch_memory_pool_t *pool;
  4. switch_mutex_lock(conference->file_mutex);
  5. if (conference->fnode->type != NODE_TYPE_SPEECH) {
  6. conference_file_close(conference, conference->fnode);
  7. }
  8. if (conference->canvases[0] && conference->fnode->layer_id > -1 ) {
  9. conference_video_canvas_del_fnode_layer(conference, conference->fnode);
  10. }
  11. fnode = conference->fnode;
  12. conference->fnode = conference->fnode->next;
  13. if (conference->fnode) {
  14. conference_video_fnode_check(conference->fnode, -1);
  15. }
  16. pool = fnode->pool;
  17. fnode = NULL;
  18. switch_core_destroy_memory_pool(&pool);
  19. switch_mutex_unlock(conference->file_mutex);
  20. }

读取视频文件帧

下面的代码会检查fnode是否为空,不为空的话,处理如下:
image.png

裁剪图像帧

对layer上面的img进行裁剪,并贴到canvas->img上。
image.png

编码图像帧,待发

对视频帧进行编码,并保存到各个会议成员的帧缓存队列中

  1. 4077 write_img = canvas->img;
  2. 4150 write_frame.img = write_img;
  3. 4174 if (min_members && conference_utils_test_flag(conference, CFLAG_MINIMIZE_VIDEO_ENCODING)) {
  4. //将图片帧数据,使用各个视频编码进行编码,并保存到各个会议成员的帧缓存队列中
  5. for (i = 0; canvas->write_codecs[i] && switch_core_codec_ready(&canvas->write_codecs[i]->codec) && i < MAX_MUX_CODECS; i++) {
  6. canvas->write_codecs[i]->frame.img = write_img;
  7. conference_video_write_canvas_image_to_codec_group(conference, canvas, canvas->write_codecs[i], i,
  8. timestamp, need_refresh, send_keyframe, need_reset);
  9. }
  10. }

收集流:from客户端

这个在收到客户端送来的视频帧之后的回调函数。
从代码中可以看出,如果是mux混屏模式,则处理后,直接扔到member->video_queue中,等待后续处理。
也就是说,在mux模式下,都是统一通过conference_video_muxing_write_thread_run来发送帧,而不会主动调用switch_core_session_write_video_frame来发送视频帧的。

  1. if (conference_utils_test_flag(member->conference, CFLAG_VIDEO_MUXING)) {
  2. switch_image_t *img_copy = NULL;
  3. int canvas_id = member->canvas_id;
  4. if (frame->img && (((member->video_layer_id > -1) && canvas_id > -1) || member->canvas) &&
  5. conference_utils_member_test_flag(member, MFLAG_CAN_BE_SEEN) &&
  6. !conference_utils_member_test_flag(member, MFLAG_HOLD) &&
  7. switch_queue_size(member->video_queue) < (int)member->conference->video_fps.fps &&
  8. !member->conference->canvases[canvas_id]->playing_video_file) {
  9. //对收到的视频进行处理:翻转、旋转、镜像
  10. if (conference_utils_member_test_flag(member, MFLAG_FLIP_VIDEO) ||
  11. conference_utils_member_test_flag(member, MFLAG_ROTATE_VIDEO) || conference_utils_member_test_flag(member, MFLAG_MIRROR_VIDEO)) {
  12. if (conference_utils_member_test_flag(member, MFLAG_ROTATE_VIDEO)) {
  13. if (member->flip_count++ > (int)(member->conference->video_fps.fps / 2)) {
  14. member->flip += 90;
  15. if (member->flip > 270) {
  16. member->flip = 0;
  17. }
  18. member->flip_count = 0;
  19. }
  20. switch_img_rotate_copy(frame->img, &img_copy, member->flip);
  21. } else if (conference_utils_member_test_flag(member, MFLAG_MIRROR_VIDEO)) {
  22. switch_img_mirror(frame->img, &img_copy);
  23. } else {
  24. switch_img_rotate_copy(frame->img, &img_copy, member->flip);
  25. }
  26. } else {
  27. switch_img_copy(frame->img, &img_copy);
  28. }
  29. //将收到的帧压入视频队列中,等待后续处理
  30. if (switch_queue_trypush(member->video_queue, img_copy) != SWITCH_STATUS_SUCCESS) {
  31. switch_img_free(&img_copy);
  32. }
  33. }
  34. switch_thread_rwlock_unlock(member->conference->rwlock);
  35. return SWITCH_STATUS_SUCCESS;
  36. }
  37. ...

发送流

每个会议成员都会启动线程,执行函数:conference_video_muxing_write_thread_run。
该函数用于读取帧缓存队列,然后发送视频帧。

  1. 2294: pop_status = switch_frame_buffer_pop(member->fb, &pop);
  2. ...
  3. 2309: if ((switch_size_t)pop != 1) {
  4. frame = (switch_frame_t *) pop;
  5. if (switch_test_flag(frame, SFF_ENCODED)) {
  6. switch_core_session_write_encoded_video_frame(member->session, frame, 0, 0);
  7. } else {
  8. switch_core_session_write_video_frame(member->session, frame, SWITCH_IO_FLAG_NONE, 0);
  9. }
  10. ...
  11. }

待整理

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中
image.png

QA

1. 为什么在单步调试的时候,单步到不相干的代码,客户端却收到了黑屏图像?

这个是发送视频是一个独立的线程,只负责从队列中取出内容然后直接发送出去。
我们单步停住时,每执行一步,那个发送视频线程也执行一步。
有可能单步时,已经将黑屏图像压入队列中,然后执行到其他地方,发送视频线程才来得及取出该黑屏图像并发送出去。

如何将异步送流修改为同步送流,以便定位问题?

文件:conference_video.c

  1. if (switch_frame_buffer_dup(imember->fb, frame, &dupframe) == SWITCH_STATUS_SUCCESS) {
  2. //原先的代码
  3. //if (switch_frame_buffer_trypush(imember->fb, dupframe) != SWITCH_STATUS_SUCCESS) {
  4. // switch_frame_buffer_free(imember->fb, &dupframe);
  5. //}
  6. //新的代码
  7. switch_core_session_write_encoded_video_frame(imember->session, dupframe, 0, 0);
  8. dupframe = NULL;
  9. }

image.png