ijkplayer 单视频流直播延迟问题解决过程
一开始我尝试是通过设置ijkplayer的参数去修改延迟,参数的修改能把ijkplayer的开播延迟拉到200ms左右,但是随着播放时间增加延迟也在增加,然后带着问题去网上寻找答案,找到暴走大牙和Gongjia两位大神的解决方案,但是这种方案仅适用于带有音频流的,现在适配的流仅有视频流,用了这两位的方案后,丢帧是可以,但是延迟的问题并没有解决,因为没有音频流的视频的时间基准是用视频流的时间,丢完帧后视频会卡顿等待视频的时间基准。后来我在ijkplayer的issue上找了很多关于卡顿的问题,都没有找到解决方案,后面尝试通过变速的方式去解决这个问题,找到了Mr_xkHuang的一篇ijkplayer-音视频变速播放实现,才了解到了单视频流的视频和有音视频流的视频关于时间基准的差别。修改了时间基准之后,长时直播的延迟问题解决了。
ff_ffplay.c代码修改如下:
static int read_thread(void *arg)
{
.....
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
stream_component_open(ffp,st_index[AVMEDIA_TYPE_AUDIO]);
} else {
//添加新代码
ffp->av_sync_type = AV_SYNC_EXTERNAL_CLOCK;
//注释掉原有代码
// ffp->av_sync_type = AV_SYNC_VIDEO_MASTER;
is->av_sync_type = ffp->av_sync_type;
}
.....
}
经过上面的修改,长时放置直播软解码延迟稳定在400ms左右硬解码延迟稳定在200ms左右,但是出现了一个更加严重的问题,那就是闪退
代码会在renderer_yuv420sp_vtb.m这个类
static GLboolean yuv420sp_vtb_uploadTexture(IJK_GLES2_Renderer *renderer, SDL_VoutOverlay *overlay)
{
.....
}
这个方法内的随机几行代码奔溃,报的错误是EXC_BAD_ACCESS,通过打开僵尸对象检测,发现是
CVPixelBufferRef pixel_buffer
这个对象的原因,然后通过和一个做c语言的大神沟通后他告诉我要从对象初始化和查对象被释放这两个地方入手。
经过一番查找,找到pixel_buffer是通过SDL_VoutOverlay *overlay这个类拿到的,然后我就在ijksdl_vout_overlay_videotoolbox.m这个类的overlay的初始化方法里面加了
SDL_VoutOverlay *SDL_VoutVideoToolBox_CreateOverlay(int width, int height, SDL_Vout *display)
{
...
//add
if (opaque->pixel_buffer != NULL) {
CVBufferRelease(opaque->pixel_buffer);
}
opaque->pixel_buffer = NULL;
return overlay;
}
当时我是用的模拟器测试的,结果很美好,没有闪退了,年轻的我以为bug已经解决了,但是我想用真机测试下延迟问题时,突然发现闪退依然存在。然后我就继续去找还有没有哪里有使用pixel_buffer这个结构体的。但是仅有创建pixel_buffer的ijksdl_vout_overlay_videotoolbox.m和使用pixel_buffer的renderer_yuv420sp_vtb.m这两个类使用了pixel_buffer,一时间大神给的方向找不到路了。
后面我考虑到既然是EXC_BAD_ACCESS,那么久应该是野指针的问题,我就冲这个方向入手,在使用pixel_buffer的地方对这个结构体进行retain操作,增加它的引用计数使它不被释放,结果是有一点用处,奔溃的概率降低了,以前1-5分钟就会奔溃的代码现在可以到20-30分钟奔溃,这让我以为找到了一条正确的路,结果确实我把引用计数增加做到了极致,仍然会有闪退。
这时候我知道我是找错了方向,之后想到pixel_buffer是通过解码后的frame数据转换而来的,有没有可能是frame被释放了导致pixel_buffer这个结构体内部的指针形成野指针。带着这样的疑问,我开始打印所有释放frame的地方,结果真的找到了,每次奔溃的时候都连着调用了两次释放frame。后面我顺着这个释放的流程,将渲染与释放的各个地方按上打印,终于理清楚了ijkplayer渲染的流程。
通过对这里流程的理清楚,我发现终于找到了为什么会发生这样的问题,既然知道了是由于在渲染之前frame就被释放了,这样就好解决问题了。
整体是frame在还么有进行渲染就进行了引用释放,释放后frame在进行渲染时就会导致上面的情况
我的最终如下:
修改对ijksdl_vout.h 文件overlay的定义位置添加了一个属性:
struct SDL_VoutOverlay {
//add
bool nowUseing;//当前是否还在使用中
}
然后在ijksdl_vout_overlay_videotoolbox.m这个类中
//这是overlay的frame数据重载方法
static int func_fill_frame(SDL_VoutOverlay *overlay, const AVFrame *frame)
{
//add
// printf("new func_fill_frame :%p\n",overlay);
if (overlay->NowUseing) {//如果当前frame正在使用中,就不进行数据加载
// printf("nowUsing want new func_fill_frame:%p\n",overlay);
return 1;
}
overlay->nowUseing = true;//数据重载后将overlay的使用状态置为true
....
}
//overlay的初始化方法
SDL_VoutOverlay *SDL_VoutVideoToolBox_CreateOverlay(int width, int height, SDL_Vout *display)
{
.....
//add
overlay->nowUseing = false;//初始化nowUsing
//初始化pixel_buffer
if (opaque->pixel_buffer != NULL) {
CVBufferRelease(opaque->pixel_buffer);
}
opaque->pixel_buffer = NULL;
return overlay;
}
在renderer_yuv420sp_vtb.m这个类中
//渲染的方法
static GLboolean yuv420sp_vtb_uploadTexture(IJK_GLES2_Renderer *renderer, SDL_VoutOverlay *overlay)
{
//add
if (!overlay->nowUseing) {
return GL_FALSE;
}
.....
//add
// printf("display over:%p\n",overlay);
overlay->nowUseing = false;
return GL_TRUE;
}
在ff_ffplay.c这个类中
//这是item的引用释放方法,修改为如果该frame还没有渲染,这不释放引用。
static void frame_queue_unref_item(Frame *vp)
{
//添加这行,防止刚启动APP或者切换前后台bmp为null,bmp->nowUseing闪退
if (!vp->bmp) {
av_frame_unref(vp->frame);
vp->frame = NULL;
SDL_VoutUnrefYUVOverlay(vp->bmp);
avsubtitle_free(&vp->sub);
return;
}
if (vp->bmp->nowUseing) {
printf("nowUsing wait av_frame_unref:%p\n",vp->bmp);
}else {
// printf("SDL_VoutUnrefYUVOverlay(vp->bmp):%p\n",vp->bmp);
av_frame_unref(vp->frame);
vp->frame = NULL;
SDL_VoutUnrefYUVOverlay(vp->bmp);
avsubtitle_free(&vp->sub);
}
}
这个解决办法并不完美,只是粗暴的处理不让程序崩溃,我觉得最好的解决办法应该是找到为什么overlay会在使用前释放。然后让这种情况不再出现。最近没有时间去处理这个问题,后面有时间了再来解决。