如何同时使用AVAssetReader和AVAssetWriter处理多个音视频轨道?

11
我知道如何使用AVAssetReaderAVAssetWriter,并已成功地将其用于从一个电影中获取视频轨道并转码为另一个电影。但是,我也想用这种方法处理音频。我必须在完成初始转码后创建AVAssetExportSession,还是有一种方法可以在写入会话的过程中在不同的轨道之间切换?我不想处理AVAssetExportSession的额外开销。

我之所以问这个问题,是因为使用拉取式方法 - while ([assetWriterInput isReadyForMoreMediaData]) {...} - 假定只有一个轨道。那么如何同时使用多个轨道,即同时处理音频和视频轨道呢?


你能指导我学习如何使用AVAssetWriter进行转码吗?我只是想将比特率和分辨率降低。 - Ryan
3个回答

8
AVAssetWriter会自动交错请求其关联的AVAssetWriterInput,以将不同的轨道整合到输出文件中。只需为每个轨道添加一个AVAssetWriterInput,然后在每个AVAssetWriterInput上调用requestMediaDataWhenReadyOnQueue:usingBlock:

这是我拥有的一个方法,它调用了requestMediaDataWhenReadyOnQueue:usingBlock:。我从循环输出/输入对的数量中调用此方法。(单独的方法既有利于代码可读性,也因为与循环不同,每个调用都为块设置了单独的堆栈帧。)

您只需要一个dispatch_queue_t,并且可以重复使用它来处理所有轨道。请注意,绝对不应从块中调用dispatch_async,因为requestMediaDataWhenReadyOnQueue:usingBlock:期望该块阻塞,直到填充了尽可能多的数据,直到AVAssetWriterInput将其接受。在此之前,您不希望返回。

- (void)requestMediaDataForTrack:(int)i {
  AVAssetReaderOutput *output = [[_reader outputs] objectAtIndex:i];
  AVAssetWriterInput *input = [[_writer inputs] objectAtIndex:i];

  [input requestMediaDataWhenReadyOnQueue:_processingQueue usingBlock:
    ^{
      [self retain];
      while ([input isReadyForMoreMediaData]) {
        CMSampleBufferRef sampleBuffer;
        if ([_reader status] == AVAssetReaderStatusReading &&
            (sampleBuffer = [output copyNextSampleBuffer])) {

          BOOL result = [input appendSampleBuffer:sampleBuffer];
          CFRelease(sampleBuffer);

          if (!result) {
            [_reader cancelReading];
            break;
          }
        } else {
          [input markAsFinished];

          switch ([_reader status]) {
            case AVAssetReaderStatusReading:
              // the reader has more for other tracks, even if this one is done
              break;

            case AVAssetReaderStatusCompleted:
              // your method for when the conversion is done
              // should call finishWriting on the writer
              [self readingCompleted];
              break;

            case AVAssetReaderStatusCancelled:
              [_writer cancelWriting];
              [_delegate converterDidCancel:self];
              break;

            case AVAssetReaderStatusFailed:
              [_writer cancelWriting];
              break;
          }

          break;
        }
      }
    }
  ];
}

1
[self retain] 是怎么回事?这不会使对象的保留计数任意增加吗?谁会释放这个对象? - Robotbugs
可能的意图是要释放对象readingCompleted。话虽如此,因为存在[self readingCompleted]的调用,使得运行时自动保留self,这可能是不必要的。 - Fiona Hopkins

1

你尝试过使用两个AVAssetWriterInputs并通过工作队列推送样本吗?这是一个大致的草图。

processing_queue = dispatch_queue_create("com.mydomain.gcdqueue.mediaprocessor", NULL);

[videoAVAssetWriterInput requestMediaDataWhenReadyOnQueue:myInputSerialQueue usingBlock:^{
    dispatch_asyc(processing_queue, ^{process video});
}];

[audioAVAssetWriterInput requestMediaDataWhenReadyOnQueue:myInputSerialQueue usingBlock:^{
    dispatch_asyc(processing_queue, ^{process audio});
}];

