欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

一次“Error Domain=AVFoundationErrorDomain Code=-11841”的调试

程序员文章站 2022-04-12 13:00:16
一次“Error Domain=AVFoundationErrorDomain Code= 11841”的调试 起因 最近在重构视频输出模块的时候,调试碰到AVAssetReader 调用开始方法总是返回NO而失败,代码如下: reader的创建代码如下,主要用的是GPUImageMovieComp ......

一次“error domain=avfoundationerrordomain code=-11841”的调试

起因

最近在重构视频输出模块的时候,调试碰到avassetreader 调用开始方法总是返回no而失败,代码如下:

if ([reader startreading] == no) 
    {
        nslog(@"error reading from file at url: %@", self.url);
        return;
    }

reader的创建代码如下,主要用的是gpuimagemoviecompostion.

- (avassetreader*)createassetreader
{
    nserror *error = nil;
    avassetreader *assetreader = [avassetreader assetreaderwithasset:self.compositon error:&error];
    
    nsdictionary *outputsettings = @{(id)kcvpixelbufferpixelformattypekey: @(kcvpixelformattype_420ypcbcr8biplanarfullrange)};
    avassetreadervideocompositionoutput *readervideooutput = [avassetreadervideocompositionoutput assetreadervideocompositionoutputwithvideotracks:[_compositon trackswithmediatype:avmediatypevideo]
                                                                                                                                     videosettings:outputsettings];
    readervideooutput.videocomposition = self.videocomposition;
    readervideooutput.alwayscopiessampledata = no;
    if ([assetreader canaddoutput:readervideooutput]) {
        [assetreader addoutput:readervideooutput];
    }
    
    nsarray *audiotracks = [_compositon trackswithmediatype:avmediatypeaudio];
    bool shouldrecordaudiotrack = (([audiotracks count] > 0) && (self.audioencodingtarget != nil) );
    avassetreaderaudiomixoutput *readeraudiooutput = nil;
    
    if (shouldrecordaudiotrack)
    {
        [self.audioencodingtarget setshouldinvalidateaudiosamplewhendone:yes];
        nsdictionary *audioreadersetting = @{avformatidkey: @(kaudioformatlinearpcm),
                                             avlinearpcmisbigendiankey: @(no),
                                             avlinearpcmisfloatkey: @(no),
                                             avlinearpcmbitdepthkey: @(16)};
        readeraudiooutput = [avassetreaderaudiomixoutput assetreaderaudiomixoutputwithaudiotracks:audiotracks audiosettings:audioreadersetting];
        readeraudiooutput.audiomix = self.audiomix;
        readeraudiooutput.alwayscopiessampledata = no;
        [assetreader addoutput:readeraudiooutput];
    }
    
    return assetreader;
}

然后在该处断点,查看reader的status和error,显示error domain=avfoundationerrordomain code=-11841。查了一下文档发现如下:

averrorinvalidvideocomposition = -11841

you attempted to perform a video composition operation that is not supported.

应该就是readervideooutput.videocomposition = self.videocomposition; 这个videocompostion有问题了。avmutablevideocomposition这个类在视频编辑里面非常重要,包含对avcomposition中的各个视频track如何融合的信息,我做的是两个视频的融合小demo,就需要用到它,设置这个类稍微有点复杂,比较容易出错,特别是设置avmutablevideocompositioninstruction这个类的timerange属性,我的这次错误就是因为这个。

开始调试

马上调用avmutablevideocomposition的如下方法:

bool isvalid = [self.videocomposition isvalidforasset:self.compositon timerange:cmtimerangemake(kcmtimezero, self.compositon.duration) validationdelegate:self];

这个时候isvalid为no,确定就是这个videocompostion问题了,添加了avvideocompositionvalidationhandling协议四个方法打印一下,这个协议太贴心了,估计知道这个地方出错概率比较高,所以特地弄的吧。代码如下:

- (bool)videocomposition:(avvideocomposition *)videocomposition shouldcontinuevalidatingafterfindinginvalidvalueforkey:(nsstring *)key 
{
    nslog(@"%s===%@",__func__,key);
    return yes;
}

- (bool)videocomposition:(avvideocomposition *)videocomposition shouldcontinuevalidatingafterfindingemptytimerange:(cmtimerange)timerange 
{
    nslog(@"%s===%@",__func__,cfbridgingrelease(cmtimerangecopydescription(kcfallocatordefault, timerange)));
    return yes;
}

- (bool)videocomposition:(avvideocomposition *)videocomposition shouldcontinuevalidatingafterfindinginvalidtimerangeininstruction:(id<avvideocompositioninstruction>)videocompositioninstruction 
{
    nslog(@"%s===%@",__func__,videocompositioninstruction);
    return yes;
}

- (bool)videocomposition:(avvideocomposition *)videocomposition shouldcontinuevalidatingafterfindinginvalidtrackidininstruction:(id<avvideocompositioninstruction>)videocompositioninstruction layerinstruction:(avvideocompositionlayerinstruction *)layerinstruction asset:(avasset *)asset 
{
    nslog(@"%s===%@===%@",__func__,layerinstruction,asset);
    return yes;
}

重新运行一下发现第三个方法打印输出,就是instruction的timerange问题了,又重新看了一下两个instruction的设置问题,感觉又没什么问题,代码如下:

