美文网首页
iOS中基于ffmpeg开发的播放器打开多个samba链接的解决

iOS中基于ffmpeg开发的播放器打开多个samba链接的解决

作者: yellowei | 来源:发表于2020-08-31 16:18 被阅读0次

    之前写过一篇关于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_frameav_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()

    相关文章

      网友评论

          本文标题:iOS中基于ffmpeg开发的播放器打开多个samba链接的解决

          本文链接:https://www.haomeiwen.com/subject/kxdusktx.html