谢谢您的建议。我已经尝试了,但是无法弄清如何正确结束写入会话。对于一个轨道,我只需要查看copyNextSampleBuffer是否为空,然后结束会话。对于多个轨道,我尝试创建一个标志来表示音频或视频是否完成,如果两者都完成,则结束会话。然而,即使我检查了资产阅读器状态为AVAssetReaderStatusReading,我仍然收到错误消息:[AVAssetReaderTrackOutput copyNextSampleBuffer] 除非资产阅读器处于“reading”状态,否则无法复制下一个样本缓冲区。 - akaru
我将在不久的将来测试类似这样的策略。如果我找到了解决方案,我会回复发帖。 - Steve McFarlin
你是否在 requestMediaDataWhenReadyOnQueue 方法中调用了 [AVAssetWriterInput markAsFinished]; ?我不会调用那个方法,而是在 AVAssetWriter 完成所有操作后调用 finishWriting 方法。 - Steve McFarlin
抱歉回复晚了,我没有收到评论通知。我只调用finishWriting,就像你正在做的那样。 - akaru
顺便提一下,你的“阅读状态”错误可能实际上是编解码器问题。如果输入和输出不兼容的格式,有时就会出现这种情况。 - Fiona Hopkins

0

你可以使用调度组!

查看MacOSX的AVReaderWriter示例...

我直接引用了样本RWDocument.m:

- (BOOL)startReadingAndWritingReturningError:(NSError **)outError
{
    BOOL success = YES;
    NSError *localError = nil;

    // Instruct the asset reader and asset writer to get ready to do work
    success = [assetReader startReading];
    if (!success)
        localError = [assetReader error];
    if (success)
    {
        success = [assetWriter startWriting];
        if (!success)
            localError = [assetWriter error];
    }

    if (success)
    {
        dispatch_group_t dispatchGroup = dispatch_group_create();

        // Start a sample-writing session
        [assetWriter startSessionAtSourceTime:[self timeRange].start];

        // Start reading and writing samples
        if (audioSampleBufferChannel)
        {
            // Only set audio delegate for audio-only assets, else let the video channel drive progress
            id <RWSampleBufferChannelDelegate> delegate = nil;
            if (!videoSampleBufferChannel)
                delegate = self;

            dispatch_group_enter(dispatchGroup);
            [audioSampleBufferChannel startWithDelegate:delegate completionHandler:^{
                dispatch_group_leave(dispatchGroup);
            }];
        }
        if (videoSampleBufferChannel)
        {
            dispatch_group_enter(dispatchGroup);
            [videoSampleBufferChannel startWithDelegate:self completionHandler:^{
                dispatch_group_leave(dispatchGroup);
            }];
        }

        // Set up a callback for when the sample writing is finished
        dispatch_group_notify(dispatchGroup, serializationQueue, ^{
            BOOL finalSuccess = YES;
            NSError *finalError = nil;

            if (cancelled)
            {
                [assetReader cancelReading];
                [assetWriter cancelWriting];
            }
            else
            {
                if ([assetReader status] == AVAssetReaderStatusFailed)
                {
                    finalSuccess = NO;
                    finalError = [assetReader error];
                }

                if (finalSuccess)
                {
                    finalSuccess = [assetWriter finishWriting];
                    if (!finalSuccess)
                        finalError = [assetWriter error];
                }
            }

            [self readingAndWritingDidFinishSuccessfully:finalSuccess withError:finalError];
        });

        dispatch_release(dispatchGroup);
    }

    if (outError)
        *outError = localError;

    return success;
}

注意最后的调度组通知命令,以了解所有音频/视频何时完成...这是离屏渲染...不是实时的。 - Orbitus007
“离屏”和“实时”不是相反的概念;而“渲染”和“离屏”则是(相反的)。 “实时”是一个主观的概念,它描述了视频帧从源获取后的显示速率。它没有相反的概念,甚至没有“非实时”的概念。你不会希望转码器以实时的速度工作。转码应该尽可能快地完成;在广播行业中有专门的设备来完成这项工作。无论如何,没有比正常实时更慢的东西了。即使是播放软件也必须控制视频的播放速度,以避免其显示得比人眼所能感知的更快。 - James Bush

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接