- (void)buildcompositionwithassets:(nsarray *)assetsarray
{
    for (int i = 0; i < assetsarray.count; i++) {
        avurlasset *asset = assetsarray[i];
        nsarray *videotracks = [asset trackswithmediatype:avmediatypevideo];
        nsarray *audiotracks = [asset trackswithmediatype:avmediatypeaudio];
        
        avassettrack *videotrack = videotracks[0];
        avassettrack *audiotrack = audiotracks[0];
        nserror *error = nil;
        avmutablecompositiontrack *videot = [self.avcompostions.mutablecomps addmutabletrackwithmediatype:avmediatypevideo preferredtrackid:kcmpersistenttrackid_invalid];
        avmutablecompositiontrack *audiot = [self.avcompostions.mutablecomps addmutabletrackwithmediatype:avmediatypeaudio preferredtrackid:kcmpersistenttrackid_invalid];
        [videot inserttimerange:videotrack.timerange oftrack:videotrack attime:self.offsettime error:&error];
        [audiot inserttimerange:audiotrack.timerange oftrack:audiotrack attime:self.offsettime error:nil];
        nsassert(!error, @"insert error = %@",error);
        avmutablevideocompositioninstruction *instruction = [avmutablevideocompositioninstruction videocompositioninstruction];
        avmutablevideocompositionlayerinstruction *layerinstruction = [avmutablevideocompositionlayerinstruction videocompositionlayerinstructionwithassettrack:videot];
        instruction.layerinstructions = @[layerinstruction];
        instruction.timerange = cmtimerangemake(self.offsettime, asset.duration);
        [self.instrucionarray addobject:instruction];
        self.offsettime = cmtimeadd(self.offsettime,asset.duration);
    }
}

这个数组里面会有两个加载好的avasset对象,加载avasset的代码如下:

- (void)loadassetfrompath:(nsarray *)paths
{
    nsmutablearray *assetsarray = [nsmutablearray arraywithcapacity:paths.count];
    dispatch_group_t dispatchgroup = dispatch_group_create();
    for (int i = 0; i < paths.count; i++) {
        nsstring *path = paths[i];
        //first find from cache
        avasset *asset = [self.assetscache objectforkey:path];
        if (!asset) {
            nsdictionary *inputoptions = [nsdictionary dictionarywithobject:[nsnumber numberwithbool:yes] forkey:avurlassetpreferprecisedurationandtimingkey];
            asset = [avurlasset urlassetwithurl:[nsurl fileurlwithpath:path] options:inputoptions];
            // cache asset
            nsassert(asset != nil, @"can't create asset from path", path);
            nsarray *loadkeys = @[@"tracks", @"duration", @"composable"];
            [self loadasset:asset withkeys:loadkeys usingdispatchgroup:dispatchgroup];
            [self.assetscache setobject:asset forkey:path];
        }else {
        }
        [assetsarray addobject:asset];
    }
    dispatch_group_notify(dispatchgroup, dispatch_get_main_queue(), ^{
        !self.assetloadblock?:self.assetloadblock(assetsarray);
        
    });
}


- (void)loadasset:(avasset *)asset withkeys:(nsarray *)assetkeystoload usingdispatchgroup:(dispatch_group_t)dispatchgroup
{
    dispatch_group_enter(dispatchgroup);
    [asset loadvaluesasynchronouslyforkeys:assetkeystoload completionhandler:^(){
        for (nsstring *key in assetkeystoload) {
            nserror *error;
            
            if ([asset statusofvalueforkey:key error:&error] == avkeyvaluestatusfailed) {
                nslog(@"key value loading failed for key:%@ with error: %@", key, error);
                goto bail;
            }
        }
        if (![asset iscomposable]) {
            nslog(@"asset is not composable");
            goto bail;
        }
    bail:
        dispatch_group_leave(dispatchgroup);
    }];
}

按照正常来说,应该不会有什么问题的,但是问题还是来了。打印出创建的两个avmutablevideocompositioninstruction的信息,发现他们的timerange确实有重合的地方。avmutablevideocompositioninstruction的timerange必须对应avmutablecompositiontrack里面的一段段插入的track的timerange。开发文档是这么说的:

to report a video composition instruction with a timerange that's invalid, that overlaps with the timerange of a prior instruction, or that contains times earlier than the timerange of a prior instruction

我在插入的时候使用的是videotrack.timerange,而设置instruction.timerange = cmtimerangemake(self.offsettime, asset.duration);使用的是asset.duration。这个两个竟然是不一样的。asset.duration是{59885/1000 = 59.885},videotrack.timerange是{{0/1000 = 0.000}, {59867/1000 = 59.867}}
这两个时长有细微差别,到底哪个是比较准确一点的呢?

视频文件时长的计算

笔者在demo中使用的是mp4格式的文件,mp4文件是若干个不同的类型box组成的,box可以理解为装有数据的容器。其中有一种moov类型的box里面装有视频播放的元数据(metadata),这里面有视频的时长信息:timescale和duration。 duration / timescale = 可播放时长(s)。从mvhd中读到timescal为0x03e8,duration为0xe9ed,也就是59885 / 1000,为59.885。再看看tkhd box里面的内容,这里面是包含单一track的信息。如下图:

一次“Error Domain=AVFoundationErrorDomain Code=-11841”的调试

一次“Error Domain=AVFoundationErrorDomain Code=-11841”的调试

一次“Error Domain=AVFoundationErrorDomain Code=-11841”的调试

从上图可以看出videotrack和audiotrack两个的时长是不一样的,videotrack为0xe9db,也就是59.867,audiotrack为0xe9ed,就是59.885。所以我们在计算timerange的时候最好统一使用相对精确一点的videotrack,而不要avasset的duration,尽量避免时间上的误差,视频精细化的编辑,对这些误差敏感。