记一次linphone sip视频功能调试
前文再续,书接上一回。上回说到,我司的sip客户端基于linphone开发,使用中发现一些影响很大的问题,部分解决了,部分还有待解决。目前的情况是,大规模发生的问题暂时没有了,但是1000坐席,每天还是会报好几个问题。
遗留问题暂时没有进展,有一个音频资源释放时阻塞的问题,无法重现,不确定是否解决。
虽然历史问题还没有解决完,但是又有新的任务了。前几天领导提出需要支持sip视频,之前升级linphone 5.2.114新版sdk就存在不支持winform下显示视频的问题。现在必须要解决这个问题了。
新版的sdk windows版本在C#中默认视频是用opengl的视频过滤器显示,需要在UWP项目中才能使用。但是我们用的是CPF界面框架,只能用winform的PictureBox来显示视频,在CPF中用NativeElement控件引入winform的PictureBox。为啥要用CPF框架?因为我们要同时支持windows和国产统信系统。x86的统信系统用X11窗口显示视频是正常的,就是windows系统新版sdk不行。
旧版sdk(4.5.26)用winform的PictureBox是可以正常显示视频的。最早想到的就是,看下旧版sdk用的什么视频过滤器,如果新版也用同样的过滤器,是不是就可以了(过滤器是linphone的mediastreamer2模块的一个概念,类似于linux的管道一样,对音视频的处理由一组过滤器流式拼接而成)。查看旧版Core.VideoDisplayFilter的值,发现是MSDrawDibDisplay,于是在新版中也设置为MSDrawDibDisplay。不幸的是,一开启视频或视频预览就程序崩溃了。
怎么办呢?日志中没有报错。经过前段时间的问题排查过程,已经有一些经验了。遇到问题不要惊慌,可以看下转储出来的dump文件。报错信息是:
点击查看代码
Unhandled exception at 0x79E9D613 (mediastreamer.dll) in CPFCcodSipPhone.exe.46856.dmp: 0xC0000005: Access violation writing location 0xFFFFFD00.
堆栈信息是:
点击查看代码
> mediastreamer.dll!ScaleFilterCols_SSSE3(unsigned char * dst_ptr, const unsigned char * src_ptr, int dst_width, int x, int dx) Line 934 C++
mediastreamer.dll!ScalePlaneBilinearDown(int src_width, int src_height, int dst_width, int dst_height, int src_stride, int dst_stride, const unsigned char * src_ptr, unsigned char * dst_ptr, libyuv::FilterMode filtering) Line 1095 C++
mediastreamer.dll!ScalePlane(const unsigned char * src, int src_stride, int src_width, int src_height, unsigned char * dst, int dst_stride, int dst_width, int dst_height, libyuv::FilterMode filtering) Line 1635 C++
mediastreamer.dll!I420Scale(const unsigned char * src_y, int src_stride_y, const unsigned char * src_u, int src_stride_u, const unsigned char * src_v, int src_stride_v, int src_width, int src_height, unsigned char * dst_y, int dst_stride_y, unsigned char * dst_u, int dst_stride_u, unsigned char * dst_v, int dst_stride_v, int dst_width, int dst_height, libyuv::FilterMode filtering) Line 1759 C++
mediastreamer.dll!yuv_scale(_MSScalerContext * ctx, unsigned char * * src, int * src_strides, unsigned char * * dst, int * dst_strides) Line 506 C
mediastreamer.dll!yuv2rgb_process(Yuv2RgbCtx * ctx, _MSPicture * src, MSVideoSize dstsize, unsigned char mirroring) Line 104 C
mediastreamer.dll!dd_display_process(_MSFilter * f) Line 450 C
mediastreamer.dll!ms_filter_process(_MSFilter * f) Line 233 C
mediastreamer.dll!call_process(_MSFilter * f) Line 252 C
定位到报错的代码是libyuv库中的一段汇编代码,位于external\libyuv\source\scale_win.cc, ScaleFilterCols_SSSE3()函数,934行(mov [edi], bx这行)。
点击查看代码
// 2 Pixel loop.
xloop2:
movdqa xmm1, xmm2 // x0, x1 fractions.
paddd xmm2, xmm3 // x += dx
movzx ebx, word ptr [esi + eax] // 2 source x0 pixels
movd xmm0, ebx
psrlw xmm1, 9 // 7 bit fractions.
movzx ebx, word ptr [esi + edx] // 2 source x1 pixels
movd xmm4, ebx
pshufb xmm1, xmm5 // 0011
punpcklwd xmm0, xmm4
psubb xmm0, xmmword ptr kFsub80 // make pixels signed.
pxor xmm1, xmm6 // 0..7f and 7f..0
paddusb xmm1, xmm7 // +1 so 0..7f and 80..1
pmaddubsw xmm1, xmm0 // 16 bit, 2 pixels.
pextrw eax, xmm2, 1 // get x0 integer. next iteration.
pextrw edx, xmm2, 3 // get x1 integer. next iteration.
paddw xmm1, xmmword ptr kFadd40 // make pixels unsigned and round.
psrlw xmm1, 7 // 8.7 fixed point to low 8 bits.
packuswb xmm1, xmm1 // 8 bits, 2 pixels.
movd ebx, xmm1
mov [edi], bx (这一行崩溃了)
lea edi, [edi + 2]
sub ecx, 2 // 2 pixels
jge xloop2
xloop29:
add ecx, 2 - 1
jl xloop99
汇编代码我工作中没有用过,只能勉强看懂一点。实在看不懂的指令,百度下了解个大概。这段汇编代码,以及报错时的寄存器值让我困惑了一整天。因为edi是从函数参数dst_ptr中赋过来的,函数调用方的变量值是正确的,但是赋过来就错了,变成0xfffffd00了。0xfffffd00是代码段的地址,实际上是ScaleFilterCols_SSSE3这个函数的地址,当然不能给它指向的内存位置赋值。一开始没注意到ecx在上面也减过2,以为是第二次xloop2循环报的错,但是edi没理由第一次循环是对的,第二次就错了呀。这中间除了加2又没有改变它的值。
后来发现VS2022给出的函数调用方中的变量值是错的!其实传过来的参数就是错的。怎么发现的呢?因为我发现ecx第一次循环前就减了2,实际上是第一次循环就报错了。于是我就想着在函数调用方中把参数值打印出来。这个libyuv库中没有日志功能,于是我手写了三行C语言写文件的代码,输出到一个文本文件中。
打印出来一看,果然传过来的参数就是错的。最后追溯到mediastreamer2的drawdib-display.c里面yuv2rgb_process()函数中第104行对ms_scaler_process()函数的调用,传过来的(&p)[1]、(&p)[2]是错的(ms_scaler_process()函数会调到libyuv库的I420Scale方法,传的两个参数就是(&p)[1]、(&p)[2]),代码是这样的:
点击查看代码
{
int rgb_stride=-dstsize.width*3;
uint8_t *p;
p=ctx->rgb+(dstsize.width*3*(dstsize.height-1));
if (ms_scaler_process(ctx->sws,src->planes,src->strides, &p, &rgb_stride)<0){
ms_error("Error in 420->rgb ms_scaler_process().");
}
if (mirroring) rgb24_mirror(ctx->rgb,dstsize.width,dstsize.height,dstsize.width*3);
}
uint8_t 就是unsigned char的typedef。
C语言我工作中也没有用过,我主要是写java的。但是遇到这个问题,只能勉强回忆下C语言的指针概念。p是一个char *指针,可以看做指向一段内存。&p是p的取址,就是一个二级指针。那么(&p)[1]、(&p)[2]是什么呢?是这个二级指针的近邻内存位置的值。但是p是一个局部变量!所以(&p)[1]这个值是个未定义的非法的地址。后面的(&rgb_stride)[1]也是类似的非法值。
这时我就疑惑了。这样的代码怎么可能运行得起来呢?于是看了下旧版的linphone sdk代码,发现旧版没有用libyuv库来缩放视频,而是用的ffmpeg。但是我还是先尝试改了下新版代码,改成这样:
点击查看代码
{
int rgb_stride=-dstsize.width*3;
uint8_t *p;
p=ctx->rgb+(dstsize.width*3*(dstsize.height-1));
uint8_t **plist = (uint8_t**)ms_malloc0(sizeof(uint8_t*)*3);
plist[0] = p;
plist[1] = p + 1; //dstsize.width * dstsize.height;
plist[2] = p + 2; //dstsize.width * dstsize.height + (dstsize.width >> 1) * (dstsize.height >> 1);
int* rgb_stride_list = (int*)ms_malloc0(sizeof(int)*3);
rgb_stride_list[0] = rgb_stride;
rgb_stride_list[1] = rgb_stride;
rgb_stride_list[2] = rgb_stride;
//ms_message("(&p)[0]=%p,(&p)[1]=%p,(&p)[2]=%p", (&p)[0], (&p)[1], (&p)[2]);
//ms_message("plist[0]=%p,plist[1]=%p,plist[2]=%p", plist[0], plist[1], plist[2]);
ms_message("src->planes[0]=%p,src->planes[1]=%p,src->planes[2]=%p", src->planes[0], src->planes[1], src->planes[2]);
ms_message("src->strides[0]=%d,src->strides[1]=%d,src->strides[2]=%d", src->strides[0], src->strides[1], src->strides[2]);
if (ms_scaler_process(ctx->sws,src->planes,src->strides, plist, rgb_stride_list)<0){
ms_error("Error in 420->rgb ms_scaler_process().");
}
if (mirroring) rgb24_mirror(ctx->rgb,dstsize.width,dstsize.height,dstsize.width*3);
}
测试发现,摄像头视频倒是动态显示出来了,但是是黑白的,而且是压扁了的左右2个重复图像,中间隔了一段黑带。百度了解了下yuv格式和rgb格式的存储,怀疑这段处理只是缩放了yuv格式,没有转换成rgb格式。怀疑这段linphone代码功能都不全。
然后我就尝试换ffmpeg库。编译时可以指定关闭libyuv库,使用ffmpeg库。只需要这样执行cmake配置命令:
点击查看代码
cmake .. -G "Visual Studio 15 2017" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_LIBYUV=OFF -DENABLE_FFMPEG=ON
编译ffmpeg库时报了一个error。是一个int16_t *转换uint8_t *的incompatible pointer type错误。我改了下代码,再次编译,又报了一个error,是另外一个地方的同样的错误。我又改。折腾了几次之后,突然报了十几个错误。是一些函数指针的转换错误。我怀疑是编译命令有问题,好像incompatible pointer type应该是个warning而不是error才对。但是没找到在哪改。mediastreamer的ENABLE_STRICT关了没用。ffmpeg的makefile也没有单独的-Werror配置。
只好继续改代码,要读懂linphone的代码还真不容易,ff_yuyvToY_mmx这些函数找了一小时找不到定义在哪,结果是用#define拼接的函数名。看到这段代码,我是服气的。
点击查看代码
#define INPUT_UV_FUNC(fmt, opt) \
extern void ff_ ## fmt ## ToUV_ ## opt(uint8_t *dstU, uint8_t *dstV, \
const uint8_t *src, const uint8_t *unused1, const uint8_t *src3, \
int w, uint32_t *unused2)
#define INPUT_FUNC(fmt, opt) \
extern void ff_ ## fmt ## ToY_ ## opt(uint8_t *dst, const uint8_t *src, const uint8_t *src2, const uint8_t *src3, \
int w, uint32_t *unused); \
INPUT_UV_FUNC(fmt, opt)
#define INPUT_FUNCS(opt) \
INPUT_FUNC(uyvy, opt); \
INPUT_FUNC(yuyv, opt); \
INPUT_UV_FUNC(nv12, opt); \
INPUT_UV_FUNC(nv21, opt)
#if ARCH_X86_32
INPUT_FUNCS(mmx);
#endif
INPUT_FUNCS(sse2);
INPUT_FUNCS(avx);
折腾了半天,终于把错误改完了。编译出来之后一运行,正确的视频显示出来了!掐指算来花了3天时间。

浙公网安备 33010602011771号