如何控制 AVAssetWriter 以正确的 FPS 写入

How do I control AVAssetWriter to write at the correct FPS

让我看看我是否理解正确。

目前最先进的硬件,iOS 允许我以以下 fps 进行录制:30、60、120 和 240。

但是这些 fps 的行为不同。如果我以 30 或 60 fps 拍摄,我希望以这些 fps 拍摄创建的视频文件分别以 30 和 60 fps 播放。

但是如果我以 120 或 240 fps 拍摄,我希望以这些 fps 拍摄的视频文件以 30 fps 播放,否则我将看不到慢动作。

几个问题:

  1. 我说得对吗?
  2. 有没有办法分别以 120 或 240 fps 拍摄并分别以 120 和 240 fps 播放?我的意思是在没有慢动作的情况下以 fps 播放视频?
  3. 写入文件时如何控制帧率?

我正在像这样创建 AVAssetWriter 输入...

  NSDictionary *videoCompressionSettings = @{AVVideoCodecKey                  : AVVideoCodecH264,
                                             AVVideoWidthKey                  : @(videoWidth),
                                             AVVideoHeightKey                 : @(videoHeight),
                                             AVVideoCompressionPropertiesKey  : @{ AVVideoAverageBitRateKey      : @(bitsPerSecond),
                                                                                   AVVideoMaxKeyFrameIntervalKey : @(1)}
                                             };

    _assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoCompressionSettings];

并且没有明显的方法来控制它。

注意:我在 1 所在的位置尝试了不同的数字。我试过 1.0/fps,我试过 fps,我已经删除了密钥。没有区别。

