之前写过一篇关于ffmpeg中samba协议的调用分析的文章,如果需要参考samba和ffmpeg的相关源码,那这里挺方便查询:
这次想记录的是:关于如何在不修改原有ffmpeg代码或者编译好的动态库、静态库的情况下,使用URLProtocol来“override”相应函数的解决方案。
问题背景
这次遇到了一个难题,在开发播放器关于“生成预览图功能”的时候,发现调用avformat_open_input打开samba流是没问题的,但是同时打开(多线程)多个samba流会导致之前线程打开的samba流在av_read_frame时crash的问题。
我会创建两个CYVideoDecoder解码器,一个用于视频的播放,另一用于预览图生成。生成预览图和视频解码,这两个操作分别位于不同队列,异步并发执行。
当CYFFmpegPlayer初始化完成直至开始播放视频的时候,用于播放的解码器一,一切都是正常执行。
这时,通过dispatch_after延迟开启预览图生成--这里延迟开始是为了不影响视频的播放的加载速度,这里会创建第二个解码器,我们称之为解码器二。解码器二的io流程和解码器一类似,都是先从avformat_open_input开始的,之后就是流程的控制,包括调用av_read_frame和av_seek_frame等。
当解码器二调用avformat_open_input时,导致解码器一的io操作直接失败了(av_read_frame和av_seek_frame等)。
WHY?
通过调试,发现解码器一中ffmpeg抛出的的日志信息为:
[smb @ 0x107d041d0] File open failed: Bad file descriptor
可见是由于smb中的某个错误导致的。
在samba中,一般“Bad file descriptor”错误多见于“smbc_context”上下文失效,为了验证这个猜想,我们先按照ffmpeg中libsmbc_connect函数对samba的调用方式那样去使用多线程GCD来测试一下:
+ (void)testSMB2 {
SMBCCTX * ctx = smbc_new_context();
if (!ctx) {
NSLog(@"smbc_new_context failed");
}
if (!smbc_init_context(ctx))
{
NSLog(@"smbc_init_context failed");
}
smbc_set_context(ctx);
if (smbc_init(NULL, 0) < 0) {
NSLog(@"smbc_init failed");
}
int fd = smbc_open("smb://workgroup;mobile:123123@172.16.9.10/video/test.mp4", O_RDONLY, 0666);
if ( fd < 0) {
NSLog(@"File open failed");
}
else
{
NSLog(@"File open successed");
}
smbc_close(fd);
smbc_free_context(ctx, 1);
}
+ (void)test {
dispatch_queue_t disqueue = dispatch_queue_create("com.cyplayer.testsmb", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t disgroup = dispatch_group_create();
dispatch_group_async(disgroup, disqueue, ^{
[self testSMB2];
NSLog(@"任务一完成");
});
dispatch_group_async(disgroup, disqueue, ^{
[self testSMB2];
NSLog(@"任务二完成");
});
dispatch_group_notify(disgroup, disqueue, ^{
NSLog(@"dispatch_group_notify 执行");
});
}
上面代码就是多线程创建两个smbc_context,打开两个smb链接
当运行起来的时候,随机crash了,打全局断点,通过调用栈可以看到,崩溃有时在smbc内部的talloc中,有时会是"Bad Access"
talloc顺带提一下,这个是一个C编写的内存池框架,是一个基于栈的自动内存管理。
这里导致crash是由于smbc_context是一个全局变量,代码中每次testSMB2都会通过smbc_set_context函数对全局的context重新赋值,这就会导致正在使用的上下文被修改从而使得原有链接的io失效, 二crash是由于smbc_free_context已经释放了原有context,再次调用就crash了。
也许你会问,代码逻辑来看,每次smbc_free_context操作的都是不一样context啊,为何会crash呢?
这个就得从libsmbclient源码中的“ smbc_set_context”讲起:
smbc_set_context(SMBCCTX * context)
{
SMBCCTX *old_context = statcont;
if (context) {
/* Save provided context. It must have been initialized! */
statcont = context;
/* You'd better know what you're doing. We won't help you. */
smbc_compat_initialized = 1;
}
return old_context;
}
statcont为:
static SMBCCTX * statcont = NULL;
这下就看明白了吧。
那么,调整调用方式,我重构一下测试代码:
+ (void)testGCDForSMBC
{
SMBCCTX * ctx = smbc_new_context();
if (!ctx) {
NSLog(@"smbc_new_context failed");
return;
}
if (!smbc_init_context(ctx))
{
NSLog(@"smbc_init_context failed");
return;
}
smbc_set_context(ctx);
smbc_setOptionUserData(ctx, @"work");
smbc_setTimeout(ctx,3000);
//smbc_setFunctionAuthDataWithContext(ctx, my_smbc_get_auth_data_with_context_fn);
if (smbc_init(NULL, 0) < 0) {
NSLog(@"smbc_init failed");
return;
}
__block int open1, open2, open3;
dispatch_queue_t disqueue = dispatch_queue_create("com.cyplayer.testsmb", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t disgroup = dispatch_group_create();
dispatch_group_async(disgroup, disqueue, ^{
open1 = smbc_open("smb://workgroup;mobile:123123@172.16.9.10/video/test.mp4", O_RDONLY, 0666);
if ( open1 < 0) {
NSLog(@"File open failed");
}
else
{
NSLog(@"File open successed");
}
NSLog(@"任务一完成");
});
dispatch_group_async(disgroup, disqueue, ^{
open2 = smbc_open("smb://workgroup;mobile:123123@172.16.9.10/video/test.mp4", O_RDONLY, 0666);
if ( open2 < 0) {
NSLog(@"File open failed");
}
else
{
NSLog(@"File open successed");
}
NSLog(@"任务二完成");
});
dispatch_group_notify(disgroup, disqueue, ^{
smbc_close(open1);
smbc_close(open2);
smbc_free_context(ctx, 1);
NSLog(@"dispatch_group_notify 执行");
});
}
上述这段代码相当于对context进行了“复用”。
执行成功,没有crash了。
结论: 在使用libsmbclient时,复用同一个context对文件进行io操作不会导致crash,多线程下互斥调用context相关函数。
解决方案
目标:为CYPlayerDecoder提供smbc线程安全
通过修改ffmpeg源码来重新编译一份ffmpeg动态库当然是可以解决这个问题的,但是由于制作动态库工序繁杂,编译也费时费事,并且ffmpeg这么做也没错(嘻嘻),那么我们能不能hook它的相关方法呢?
了解过FB的朋友肯定知道fishhook,但我没有采用这种方式,因为我觉得我尽量考虑播放器做到系统的最小完整性。
我将目光放在了URLProtocol上。
回顾URLProtocol
URLProtocol是一个结构体,申明了ffmpeg所支持的网络协议中需要用到的打开流、关闭流、read、write等方法。
typedef struct URLProtocol {
const char *name;
int (*url_open)( URLContext *h, const char *url, int flags);
/**
* This callback is to be used by protocols which open further nested
* protocols. options are then to be passed to ffurl_open()/ffurl_connect()
* for those nested protocols.
*/
int (*url_open2)(URLContext *h, const char *url, int flags, AVDictionary **options);
int (*url_accept)(URLContext *s, URLContext **c);
int (*url_handshake)(URLContext *c);
/**
* Read data from the protocol.
* If data is immediately available (even less than size), EOF is
* reached or an error occurs (including EINTR), return immediately.
* Otherwise:
* In non-blocking mode, return AVERROR(EAGAIN) immediately.
* In blocking mode, wait for data/EOF/error with a short timeout (0.1s),
* and return AVERROR(EAGAIN) on timeout.
* Checking interrupt_callback, looping on EINTR and EAGAIN and until
* enough data has been read is left to the calling function; see
* retry_transfer_wrapper in avio.c.
*/
int (*url_read)( URLContext *h, unsigned char *buf, int size);
int (*url_write)(URLContext *h, const unsigned char *buf, int size);
int64_t (*url_seek)( URLContext *h, int64_t pos, int whence);
int (*url_close)(URLContext *h);
int (*url_read_pause)(URLContext *h, int pause);
int64_t (*url_read_seek)(URLContext *h, int stream_index,
int64_t timestamp, int flags);
int (*url_get_file_handle)(URLContext *h);
int (*url_get_multi_file_handle)(URLContext *h, int **handles,
int *numhandles);
int (*url_get_short_seek)(URLContext *h);
int (*url_shutdown)(URLContext *h, int flags);
int priv_data_size;
const AVClass *priv_data_class;
int flags;
int (*url_check)(URLContext *h, int mask);
int (*url_open_dir)(URLContext *h);
int (*url_read_dir)(URLContext *h, AVIODirEntry **next);
int (*url_close_dir)(URLContext *h);
int (*url_delete)(URLContext *h);
int (*url_move)(URLContext *h_src, URLContext *h_dst);
const char *default_whitelist;
} URLProtocol;
之前也分析过,对libsmbc_connect的调用实际在libsmbc_open中,可以看出,假如我们能替换libsmbc_connect或者libsmbc_open岂不是美哉?
hook掉libsmbc_connect而不用fishhook怕是难了,但libsmbc_open还有机会
早之前也分析到了ffmpeg源码中libsmbclient.c有一个结构体:ff_libsmbclient_protocol
const URLProtocol ff_libsmbclient_protocol = {
.name = "smb",
.url_open = libsmbc_open,
.url_read = libsmbc_read,
.url_write = libsmbc_write,
.url_seek = libsmbc_seek,
.url_close = libsmbc_close,
.url_delete = libsmbc_delete,
.url_move = libsmbc_move,
.url_open_dir = libsmbc_open_dir,
.url_read_dir = libsmbc_read_dir,
.url_close_dir = libsmbc_close_dir,
.priv_data_size = sizeof(LIBSMBContext),
.priv_data_class = &libsmbclient_context_class,
.flags = URL_PROTOCOL_FLAG_NETWORK,
};
此结构体ff_libsmbclient_protocol在libavformat/protocols.c中有这样的引入:
extern const URLProtocol ff_libsmbclient_protocol;
那么是否在avformat_open_input之前修改ff_libsmbclient_protocol中的url_open函数指针指向我定义的my_ libsmbc_open就可以实现替换了呢?
带着猜想,在CYVideoDecoder的初始化方法中修改此结构体,再次测试:
extern URLProtocol ff_libsmbclient_protocol;
+ (void)initialize
{
// av_log_set_callback(FFLog);
//替换ffmpeg的samba protocol的方法
ff_libsmbclient_protocol.url_open = my_libsmbc_open;
ff_libsmbclient_protocol.url_close = my_libsmbc_close;
avcodec_register_all();
av_register_all();
avformat_network_init();
avfilter_register_all();
}
static void my_smbc_get_auth_data_fn (const char *srv,
const char *shr,
char *wg, int wglen,
char *un, int unlen,
char *pw, int pwlen)
{
}
static int my_libsmbc_connect(URLContext *h)
{
LIBSMBContext *libsmbc = h->priv_data;
// //这里替换掉原有的ffmpeg写法,是因为每次open_input造成会调用这个connect,然后smbc_new_context造成原有context失效,崩溃
// libsmbc->ctx = smbc_new_context();
// if (!libsmbc->ctx) {
// int ret = AVERROR(errno);
// av_log(h, AV_LOG_ERROR, "Cannot create context: %s.\n", strerror(errno));
// return ret;
// }
// if (!smbc_init_context(libsmbc->ctx)) {
// int ret = AVERROR(errno);
// av_log(h, AV_LOG_ERROR, "Cannot initialize context: %s.\n", strerror(errno));
// return ret;
// }
libsmbc->ctx = smbc_set_context(NULL);
if (libsmbc->ctx == NULL) {
if (smbc_init(my_smbc_get_auth_data_fn, 0) < 0) {
int ret = AVERROR(errno);
av_log(h, AV_LOG_ERROR, "Cannot initialize context: %s.\n", strerror(errno));
return ret;
}
libsmbc->ctx = smbc_set_context(NULL);
}
smbc_setOptionUserData(libsmbc->ctx, h);
// smbc_setFunctionAuthDataWithContext(libsmbc->ctx, libsmbc_get_auth_data);
if (libsmbc->timeout != -1)
smbc_setTimeout(libsmbc->ctx, libsmbc->timeout);
if (libsmbc->workgroup)
smbc_setWorkgroup(libsmbc->ctx, libsmbc->workgroup);
if (smbc_init(my_smbc_get_auth_data_fn, 0) < 0) {
int ret = AVERROR(errno);
av_log(h, AV_LOG_ERROR, "Initialization failed: %s\n", strerror(errno));
return ret;
}
return 0;
}
static int my_libsmbc_close(URLContext *h)
{
LIBSMBContext *libsmbc = h->priv_data;
if (libsmbc->fd >= 0) {
smbc_close(libsmbc->fd);
libsmbc->fd = -1;
}
if (libsmbc->ctx) {
// smbc_free_context(libsmbc->ctx, 1);
// libsmbc->ctx = NULL;
}
return 0;
}
static int my_libsmbc_open( URLContext *h, const char *url, int flags)
{
LIBSMBContext *libsmbc = h->priv_data;
int access, ret;
struct stat st;
libsmbc->fd = -1;
libsmbc->filesize = -1;
if ((ret = my_libsmbc_connect(h)) < 0)
goto fail;
if ((flags & AVIO_FLAG_WRITE) && (flags & AVIO_FLAG_READ)) {
access = O_CREAT | O_RDWR;
if (libsmbc->trunc)
access |= O_TRUNC;
} else if (flags & AVIO_FLAG_WRITE) {
access = O_CREAT | O_WRONLY;
if (libsmbc->trunc)
access |= O_TRUNC;
} else
access = O_RDONLY;
/* 0666 = -rw-rw-rw- = read+write for everyone, minus umask */
if ((libsmbc->fd = smbc_open(url, access, 0666)) < 0) {
ret = AVERROR(errno);
av_log(h, AV_LOG_ERROR, "File open failed: %s\n", strerror(errno));
goto fail;
}
if (smbc_fstat(libsmbc->fd, &st) < 0)
av_log(h, AV_LOG_WARNING, "Cannot stat file: %s\n", strerror(errno));
else
libsmbc->filesize = st.st_size;
return 0;
fail:
my_libsmbc_close(h);
return ret;
}
调试通过!
总结
- FFmpeg是基于C编写的,所以巧用extern是这里实现的关键
- 对于libsmbclient这种基于C编写的库,Context是灵魂,Think this: CGContextRef、UIGraphicsGetCurrentContext()
- 运用信号量实现libsmbclient的线程安全:dispatch_semaphore_wait()、dispatch_semaphore_signal()