PJSIP视频卡顿的原因以及解决办法
现象:网络状况很好,没有丢包,视频也不会花屏,但是不流畅卡顿很厉害,这个时候作为用户是崩溃的。
分析:花屏和卡顿两个现象是不一样的原因造成。
1、花屏是解码宏块出现错误导致,抛开解码器自身可能存在的问题,直接原因99%都是数据错误造成,而数据错误的直接原因就是网络丢包,这里不涉及暂不讨论。
2、视频不流畅卡顿,可能的原因有几个:
(1)网络抖动严重,数据包以脉冲式的方式进行传输,应对方式是解码端设计抖动缓冲,延迟解码时间使视频平滑。
(2)编码器不能以设定的帧率输出数据,但是RTP传输时仍然按照固定TS增加时间戳,比如设置为30帧,但实际只能输出20帧,这个也可能造成解码端渲染视频速度不平滑的问题(这个在手机移动端比较明显),通常的处理是按照从编码器输出数据时系统时间的增量来换算对应的TS增量。
(3)显示端的渲染速度不正确,没有按照发送端的速率进行渲染。理论上如果渲染端按照编码发送端的速率进行渲染,肯定可以流畅的呈现视频,即使视频出现花屏,也可以做到流畅的花屏(囧)。
那么这里的关键点在于渲染端怎么样知道发送端的速率,常规的做法是假定编码器稳定,以恒定的时间戳增量进行编码,比如设定帧率30,则每一帧的时间戳TS步进是90000/30=3000,那么解码端只需要知道这个时间戳增量,就可以知晓帧率,从而以合适的速率进行渲染呈现。
实际情况中因编码器厂家差异,有的编码器并不能按照预设的帧率输出数据(也就是上面的第二种情况),如果这时仍然按照固定的时间戳步进去打时间戳,则也会出现解码端不流畅的问题,变通的做法是需要对比每一帧前后的时间戳增量,如果计算出来的帧率较之前有较大变化(比如大于2帧)那么更改渲染线程的速率。
事实上在融合通信领域,手机app因为有前后摄像头的关系,很有可能切换摄像头之后分辨率、帧率、码率都发生了变化,所以解码渲染端需要实时计算每一帧的ts,学习新的视频帧率,动态调整渲染速率。
PJSIP中的渲染速率控制
针对第一点,pj有良好的抖动缓冲,第二点需要编码端合理的处理,下面主要讲讲第三点:
1、默认的渲染速率是按照解码帧率进行初始化(比如30),代码位于vid_port.c的pjmedia_vid_port_create中。
param.usec_interval = PJMEDIA_PTIME(&vfd->fps);
param.clock_rate = prm->vidparam.clock_rate;
status = pjmedia_clock_create2(pool, ¶m,
PJMEDIA_CLOCK_NO_HIGHEST_PRIO,
(vp->dir & PJMEDIA_DIR_ENCODING) ?
&enc_clock_cb : &dec_clock_cb,
vp, &vp->clock);
if (status != PJ_SUCCESS)
goto on_error;
2、在首次解码成功后,解码器将发出PJMEDIA_EVENT_FMT_CHANGED事件,代码位于ffmpeg_vid_codecs.c的check_decode_result中(如果采用openH264编解码器,则在oh264_got_decoded_frame中)。
/* Broadcast format changed event */
pjmedia_event_init(&event, PJMEDIA_EVENT_FMT_CHANGED, ts, codec);
event.data.fmt_changed.dir = PJMEDIA_DIR_DECODING;
pj_memcpy(&event.data.fmt_changed.new_fmt, &ff->param.dec_fmt,
sizeof(ff->param.dec_fmt));
pjmedia_event_publish(NULL, codec, &event, 0);
这个事件的广播主要意义在于通知上层真实的视频分辨率,从而调整对应的存储空间,以及设置相应的码流属性。
3、解码器发出的PJMEDIA_EVENT_FMT_CHANGED时间广播,主要由vid_stream.c消费,代码在stream_event_cb中。
/*
* Handle events from stream components.
*/
static pj_status_t stream_event_cb(pjmedia_event *event,
void *user_data)
{
pjmedia_vid_stream *stream = (pjmedia_vid_stream*)user_data;
if (event->epub == stream->codec) {
/* This is codec event */
switch (event->type) {
case PJMEDIA_EVENT_FMT_CHANGED:
/* Copy the event to avoid deadlock if we publish the event
* now. This happens because fmt_event may trigger restart
* while we're still holding the jb_mutex.
*/
pj_memcpy(&stream->fmt_event, event, sizeof(*event));
return PJ_SUCCESS;
case PJMEDIA_EVENT_KEYFRAME_MISSING:
/* Republish this event later from get_frame(). */
pj_memcpy(&stream->miss_keyframe_event, event, sizeof(*event));
return PJ_SUCCESS;
default:
break;
}
}
return pjmedia_event_publish(NULL, stream, event, 0);
}
这里仅仅是将数据进行copy,进一步的处理在于get_frame中。
/* Update stream info and decoding channel port info */
if (fmt_chg_data->dir == PJMEDIA_DIR_DECODING) {
pjmedia_format_copy(&stream->info.codec_param->dec_fmt,
&fmt_chg_data->new_fmt);
pjmedia_format_copy(&stream->dec->port.info.fmt,
&fmt_chg_data->new_fmt);
/* Override the framerate to be 1.5x higher in the event
* for the renderer.
*/
fmt_chg_data->new_fmt.det.vid.fps.num *= 3;
fmt_chg_data->new_fmt.det.vid.fps.num /= 2;
}
这里将帧率整整放大了1.5倍再广播事件,可能设计者考虑解码和渲染消耗的缘故将速率放大1.5倍,但是实际上这个值仍然太大了,我将这个值调整为1.2倍。
4、vid_stream.c发出的广播,由vid_port.c消费,代码位于client_port_event_cb
static pj_status_t client_port_event_cb(pjmedia_event *event,
void *user_data)
{
pjmedia_vid_port *vp = (pjmedia_vid_port*)user_data;
if (event->type == PJMEDIA_EVENT_FMT_CHANGED) {
const pjmedia_video_format_detail *vfd;
const pjmedia_video_format_detail *vfd_cur;
pjmedia_vid_dev_param vid_param;
pj_status_t status;
/* Retrieve the current video format detail */
pjmedia_vid_dev_stream_get_param(vp->strm, &vid_param);
vfd_cur = pjmedia_format_get_video_format_detail(
&vid_param.fmt, PJ_TRUE);
if (!vfd_cur)
return PJMEDIA_EVID_BADFORMAT;
/* Retrieve the new video format detail */
vfd = pjmedia_format_get_video_format_detail(
&event->data.fmt_changed.new_fmt, PJ_TRUE);
if (!vfd || !vfd->fps.num || !vfd->fps.denum)
return PJMEDIA_EVID_BADFORMAT;
/* Ticket #1876: if this is a passive renderer and only frame rate is
* changing, simply modify the clock.
*/
if (vp->dir == PJMEDIA_DIR_RENDER &&
vp->stream_role == ROLE_PASSIVE && vp->role == ROLE_ACTIVE)
{
pj_bool_t fps_only;
pjmedia_video_format_detail tmp_vfd;
tmp_vfd = *vfd_cur;
tmp_vfd.fps = vfd->fps;
fps_only = pj_memcmp(vfd, &tmp_vfd, sizeof(*vfd)) == 0;
if (fps_only) {
pjmedia_clock_param clock_param;
clock_param.usec_interval = PJMEDIA_PTIME(&vfd->fps);
clock_param.clock_rate = vid_param.clock_rate;
pjmedia_clock_modify(vp->clock, &clock_param);
return pjmedia_event_publish(NULL, vp, event,
PJMEDIA_EVENT_PUBLISH_POST_EVENT);
}
}
/* Ticket #1827:
* Stopping video port should not be necessary here because
* it will also try to stop the clock, from inside the clock's
* own thread, so it may get stuck. We just stop the video device
* stream instead.
* pjmedia_vid_port_stop(vp);
*/
pjmedia_vid_dev_stream_stop(vp->strm);
/* Change the destination format to the new format */
pjmedia_format_copy(&vp->conv.conv_param.src,
&event->data.fmt_changed.new_fmt);
/* Only copy the size here */
vp->conv.conv_param.dst.det.vid.size =
event->data.fmt_changed.new_fmt.det.vid.size;
status = create_converter(vp);
if (status != PJ_SUCCESS) {
PJ_PERROR(4,(THIS_FILE, status, "Error recreating converter"));
return status;
}
if (vid_param.fmt.id != vp->conv.conv_param.dst.id ||
(vid_param.fmt.det.vid.size.h !=
vp->conv.conv_param.dst.det.vid.size.h) ||
(vid_param.fmt.det.vid.size.w !=
vp->conv.conv_param.dst.det.vid.size.w))
{
status = pjmedia_vid_dev_stream_set_cap(vp->strm,
PJMEDIA_VID_DEV_CAP_FORMAT,
&vp->conv.conv_param.dst);
if (status != PJ_SUCCESS) {
PJ_LOG(3, (THIS_FILE, "failure in changing the format of the "
"video device"));
PJ_LOG(3, (THIS_FILE, "reverting to its original format: %s",
status != PJMEDIA_EVID_ERR ? "success" :
"failure"));
return status;
}
}
if (vp->stream_role == ROLE_PASSIVE) {
pjmedia_clock_param clock_param;
/**
* Initially, frm_buf was allocated the biggest
* supported size, so we do not need to re-allocate
* the buffer here.
*/
/* Adjust the clock */
clock_param.usec_interval = PJMEDIA_PTIME(&vfd->fps);
clock_param.clock_rate = vid_param.clock_rate;
pjmedia_clock_modify(vp->clock, &clock_param);
}
/* pjmedia_vid_port_start(vp); */
pjmedia_vid_dev_stream_start(vp->strm);
}
/* Republish the event, post the event to the event manager
* to avoid deadlock if vidport is trying to stop the clock.
*/
return pjmedia_event_publish(NULL, vp, event,
PJMEDIA_EVENT_PUBLISH_POST_EVENT);
}
其中两处pjmedia_clock_modify改变了渲染线程频率。
5、上面获知的视频帧率并不是真实的值,只是提前设置好的解码帧率,真实的值是通过解码成功后进行学习计算来,代码位于vid_stream.c的decode_frame中
/* Learn remote frame rate after successful decoding */
if (frame->type == PJMEDIA_FRAME_TYPE_VIDEO && frame->size)
{
/* Only check remote frame rate when timestamp is not wrapping and
* sequence is increased by 1.
*/
if (last_ts > stream->last_dec_ts &&
frm_first_seq - stream->last_dec_seq == 1)
{
pj_uint32_t ts_diff;
pjmedia_video_format_detail *vfd;
ts_diff = last_ts - stream->last_dec_ts;
vfd = pjmedia_format_get_video_format_detail(
&channel->port.info.fmt, PJ_TRUE);
if (stream->info.codec_info.clock_rate * vfd->fps.denum !=
vfd->fps.num * ts_diff)
{
/* Frame rate changed, update decoding port info */
if (stream->info.codec_info.clock_rate % ts_diff == 0) {
vfd->fps.num = stream->info.codec_info.clock_rate/ts_diff;
vfd->fps.denum = 1;
} else {
vfd->fps.num = stream->info.codec_info.clock_rate;
vfd->fps.denum = ts_diff;
}
/* Update stream info */
stream->info.codec_param->dec_fmt.det.vid.fps = vfd->fps;
/* Publish PJMEDIA_EVENT_FMT_CHANGED event if frame rate
* increased and not exceeding 100fps.
*/
if (vfd->fps.num/vfd->fps.denum <= 100 &&
vfd->fps.num * stream->dec_max_fps.denum >
stream->dec_max_fps.num * vfd->fps.denum)
{
pjmedia_event *event = &stream->fmt_event;
/* Update max fps of decoding dir */
stream->dec_max_fps = vfd->fps;
/* Use the buffered format changed event:
* - just update the framerate if there is pending event,
* - otherwise, init the whole event.
*/
if (stream->fmt_event.type != PJMEDIA_EVENT_NONE) {
event->data.fmt_changed.new_fmt.det.vid.fps = vfd->fps;
} else {
pjmedia_event_init(event, PJMEDIA_EVENT_FMT_CHANGED,
&frame->timestamp, stream);
event->data.fmt_changed.dir = PJMEDIA_DIR_DECODING;
pj_memcpy(&event->data.fmt_changed.new_fmt,
&stream->info.codec_param->dec_fmt,
sizeof(pjmedia_format));
}
}
}
}
/* Update last frame seq and timestamp */
stream->last_dec_seq = frm_last_seq;
stream->last_dec_ts = last_ts;
}
这里将会根据TS增量计算到实际帧率,并且发出PJMEDIA_EVENT_FMT_CHANGED广播,再经过第三步,第四步完成渲染速率的调整(注意仍然会放大1.2倍)。
这个代码有一个小坑,更改帧率并发出广播的准入条件
if (vfd->fps.num/vfd->fps.denum <= 100 &&
vfd->fps.num * stream->dec_max_fps.denum >
stream->dec_max_fps.num * vfd->fps.denum)
必须超过最大解码帧率,并且小100,如果项目中将解码帧率固定设置为30(pj官方默认是15),则基本上不会触发这个广播,导致低帧率的视频卡顿尤其明显(笔者就掉过这个坑)。
解决办法是将后半段的判断去掉,加上差值比较,增大或缩小超过2帧则发起广播通知。
/* Publish PJMEDIA_EVENT_FMT_CHANGED event if frame rate
* increased and not exceeding 100fps.
*/
float y1 = (float)vfd->fps.num / (float)vfd->fps.denum;
float y2 = (float)stream->dec_max_fps.num / (float)stream->dec_max_fps.denum;
if (vfd->fps.num / vfd->fps.denum <= 100
&& PJ_ABS(y1-y2)>2 //add by
//&&
//vfd->fps.num * stream->dec_max_fps.denum >
//stream->dec_max_fps.num * vfd->fps.denum
)
至此,渲染线程的速率问题得到解决,解码端性能未到瓶颈的情况下,应该都能很好的应对视频卡顿问题。