这就是我设置 `AVAssetWriter 的方式:

  AVAssetWriter *newAssetWriter = [[AVAssetWriter alloc] initWithURL:_movieURL fileType:AVFileTypeQuickTimeMovie
                                          error:&error];

  _assetWriter = newAssetWriter;
  _assetWriter.shouldOptimizeForNetworkUse = NO;

  CGFloat videoWidth = size.width;
  CGFloat videoHeight  = size.height;

  NSUInteger numPixels = videoWidth * videoHeight;
  NSUInteger bitsPerSecond;

  // Assume that lower-than-SD resolutions are intended for streaming, and use a lower bitrate
  //  if ( numPixels < (640 * 480) )
  //    bitsPerPixel = 4.05; // This bitrate matches the quality produced by AVCaptureSessionPresetMedium or Low.
  //  else
  NSUInteger bitsPerPixel = 11.4; // This bitrate matches the quality produced by AVCaptureSessionPresetHigh.

  bitsPerSecond = numPixels * bitsPerPixel;

  NSDictionary *videoCompressionSettings = @{AVVideoCodecKey                  : AVVideoCodecH264,
                                             AVVideoWidthKey                  : @(videoWidth),
                                             AVVideoHeightKey                 : @(videoHeight),
                                             AVVideoCompressionPropertiesKey  : @{ AVVideoAverageBitRateKey      : @(bitsPerSecond)}
                                             };

  if (![_assetWriter canApplyOutputSettings:videoCompressionSettings forMediaType:AVMediaTypeVideo]) {
    NSLog(@"Couldn't add asset writer video input.");
    return;
  }

 _assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
                                                              outputSettings:videoCompressionSettings
                                                            sourceFormatHint:formatDescription];
  _assetWriterVideoInput.expectsMediaDataInRealTime = YES;      

  NSDictionary *adaptorDict = @{
                                (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
                                (id)kCVPixelBufferWidthKey : @(videoWidth),
                                (id)kCVPixelBufferHeightKey : @(videoHeight)
                                };

  _pixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc]
                         initWithAssetWriterInput:_assetWriterVideoInput
                         sourcePixelBufferAttributes:adaptorDict];


  // Add asset writer input to asset writer
  if (![_assetWriter canAddInput:_assetWriterVideoInput]) {
    return;
  }

  [_assetWriter addInput:_assetWriterVideoInput];

captureOutput方法很简单。我从过滤器中获取图像并使用以下方法将其写入文件:

if (videoJustStartWriting)
    [_assetWriter startSessionAtSourceTime:presentationTime];

  CVPixelBufferRef renderedOutputPixelBuffer = NULL;
  OSStatus err = CVPixelBufferPoolCreatePixelBuffer(nil,
                                                    _pixelBufferAdaptor.pixelBufferPool,
                                                    &renderedOutputPixelBuffer);

  if (err) return; //          NSLog(@"Cannot obtain a pixel buffer from the buffer pool");

  //_ciContext is a metal context
  [_ciContext render:finalImage
     toCVPixelBuffer:renderedOutputPixelBuffer
              bounds:[finalImage extent]
          colorSpace:_sDeviceRgbColorSpace];

   [self writeVideoPixelBuffer:renderedOutputPixelBuffer
                  withInitialTime:presentationTime];


- (void)writeVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withInitialTime:(CMTime)presentationTime
{

  if ( _assetWriter.status == AVAssetWriterStatusUnknown ) {
    // If the asset writer status is unknown, implies writing hasn't started yet, hence start writing with start time as the buffer's presentation timestamp
    if ([_assetWriter startWriting]) {
      [_assetWriter startSessionAtSourceTime:presentationTime];
    }
  }

  if ( _assetWriter.status == AVAssetWriterStatusWriting ) {
    // If the asset writer status is writing, append sample buffer to its corresponding asset writer input

      if (_assetWriterVideoInput.readyForMoreMediaData) {
        if (![_pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:presentationTime]) {
          NSLog(@"error", [_assetWriter.error localizedFailureReason]);
        }
      }
  }

  if ( _assetWriter.status == AVAssetWriterStatusFailed ) {
    NSLog(@"failed");
  }

}

我把整件事设置为每秒 240 帧的速度。这些是附加帧的显示时间。

time ======= 113594.311510508
time ======= 113594.324011508
time ======= 113594.328178716
time ======= 113594.340679424
time ======= 113594.344846383

如果你在它们之间做一些计算,你会发现帧率大约是 240 fps。因此帧以正确的时间存储。

但是当我看视频时,运动不是慢动作,快时间说视频是 30 fps。

注意:此应用程序从相机抓取帧,帧进入 CIFilters,这些过滤器的结果被转换回存储到文件并显示在屏幕上的样本缓冲区。

iOS 屏幕刷新锁定在 60fps,所以 "see" 额外帧的唯一方法就是,如您所说,减慢播放速度,a.k.a 慢运动。

所以

  1. 是的,你是对的
  2. 屏幕刷新率(假设您是人类,可能还有人类视觉系统的局限性?)意味着您无法感知 120 和 240fps 的帧速率。您可以可以 通过降低屏幕刷新率来以正常速度播放它们。当然,这就是 AVPlayer 已经在做的事情,尽管我不确定这是否是您正在寻找的答案。
  3. 当您使用 CMSampleBuffer 表示时间戳编写文件时,您可以控制文件的帧率。如果您的帧来自相机,您可能会直接传递时间戳,在这种情况下,请检查您是否确实获得了您要求的帧率(捕获回调中的日志语句应该足以验证这一点)。如果您按程序创建帧,则选择演示文稿时间戳,使它们间隔 1.0/desiredFrameRate 秒!

3. 不适合你吗?

p.s。您可以丢弃并忽略 AVVideoMaxKeyFrameIntervalKey - 这是一个质量设置,与播放帧率无关。

我到了这里,但我认为这是你出错的地方。将您的视频捕获视为管道。

(1) Capture buffer -> (2) Do Something With buffer -> (3) Write buffer as frames in video.

听起来您已经成功完成了 (1) 和 (2),您获得缓冲区的速度足够快并且正在处理它们,因此您可以将它们作为帧出售。

几乎可以肯定问题出在(3)写入视频帧。

https://developer.apple.com/reference/avfoundation/avmutablevideocomposition

检查你的 AVMutableComposition 中的 frameDuration 设置,你需要像 CMTime(1, 60) //60FPS 或 CMTime(1, 240) // 240FPS 这样的东西来得到你想要的东西(告诉视频写入这么多帧并以此速率编码)。

使用AVAssetWriter,原理是一样的,只是你在AVAssetWriterInput outputSettings 中设置帧率属性 添加AVVideoExpectedSourceFrameRateKey。

NSDictionary *videoCompressionSettings = @{AVVideoCodecKey                  : AVVideoCodecH264,
                                         AVVideoWidthKey                  : @(videoWidth),
                                         AVVideoHeightKey                 : @(videoHeight),
                                       AVVideoExpectedSourceFrameRateKey : @(60),
                                         AVVideoCompressionPropertiesKey  : @{ AVVideoAverageBitRateKey      : @(bitsPerSecond),
                                                                               AVVideoMaxKeyFrameIntervalKey : @(1)}
                                         };

再扩展一点——你不能严格控制或同步你的相机捕捉到输出/回放速率,时间只是不能那样工作,也不那么准确,当然处理管道增加了开销。当您捕获帧时,它们带有时间戳,您已经看到了,但是在写入/压缩阶段,它仅使用它需要的帧来生成为合成指定的输出。

它是双向的,你只能捕捉 30 FPS 并以 240 FPS 输出,视频会显示正常,你只是有很多帧 "missing" 并被算法填充.您甚至可以每秒仅出售 1 帧并以 30FPS 播放,两者相互独立(我捕获的速度与每秒显示的帧数和呈现的内容的速度)

至于如何以不同的速度播放它,你只需要调整播放速度-根据需要放慢速度。

如果您正确设置了时基 (frameDuration),它将始终播放 "normal" - 您告诉它 "play back is X Frames Per Second",当然,您的眼睛可能会注意到差异(几乎可以肯定在低 FPS 和高 FPS 之间),并且屏幕可能不会刷新那么高(高于 60FPS),但无论视频的时基速度如何,视频都会以 "normal" 1 倍的速度刷新。通过放慢视频速度,如果我的时基是 120,我将它减慢到 0.5 倍,我知道有效地看到 60FPS,播放一秒需要两秒。

您可以通过在 AVPlayer https://developer.apple.com/reference/avfoundation/avplayer

上设置速率 属性 来控制播放速度