chromium/media/capture/video/apple/video_capture_device_avfoundation.mm

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#import "media/capture/video/apple/video_capture_device_avfoundation.h"
#include <optional>
#include "base/feature_list.h"
#include "base/time/time.h"

#import <AVFoundation/AVFoundation.h>
#import <CoreMedia/CoreMedia.h>
#import <CoreVideo/CoreVideo.h>
#include <stddef.h>
#include <stdint.h>
#include <optional>
#include <sstream>

#include "base/apple/foundation_util.h"
#include "base/debug/dump_without_crashing.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#import "base/task/single_thread_task_runner.h"
#include "components/crash/core/common/crash_key.h"
#include "media/base/mac/color_space_util_mac.h"
#include "media/base/timestamp_constants.h"
#include "media/base/video_types.h"
#include "media/capture/video/apple/video_capture_device_apple.h"
#import "media/capture/video/apple/video_capture_device_avfoundation_utils.h"
#include "media/capture/video/apple/video_capture_device_factory_apple.h"
#include "media/capture/video_capture_types.h"
#include "ui/gfx/geometry/size.h"

#if BUILDFLAG(IS_MAC)
#import "media/capture/video/mac/video_capture_metrics_mac.h"
#endif

#if BUILDFLAG(IS_IOS)
#import <UIKit/UIKit.h>
#endif

BASE_FEATURE(kAVFoundationCaptureForwardSampleTimestamps,
             "AVFoundationCaptureForwardSampleTimestamps",
             base::FEATURE_DISABLED_BY_DEFAULT);

BASE_FEATURE(kAVFoundationCaptureSonomaRestartStalledCamera,
             "AVFoundationCaptureSonomaRestartStalledCamera",
             base::FEATURE_DISABLED_BY_DEFAULT);

namespace {

// Logitech 4K Pro
constexpr NSString* kModelIdLogitech4KPro =
    @"UVC Camera VendorID_1133 ProductID_2175";

constexpr gfx::ColorSpace kColorSpaceRec709Apple(
    gfx::ColorSpace::PrimaryID::BT709,
    gfx::ColorSpace::TransferID::BT709_APPLE,
    gfx::ColorSpace::MatrixID::SMPTE170M,
    gfx::ColorSpace::RangeID::LIMITED);

constexpr int kTimeToWaitBeforeStoppingPhotoOutputInSeconds = 60;
constexpr FourCharCode kDefaultFourCCPixelFormat =
    kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;  // NV12 (a.k.a. 420v)

// Allowable epsilon when comparing the requested framerate against the
// captures' min/max framerates, to handle float inaccuracies.
// Framerates will be in the range of 1-100 or so, meaning under- or
// overshooting by 0.001 fps will be negligible, but still handling float loss
// of precision during manipulation.
constexpr float kFrameRateEpsilon = 0.001;

std::optional<base::TimeTicks> GetCMSampleBufferTimestamp(
    CMSampleBufferRef sampleBuffer) {
  const CMTime cm_timestamp =
      CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
  if (CMTIME_IS_VALID(cm_timestamp)) {
    uint64_t mach_time = CMClockConvertHostTimeToSystemUnits(cm_timestamp);
    return base::TimeTicks::FromMachAbsoluteTime(mach_time);
  }
  return std::nullopt;
}

bool ShouldRestartStalledCamera() {
  // The stall check should not be needed on macOS 14 due to a redesign of the
  // camera capture in macOS 14. It also interferes with the Presenter's Overlay
  // feature that was introduced in macOS 14. See https://crbug.com/335210401.
  if (@available(macOS 14.0, *)) {
    return base::FeatureList::IsEnabled(
        kAVFoundationCaptureSonomaRestartStalledCamera);
  }
  return true;
}

constexpr size_t kPixelBufferPoolSize = 10;

}  // anonymous namespace

namespace media {

// Uses the most recent advice from Apple for configuring and starting.
BASE_FEATURE(kConfigureCaptureBeforeStart,
             "ConfigureCaptureBeforeStart",
             base::FEATURE_ENABLED_BY_DEFAULT);

// Allow disabling optimizations (https://crbug.com/1143477,
// https://crbug.com/959962) because of flickering (https://crbug.com/1515598).
BASE_FEATURE(kOverrideCameraIOSurfaceColorSpace,
             "OverrideCameraIOSurfaceColorSpace",
             base::FEATURE_DISABLED_BY_DEFAULT);

AVCaptureDeviceFormat* FindBestCaptureFormat(
    NSArray<AVCaptureDeviceFormat*>* formats,
    int width,
    int height,
    float frame_rate) {
  AVCaptureDeviceFormat* bestCaptureFormat = nil;
  VideoPixelFormat bestPixelFormat = VideoPixelFormat::PIXEL_FORMAT_UNKNOWN;
  bool bestMatchesFrameRate = false;
  Float64 bestMaxFrameRate = 0;

  for (AVCaptureDeviceFormat* captureFormat in formats) {
    const FourCharCode fourcc =
        CMFormatDescriptionGetMediaSubType(captureFormat.formatDescription);
    VideoPixelFormat pixelFormat =
        [VideoCaptureDeviceAVFoundation FourCCToChromiumPixelFormat:fourcc];
    CMVideoDimensions dimensions =
        CMVideoFormatDescriptionGetDimensions(captureFormat.formatDescription);

    // If the pixel format is unsupported by our code, then it is not useful.
    if (pixelFormat == VideoPixelFormat::PIXEL_FORMAT_UNKNOWN) {
      continue;
    }

    // If our CMSampleBuffers will have a different size than the native
    // capture, then we will not be the fast path.
    if (dimensions.width != width || dimensions.height != height) {
      continue;
    }

    Float64 maxFrameRate = 0;
    bool matchesFrameRate = false;
    for (AVFrameRateRange* frameRateRange in captureFormat
             .videoSupportedFrameRateRanges) {
      maxFrameRate = std::max(maxFrameRate, frameRateRange.maxFrameRate);
      matchesFrameRate |=
          frameRateRange.minFrameRate <= frame_rate + kFrameRateEpsilon &&
          frame_rate - kFrameRateEpsilon <= frameRateRange.maxFrameRate;
    }
    // Prefer a capture format that handles the requested framerate to one
    // that doesn't.
    if (bestCaptureFormat) {
      if (bestMatchesFrameRate && !matchesFrameRate) {
        continue;
      }
      if (matchesFrameRate && !bestMatchesFrameRate) {
        bestCaptureFormat = nil;
      }
    }

    // Prefer a capture format with a lower maximum framerate, under the
    // assumption that that may have lower power consumption.
    if (bestCaptureFormat) {
      if (bestMaxFrameRate < maxFrameRate) {
        continue;
      }
      if (maxFrameRate < bestMaxFrameRate) {
        bestCaptureFormat = nil;
      }
    }

    // Finally, compare according to Chromium preference.
    if (bestCaptureFormat) {
      if (VideoCaptureFormat::ComparePixelFormatPreference(bestPixelFormat,
                                                           pixelFormat)) {
        continue;
      }
    }

    bestCaptureFormat = captureFormat;
    bestPixelFormat = pixelFormat;
    bestMaxFrameRate = maxFrameRate;
    bestMatchesFrameRate = matchesFrameRate;
  }

  VLOG(1) << "Selecting AVCaptureDevice format "
          << VideoPixelFormatToString(bestPixelFormat);
  return bestCaptureFormat;
}

}  // namespace media

@implementation VideoCaptureDeviceAVFoundation {
  // The following attributes are set via -setCaptureHeight:width:frameRate:.
  float _frameRate;

#if BUILDFLAG(IS_IOS)
  UIDeviceOrientation _orientation;
#endif
  int _rotation;

  // Usage of GPU memory buffer is controlled by
  // `--disable-video-capture-use-gpu-memory-buffer` and
  // `--video-capture-use-gpu-memory-buffer` commandline switches. This flag
  // handles whether to use a GPU memory for a video frame or not.
  bool _useGPUMemoryBuffer;

  // The capture format that best matches the above attributes.
  AVCaptureDeviceFormat* __strong _bestCaptureFormat;

  // A serial queue to deliver frames on, ensuring frames are delivered in
  // order.
  dispatch_queue_t __strong _sampleQueue;

  // Protects concurrent setting and using |frameReceiver_|. Note that the
  // GUARDED_BY decoration below does not have any effect.
  base::Lock _lock;
  // Used to avoid UAF in -captureOutput.
  base::Lock _destructionLock;
  raw_ptr<media::VideoCaptureDeviceAVFoundationFrameReceiver> _frameReceiver
      GUARDED_BY(_lock);  // weak.
  bool _capturedFirstFrame GUARDED_BY(_lock);
  bool _capturedFrameSinceLastStallCheck GUARDED_BY(_lock);
  struct SelfHolder {
    VideoCaptureDeviceAVFoundation* __weak the_self;
    base::WeakPtrFactory<SelfHolder> weak_ptr_factory{this};
  };
  SelfHolder _weakPtrHolderForStallCheck;
  // TimeTicks to subtract from all frames, to avoid leaking uptime.
  base::TimeTicks _startTimestamp;

  // Used to rate-limit crash reports for https://crbug.com/1168112.
  bool _hasDumpedForFrameSizeMismatch;

  AVCaptureSession* __strong _captureSession;

  // |captureDevice_| is an object coming from AVFoundation, used only to be
  // plugged in |captureDeviceInput_| and to query for session preset support.
  AVCaptureDevice* __strong _captureDevice;
  AVCaptureDeviceInput* __strong _captureDeviceInput;
  AVCaptureVideoDataOutput* __strong _captureVideoDataOutput;

  // When enabled, converts captured frames to NV12.
  std::unique_ptr<media::SampleBufferTransformer> _sampleBufferTransformer;

  AVCapturePhotoOutput* __strong _photoOutput;

  // Only accessed on the main thread. The takePhoto() operation is considered
  // pending until we're ready to take another photo, which involves a PostTask
  // back to the main thread after the photo was taken.
  size_t _pendingTakePhotos;
  SelfHolder _weakPtrHolderForTakePhoto;

  // For testing.
  base::RepeatingCallback<void()> _onPhotoOutputStopped;
  std::optional<bool> _isPortraitEffectSupportedForTesting;
  std::optional<bool> _isPortraitEffectActiveForTesting;

  scoped_refptr<base::SingleThreadTaskRunner> _mainThreadTaskRunner;
}

#pragma mark Class methods

+ (media::VideoPixelFormat)FourCCToChromiumPixelFormat:(FourCharCode)code {
  switch (code) {
    case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
      return media::PIXEL_FORMAT_NV12;  // Mac fourcc: "420v".
    case kCVPixelFormatType_422YpCbCr8:
      return media::PIXEL_FORMAT_UYVY;  // Mac fourcc: "2vuy".
    case kCMPixelFormat_422YpCbCr8_yuvs:
      return media::PIXEL_FORMAT_YUY2;
    case kCMVideoCodecType_JPEG_OpenDML:
      return media::PIXEL_FORMAT_MJPEG;  // Mac fourcc: "dmb1".
    default:
      return media::PIXEL_FORMAT_UNKNOWN;
  }
}

#pragma mark Public methods

- (instancetype)initWithFrameReceiver:
    (media::VideoCaptureDeviceAVFoundationFrameReceiver*)frameReceiver {
  if ((self = [super init])) {
    _mainThreadTaskRunner = base::SingleThreadTaskRunner::GetCurrentDefault();
    _sampleQueue =
        dispatch_queue_create("org.chromium.VideoCaptureDeviceAVFoundation."
                              "SampleDeliveryDispatchQueue",
                              DISPATCH_QUEUE_SERIAL);
    DCHECK(frameReceiver);
    _rotation = 0;
    _useGPUMemoryBuffer = true;
    _capturedFirstFrame = false;
    _weakPtrHolderForStallCheck.the_self = self;
    _weakPtrHolderForTakePhoto.the_self = self;
    [self setFrameReceiver:frameReceiver];
    _captureSession = [[AVCaptureSession alloc] init];
    _sampleBufferTransformer = media::SampleBufferTransformer::Create();

#if BUILDFLAG(IS_IOS)
    _orientation = UIDeviceOrientationUnknown;
    [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(orientationChanged:)
               name:UIDeviceOrientationDidChangeNotification
             object:[UIDevice currentDevice]];
#endif
  }
  return self;
}

#if BUILDFLAG(IS_IOS)
- (void)orientationChanged:(NSNotification*)note {
  UIDevice* device = note.object;
  UIDeviceOrientation deviceOrientation = device.orientation;
  AVCaptureConnection* captureConnection =
      [_captureVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
  if ([captureConnection isVideoOrientationSupported]) {
    _orientation = deviceOrientation;
    AVCaptureDevicePosition camera_position =
        [[_captureDeviceInput device] position];
    _rotation =
        media::MaybeGetVideoRotation(_orientation, camera_position).value_or(0);
    [self captureConfigurationChanged];
  }
}
#endif

- (void)dealloc {
  // Stopping a running photo output takes `_lock`. To avoid this happening
  // inside stopCapture() below which would deadlock, we ensure that the photo
  // output is already stopped before taking `_lock`.
  [self stopPhotoOutput];
  {
    // To avoid races with concurrent callbacks, grab the lock before stopping
    // capture and clearing all the variables.
    base::AutoLock lock(_lock);

    // Cleanup AVCaptureSession
    // 1. Stop the AVCaptureSession
    [self stopCapture];
    // 2. Remove AVCaptureInputs and AVCaptureOutputs
    for (AVCaptureInput* input in _captureSession.inputs) {
      [_captureSession removeInput:input];
    }
    for (AVCaptureOutput* output in _captureSession.outputs) {
      [_captureSession removeOutput:output];
    }
    // 3. Set the AVCaptureSession to nil to remove strong references
    _captureSession = nil;

    // Cleanup AVCaptureDevice
    // 1. Unlock any configuration (if locked)
    [_captureDevice unlockForConfiguration];
    // 2. Remove observer
    [_captureDevice removeObserver:self forKeyPath:@"portraitEffectActive"];
    // 3. Release and deallocate the capture device
    _captureDevice = nil;

    _frameReceiver = nullptr;
    _sampleBufferTransformer.reset();
    _mainThreadTaskRunner = nullptr;
    _sampleQueue = nil;
  }
  {
    // Ensures -captureOutput has finished before we continue the destruction
    // steps. If -captureOutput grabbed the destruction lock before us this
    // prevents UAF. If -captureOutput grabbed the destruction lock after us
    // it will exit early because |_frameReceiver| is already null at this
    // point.
    base::AutoLock destructionLock(_destructionLock);
  }
}

- (void)setFrameReceiver:
    (media::VideoCaptureDeviceAVFoundationFrameReceiver*)frameReceiver {
  base::AutoLock lock(_lock);
  _frameReceiver = frameReceiver;
}

- (void)logMessage:(const std::string&)message {
  base::AutoLock lock(_lock);
  [self logMessageLocked:message];
}

- (void)logMessageLocked:(const std::string&)message {
  auto loggedMessage = std::string("AVFoundation: ") + message;
  VLOG(1) << loggedMessage;
  if (_frameReceiver) {
    _frameReceiver->OnLog(loggedMessage);
  }
}

- (void)setUseGPUMemoryBuffer:(bool)useGPUMemoryBuffer {
  _useGPUMemoryBuffer = useGPUMemoryBuffer;
}

- (BOOL)setCaptureDevice:(NSString*)deviceId
            errorMessage:(NSString**)outMessage {
  DCHECK(_captureSession);
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());

  if (!deviceId) {
    // First stop the capture session, if it's running.
    [self stopCapture];
    // Now remove the input and output from the capture session.
    [_captureSession removeOutput:_captureVideoDataOutput];
    [self stopPhotoOutput];
    if (_captureDeviceInput) {
      DCHECK(_captureDevice);
      if (@available(macOS 12.0, *)) {
        [_captureDevice removeObserver:self forKeyPath:@"portraitEffectActive"];
      }
      [_captureSession stopRunning];
      [_captureSession removeInput:_captureDeviceInput];
      _captureDeviceInput = nil;
      _captureDevice = nil;
    }
    return YES;
  }

  // Look for input device with requested name.
  _captureDevice = [AVCaptureDevice deviceWithUniqueID:deviceId];
  if (!_captureDevice) {
    *outMessage = @"Could not open video capture device.";
    return NO;
  }

  // Create the capture input associated with the device. Easy peasy.
  NSError* error = nil;
  _captureDeviceInput =
      [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice error:&error];
  if (!_captureDeviceInput) {
    _captureDevice = nil;
    *outMessage = [NSString
        stringWithFormat:@"Could not create video capture input (%@): %@",
                         error.localizedDescription,
                         error.localizedFailureReason];
    return NO;
  }
  [_captureSession addInput:_captureDeviceInput];

  // Create a new data output for video. The data output is configured to
  // discard late frames by default.
  _captureVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
  if (!_captureVideoDataOutput) {
    [_captureSession removeInput:_captureDeviceInput];
    *outMessage = @"Could not create video data output.";
    return NO;
  }
  _captureVideoDataOutput.alwaysDiscardsLateVideoFrames = true;

  [_captureVideoDataOutput setSampleBufferDelegate:self queue:_sampleQueue];
  [_captureSession addOutput:_captureVideoDataOutput];

  if (@available(macOS 12.0, *)) {
    [_captureDevice addObserver:self
                     forKeyPath:@"portraitEffectActive"
                        options:0
                        context:(__bridge void*)_captureDevice];
  }

#if BUILDFLAG(IS_IOS)
  _orientation = [[UIDevice currentDevice] orientation];
  if (_orientation == UIDeviceOrientationUnknown) {
    _orientation = UIDeviceOrientationPortrait;
  }

  AVCaptureDevicePosition camera_position =
      [[_captureDeviceInput device] position];
  _rotation =
      media::MaybeGetVideoRotation(_orientation, camera_position).value_or(0);
#endif

  return YES;
}

- (BOOL)setCaptureHeight:(int)height
                   width:(int)width
               frameRate:(float)frameRate {
  DCHECK(![_captureSession isRunning]);
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());

  _frameRate = frameRate;
  _bestCaptureFormat = media::FindBestCaptureFormat(_captureDevice.formats,
                                                    width, height, frameRate);
  FourCharCode best_fourcc = kDefaultFourCCPixelFormat;
  if (_bestCaptureFormat) {
    best_fourcc = CMFormatDescriptionGetMediaSubType(
        _bestCaptureFormat.formatDescription);
  }

  if (best_fourcc == kCMVideoCodecType_JPEG_OpenDML) {
    // Capturing MJPEG for the following camera does not work (frames not
    // forwarded). macOS can convert to the default pixel format for us instead.
    // TODO(crbug.com/40147585): figure out if there's another workaround.
    if ([_captureDevice.modelID isEqualToString:kModelIdLogitech4KPro]) {
      LOG(WARNING) << "Activating MJPEG workaround for camera "
                   << base::SysNSStringToUTF8(kModelIdLogitech4KPro);
      best_fourcc = kDefaultFourCCPixelFormat;
    }
  }

  VLOG(2) << __func__ << ": configuring '"
          << media::MacFourCCToString(best_fourcc) << "' " << width << "x"
          << height << "@" << frameRate;

  // The capture output has to be configured, despite Mac documentation
  // detailing that setting the sessionPreset would be enough. The reason for
  // this mismatch is probably because most of the AVFoundation docs are written
  // for iOS and not for macOS. AVVideoScalingModeKey() refers to letterboxing
  // yes/no and preserve aspect ratio yes/no when scaling. Currently we set
  // cropping and preservation.
  NSDictionary* videoSettingsDictionary = @{
#if BUILDFLAG(IS_MAC)
    (id)kCVPixelBufferWidthKey : @(width),
    (id)kCVPixelBufferHeightKey : @(height),
    AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill,
#endif
    (id)kCVPixelBufferPixelFormatTypeKey : @(best_fourcc)
  };

  _captureVideoDataOutput.videoSettings = videoSettingsDictionary;

#if (!defined(__IPHONE_7_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0)
  AVCaptureConnection* captureConnection =
      [_captureVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
  // CMTimeMake accepts integer arguments but |frameRate| is float, so round it.
  if (captureConnection.supportsVideoMinFrameDuration) {
    captureConnection.videoMinFrameDuration =
        CMTimeMake(media::kFrameRatePrecision,
                   (int)(frameRate * media::kFrameRatePrecision));
  }
  if (captureConnection.supportsVideoMaxFrameDuration) {
    captureConnection.videoMaxFrameDuration =
        CMTimeMake(media::kFrameRatePrecision,
                   (int)(frameRate * media::kFrameRatePrecision));
  }
#endif
  return YES;
}

- (BOOL)startCapture {
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
  if (!_captureSession) {
    [self logMessage:"Video capture session not initialized."];
    return NO;
  }
  // Connect the notifications.
  NSNotificationCenter* nc = NSNotificationCenter.defaultCenter;
  [nc addObserver:self
         selector:@selector(onVideoError:)
             name:AVCaptureSessionRuntimeErrorNotification
           object:_captureSession];

  if (base::FeatureList::IsEnabled(media::kConfigureCaptureBeforeStart)) {
    if (_bestCaptureFormat) {
      [_captureSession beginConfiguration];
      if ([_captureDevice lockForConfiguration:nil]) {
        [_captureDevice setActiveFormat:_bestCaptureFormat];
        [_captureDevice unlockForConfiguration];
      }
      [_captureSession commitConfiguration];
    }

    [_captureSession startRunning];
  } else {
    [_captureSession startRunning];
    if (_bestCaptureFormat && [_captureDevice lockForConfiguration:nil]) {
      [_captureDevice setActiveFormat:_bestCaptureFormat];
      [_captureDevice unlockForConfiguration];
    }
  }

  {
    base::AutoLock lock(_lock);
    _capturedFirstFrame = false;
    _capturedFrameSinceLastStallCheck = NO;
  }
  [self doStallCheck:0];
  return YES;
}

- (void)stopCapture {
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
  _weakPtrHolderForStallCheck.weak_ptr_factory.InvalidateWeakPtrs();
  [self stopPhotoOutput];
  if (_captureSession.running) {
    [_captureSession stopRunning];  // Synchronous.
  }
  [NSNotificationCenter.defaultCenter removeObserver:self];
}

- (void)takePhoto {
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
  DCHECK(_captureSession.running);

  ++_pendingTakePhotos;
  if (_pendingTakePhotos > 1u) {
    // There is already pending takePhoto(). When it finishes it will kick off
    // the next takePhotoInternal(), so there is nothing more to do here.
    return;
  }
  // `_pendingTakePhotos` just went from 0 to 1. In case the 60 second delayed
  // task to perform stopPhotoOutput() is in-flight, invalidate weak ptrs to
  // cancel any such operation.
  _weakPtrHolderForTakePhoto.weak_ptr_factory.InvalidateWeakPtrs();

  // Ready to take a photo immediately?
  // Thread-safe because `_photoOutput` is only modified on the main thread.
  if (_photoOutput) {
    [self takePhotoInternal];
    return;
  }

  // Lazily instantiate `_photoOutput` so that if the app never calls
  // takePhoto() we don't have to pay the associated performance cost, see
  // https://crbug.com/1116241. This procedure is purposefully delayed by 3
  // seconds because the camera needs to ramp up after re-configuring itself in
  // order for 3A to stabilize or else the photo is dark/black.
  {
    // `_lock` is needed since `_photoOutput` may be read from non-main thread.
    base::AutoLock lock(_lock);
    _photoOutput = [[AVCapturePhotoOutput alloc] init];
  }
  if (![_captureSession canAddOutput:_photoOutput]) {
    {
      base::AutoLock lock(_lock);
      if (_frameReceiver) {
        _frameReceiver->OnPhotoError();
      }
    }
    [self takePhotoResolved];
    return;
  } else {
    [_captureSession addOutput:_photoOutput];
  }
  // A delay is needed before taking the photo or else the photo may be dark.
  // 2 seconds was enough in manual testing; we delay by 3 for good measure.
  _mainThreadTaskRunner->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(
          [](base::WeakPtr<SelfHolder> weakSelf) {
            if (!weakSelf.get()) {
              return;
            }
            [weakSelf.get()->the_self takePhotoInternal];
          },
          _weakPtrHolderForTakePhoto.weak_ptr_factory.GetWeakPtr()),
      base::Seconds(3));
}

- (void)setOnPhotoOutputStoppedForTesting:
    (base::RepeatingCallback<void()>)onPhotoOutputStopped {
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
  _onPhotoOutputStopped = onPhotoOutputStopped;
}

#pragma mark Private methods

- (void)takePhotoInternal {
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
  DCHECK(_captureSession.running);
  // takePhotoInternal() can only happen when we have a `_photoOutput` because
  // stopPhotoOutput() cancels in-flight operations by invalidating weak ptrs.
  DCHECK(_photoOutput);
  @try {
    // Asynchronous success or failure is handled inside
    // captureOutput:didFinishProcessingPhoto:error on an unknown thread.
    // Synchronous failures are handled in the catch clause below.
    [_photoOutput
        capturePhotoWithSettings:[AVCapturePhotoSettings
                                     photoSettingsWithFormat:@{
                                       AVVideoCodecKey : AVVideoCodecTypeJPEG
                                     }]
                        delegate:self];
  } @catch (id exception) {
    {
      base::AutoLock lock(_lock);
      if (_frameReceiver) {
        _frameReceiver->OnPhotoError();
      }
    }
    [self takePhotoResolved];
  }
}

// Callback for the `_photoOutput` operation started in takePhotoInternal().
- (void)captureOutput:(id)output        // AVCapturePhotoOutput*
    didFinishProcessingPhoto:(id)photo  // AVCapturePhoto*
                       error:(NSError*)error {
  base::AutoLock lock(_lock);
  // If `output` is no longer current, ignore the result of this operation.
  // `_frameReceiver->OnPhotoError()` will already have been called inside
  // stopPhotoOutput().
  if (output != _photoOutput) {
    return;
  }
  if (_frameReceiver) {
    // Always non-nil according to Apple's documentation.
    DCHECK(photo);
    NSData* data = static_cast<AVCapturePhoto*>(photo).fileDataRepresentation;
    if (!error && data) {
      _frameReceiver->OnPhotoTaken(reinterpret_cast<const uint8_t*>(data.bytes),
                                   data.length, "image/jpeg");
    } else {
      _frameReceiver->OnPhotoError();
    }
  }
  // Whether we succeeded or failed, we need to resolve the pending
  // takePhoto() operation.
  _mainThreadTaskRunner->PostTask(
      FROM_HERE, base::BindOnce(
                     [](base::WeakPtr<SelfHolder> weakSelf) {
                       if (!weakSelf.get()) {
                         return;
                       }
                       [weakSelf.get()->the_self takePhotoResolved];
                     },
                     _weakPtrHolderForTakePhoto.weak_ptr_factory.GetWeakPtr()));
}

- (void)takePhotoResolved {
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
  --_pendingTakePhotos;
  if (_pendingTakePhotos > 0u) {
    // Take another photo.
    [self takePhotoInternal];
    return;
  }
  // All pending takePhoto()s have completed. If no more photos are taken
  // within 60 seconds, stop photo output to avoid expensive MJPEG conversions
  // going forward.
  _mainThreadTaskRunner->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(
          [](base::WeakPtr<SelfHolder> weakSelf) {
            if (!weakSelf.get()) {
              return;
            }
            [weakSelf.get()->the_self stopPhotoOutput];
          },
          _weakPtrHolderForTakePhoto.weak_ptr_factory.GetWeakPtr()),
      base::Seconds(kTimeToWaitBeforeStoppingPhotoOutputInSeconds));
}

- (void)stopPhotoOutput {
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
  // Already stopped?
  // Thread-safe because `_photoOutput` is only modified on the main thread.
  if (!_photoOutput) {
    return;
  }
  // Cancel all in-flight operations.
  _weakPtrHolderForTakePhoto.weak_ptr_factory.InvalidateWeakPtrs();
  {
    base::AutoLock lock(_lock);
    if (_captureSession) {
      [_captureSession removeOutput:_photoOutput];
    }
    // `_lock` is needed since `_photoOutput` may be read from non-main thread.
    _photoOutput = nil;
    // For every pending photo, report OnPhotoError().
    if (_pendingTakePhotos) {
      if (_frameReceiver) {
        for (size_t i = 0; i < _pendingTakePhotos; ++i) {
          _frameReceiver->OnPhotoError();
        }
      }
      _pendingTakePhotos = 0u;
    }
  }

  if (_onPhotoOutputStopped) {
    // Callback used by tests.
    _onPhotoOutputStopped.Run();
  }
}

- (void)processSample:(CMSampleBufferRef)sampleBuffer
         captureFormat:(const media::VideoCaptureFormat&)captureFormat
            colorSpace:(const gfx::ColorSpace&)colorSpace
             timestamp:(const base::TimeDelta)timestamp
    capture_begin_time:(std::optional<base::TimeTicks>)capture_begin_time {
  VLOG(3) << __func__;
  // Trust |_frameReceiver| to do decompression.
  char* baseAddress = nullptr;
  size_t frameSize = 0;
  _lock.AssertAcquired();
  DCHECK(_frameReceiver);
  const bool sample_buffer_addressable = media::ExtractBaseAddressAndLength(
      &baseAddress, &frameSize, sampleBuffer);
  DCHECK(sample_buffer_addressable);
  if (sample_buffer_addressable) {
    const bool safe_to_forward =
        captureFormat.pixel_format == media::PIXEL_FORMAT_MJPEG ||
        media::VideoFrame::AllocationSize(
            captureFormat.pixel_format, captureFormat.frame_size) <= frameSize;
    DCHECK(safe_to_forward);
    if (safe_to_forward) {
      _frameReceiver->ReceiveFrame(
          reinterpret_cast<const uint8_t*>(baseAddress), frameSize,
          captureFormat, colorSpace, 0, 0, timestamp, capture_begin_time,
          _rotation);
    }
  }
}

- (BOOL)processPixelBufferPlanes:(CVImageBufferRef)pixelBuffer
                   captureFormat:(const media::VideoCaptureFormat&)captureFormat
                      colorSpace:(const gfx::ColorSpace&)colorSpace
                       timestamp:(const base::TimeDelta)timestamp
              capture_begin_time:
                  (std::optional<base::TimeTicks>)capture_begin_time {
  VLOG(3) << __func__;
  if (CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly) !=
      kCVReturnSuccess) {
    return NO;
  }

  // Retrieve the layout of the planes of |pixelBuffer|.
  const size_t numPlanes =
      media::VideoFrame::NumPlanes(captureFormat.pixel_format);
  std::vector<uint8_t*> pixelBufferAddresses;
  std::vector<size_t> pixelBufferBytesPerRows;
  std::vector<size_t> pixelBufferHeights;
  if (!CVPixelBufferIsPlanar(pixelBuffer)) {
    // For nonplanar buffers, CVPixelBufferGetBaseAddress returns a pointer
    // to (0,0). (For planar buffers, it returns something else.)
    // https://developer.apple.com/documentation/corevideo/1457115-cvpixelbuffergetbaseaddress?language=objc
    CHECK_EQ(numPlanes, 1u);
    pixelBufferAddresses.push_back(
        static_cast<uint8_t*>(CVPixelBufferGetBaseAddress(pixelBuffer)));
    pixelBufferBytesPerRows.push_back(CVPixelBufferGetBytesPerRow(pixelBuffer));
    pixelBufferHeights.push_back(CVPixelBufferGetHeight(pixelBuffer));
  } else {
    // For planar buffers, CVPixelBufferGetBaseAddressOfPlane() is used. If
    // the buffer is contiguous (CHECK'd below) then we only need to know
    // the address of the first plane, regardless of
    // CVPixelBufferGetPlaneCount().
    CHECK_EQ(numPlanes, CVPixelBufferGetPlaneCount(pixelBuffer));
    for (size_t plane = 0; plane < numPlanes; ++plane) {
      pixelBufferAddresses.push_back(static_cast<uint8_t*>(
          CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, plane)));
      pixelBufferBytesPerRows.push_back(
          CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, plane));
      pixelBufferHeights.push_back(
          CVPixelBufferGetHeightOfPlane(pixelBuffer, plane));
    }
  }
  // CVPixelBufferGetDataSize() works for both nonplanar and planar buffers
  // as long as they are contiguous in memory. If it is not contiguous, 0 is
  // returned.
  size_t frameSize = CVPixelBufferGetDataSize(pixelBuffer);
  // Only contiguous buffers are supported.
  CHECK(frameSize);

  // Compute the tightly-packed layout for |captureFormat|.
  size_t packedBufferSize = 0;
  std::vector<size_t> packedBytesPerRows;
  std::vector<size_t> packedHeights;
  for (size_t plane = 0; plane < numPlanes; ++plane) {
    size_t bytesPerRow = media::VideoFrame::RowBytes(
        plane, captureFormat.pixel_format, captureFormat.frame_size.width());
    size_t height =
        media::VideoFrame::PlaneSize(captureFormat.pixel_format, plane,
                                     captureFormat.frame_size)
            .height();
    packedBytesPerRows.push_back(bytesPerRow);
    packedHeights.push_back(height);
    packedBufferSize += bytesPerRow * height;
  }

  // If media::VideoFrame::PlaneSize differs from the CVPixelBuffer's size then
  // generate a crash report to show the difference.
  // https://crbug.com/1168112
  CHECK_EQ(pixelBufferHeights.size(), packedHeights.size());
  for (size_t plane = 0; plane < pixelBufferHeights.size(); ++plane) {
    if (pixelBufferHeights[plane] != packedHeights[plane] &&
        !_hasDumpedForFrameSizeMismatch) {
      static crash_reporter::CrashKeyString<64> planeInfoKey(
          "core-video-plane-info");
      planeInfoKey.Set(
          base::StringPrintf("plane:%zu cv_height:%zu packed_height:%zu", plane,
                             pixelBufferHeights[plane], packedHeights[plane]));
      base::debug::DumpWithoutCrashing();
      _hasDumpedForFrameSizeMismatch = true;
    }
  }

  // If |pixelBuffer| is not tightly packed, then copy it to |packedBufferCopy|,
  // because ReceiveFrame() below assumes tight packing.
  // https://crbug.com/1151936
  bool needsCopyToPackedBuffer = pixelBufferBytesPerRows != packedBytesPerRows;
  std::vector<uint8_t> packedBufferCopy;
  if (needsCopyToPackedBuffer) {
    packedBufferCopy.resize(packedBufferSize, 0);
    uint8_t* dstAddr = packedBufferCopy.data();
    for (size_t plane = 0; plane < numPlanes; ++plane) {
      uint8_t* srcAddr = pixelBufferAddresses[plane];
      size_t row = 0;
      for (row = 0;
           row < std::min(packedHeights[plane], pixelBufferHeights[plane]);
           ++row) {
        memcpy(dstAddr, srcAddr,
               std::min(packedBytesPerRows[plane],
                        pixelBufferBytesPerRows[plane]));
        dstAddr += packedBytesPerRows[plane];
        srcAddr += pixelBufferBytesPerRows[plane];
      }
    }
  }

  _lock.AssertAcquired();
  DCHECK(_frameReceiver);
  _frameReceiver->ReceiveFrame(packedBufferCopy.empty()
                                   ? pixelBufferAddresses[0]
                                   : packedBufferCopy.data(),
                               frameSize, captureFormat, colorSpace, 0, 0,
                               timestamp, capture_begin_time, _rotation);
  CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
  return YES;
}

- (void)processPixelBufferNV12IOSurface:(CVPixelBufferRef)pixelBuffer
                          captureFormat:
                              (const media::VideoCaptureFormat&)captureFormat
                             colorSpace:(const gfx::ColorSpace&)colorSpace
                              timestamp:(const base::TimeDelta)timestamp
                     capture_begin_time:
                         (std::optional<base::TimeTicks>)capture_begin_time {
  VLOG(3) << __func__;
  DCHECK_EQ(captureFormat.pixel_format, media::PIXEL_FORMAT_NV12);

  IOSurfaceRef ioSurface = CVPixelBufferGetIOSurface(pixelBuffer);
  DCHECK(ioSurface);
  media::CapturedExternalVideoBuffer externalBuffer =
      [self capturedExternalVideoBufferFromNV12IOSurface:ioSurface
                                           captureFormat:captureFormat
                                              colorSpace:colorSpace];

  // The lock is needed for |_frameReceiver|.
  _lock.AssertAcquired();
  DCHECK(_frameReceiver);
  _frameReceiver->ReceiveExternalGpuMemoryBufferFrame(
      std::move(externalBuffer), timestamp, capture_begin_time);
}

- (media::CapturedExternalVideoBuffer)
    capturedExternalVideoBufferFromNV12IOSurface:(IOSurfaceRef)ioSurface
                                   captureFormat:
                                       (const media::VideoCaptureFormat&)
                                           captureFormat
                                      colorSpace:
                                          (const gfx::ColorSpace&)colorSpace {
  DCHECK(ioSurface);
  gfx::GpuMemoryBufferHandle handle;
  handle.id = gfx::GpuMemoryBufferHandle::kInvalidId;
  handle.type = gfx::GpuMemoryBufferType::IO_SURFACE_BUFFER;
  handle.io_surface.reset(ioSurface, base::scoped_policy::RETAIN);

  // The BT709_APPLE color space is stored as an ICC profile, which is parsed
  // every frame in the GPU process. For this particularly common case, go back
  // to ignoring the color profile, because doing so avoids doing an ICC profile
  // parse.
  // https://crbug.com/1143477 (CPU usage parsing ICC profile)
  // https://crbug.com/959962 (ignoring color space)
  gfx::ColorSpace overriddenColorSpace = colorSpace;
  if (colorSpace == kColorSpaceRec709Apple &&
      base::FeatureList::IsEnabled(media::kOverrideCameraIOSurfaceColorSpace)) {
    overriddenColorSpace = gfx::ColorSpace(
        gfx::ColorSpace::PrimaryID::BT709, gfx::ColorSpace::TransferID::SRGB,
        gfx::ColorSpace::MatrixID::BT709, gfx::ColorSpace::RangeID::LIMITED);
    IOSurfaceSetValue(ioSurface, CFSTR("IOSurfaceColorSpace"),
                      kCGColorSpaceSRGB);
  }

  return media::CapturedExternalVideoBuffer(std::move(handle), captureFormat,
                                            overriddenColorSpace);
}

// Sometimes (especially when the camera is accessed by another process, e.g,
// Photo Booth), the AVCaptureSession will stop producing new frames. This check
// happens with no errors or notifications being produced. To recover from this,
// check to see if a new frame has been captured second. If 5 of these checks
// fail consecutively, restart the capture session.
// https://crbug.com/1176568
- (void)doStallCheck:(int)failedCheckCount {
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());

  int nextFailedCheckCount = failedCheckCount + 1;
  {
    base::AutoLock lock(_lock);
    // This is to detect a capture was working, but stopped submitting new
    // frames. If we haven't received any frames yet, don't do anything.
    if (!_capturedFirstFrame) {
      nextFailedCheckCount = 0;
    }

    // If we captured a frame since last check, then we aren't stalled.
    // We're also not considered stalled if takePhoto() is pending, avoiding
    // excessive capture restarts in unit tests with mock time.
    if (_capturedFrameSinceLastStallCheck || _pendingTakePhotos) {
      nextFailedCheckCount = 0;
    }
    _capturedFrameSinceLastStallCheck = NO;
  }

  constexpr int kMaxFailedCheckCount = 5;
  if (nextFailedCheckCount < kMaxFailedCheckCount) {
    // Post a task to check for progress in 1 second. Create the weak factory
    // for the posted task, if needed.
    constexpr base::TimeDelta kStallCheckInterval = base::Seconds(1);
    auto callbackLambda = [](base::WeakPtr<SelfHolder> weakSelf,
                             int failedCheckCount) {
      if (!weakSelf.get()) {
        return;
      }
      [weakSelf.get()->the_self doStallCheck:failedCheckCount];
    };
    _mainThreadTaskRunner->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(
            callbackLambda,
            _weakPtrHolderForStallCheck.weak_ptr_factory.GetWeakPtr(),
            nextFailedCheckCount),
        kStallCheckInterval);
  } else {
    if (ShouldRestartStalledCamera()) {
      [self logMessage:"Capture appears to have stalled, restarting."];
      [self stopCapture];
      [self startCapture];
    } else {
      [self logMessage:
                "Capture appears to have stalled, restarting may have helped "
                "but is disabled. See https://issues.chromium.org/335210401."];
    }
  }
}

// |captureOutput| is called by the capture device to deliver a new frame.
// Since the callback is configured to happen on a global dispatch queue, calls
// may enter here concurrently and on any thread.
- (void)captureOutput:(AVCaptureOutput*)captureOutput
    didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
           fromConnection:(AVCaptureConnection*)connection {
  VLOG(3) << __func__;

  // Concurrent calls into |_frameReceiver| are not supported, so take |_lock|
  // before any of the subsequent paths. The |_destructionLock| must be grabbed
  // first to avoid races with -dealloc.
  base::AutoLock destructionLock(_destructionLock);
  base::AutoLock lock(_lock);
  _capturedFrameSinceLastStallCheck = YES;
  if (!_frameReceiver || !_sampleBufferTransformer) {
    VLOG(1) << "dropping frame due to no receiver";
    return;
  }
  auto capture_begin_time = GetCMSampleBufferTimestamp(sampleBuffer);
  const base::TimeTicks pres_timestamp =
      capture_begin_time.value_or(base::TimeTicks());
  if (_startTimestamp.is_null()) {
    _startTimestamp = pres_timestamp;
  }
  const base::TimeDelta timestamp = pres_timestamp - _startTimestamp;
#if BUILDFLAG(IS_MAC)
  bool logUma = !std::exchange(_capturedFirstFrame, true);
  if (logUma) {
    [self logMessageLocked:"First frame received for this capturer instance"];
    media::LogFirstCapturedVideoFrame(_bestCaptureFormat, sampleBuffer);
  }
#endif
  // Forget the sample timestamp if we're out of the experiment.
  if (!base::FeatureList::IsEnabled(
          kAVFoundationCaptureForwardSampleTimestamps)) {
    capture_begin_time = std::nullopt;
  }

  // The SampleBufferTransformer CHECK-crashes if the sample buffer is not MJPEG
  // and does not have a pixel buffer (https://crbug.com/1160647) so we fall
  // back on the M87 code path if this is the case.
  // TODO(crbug.com/40162135): When the SampleBufferTransformer is
  // patched to support non-MJPEG-and-non-pixel-buffer sample buffers, remove
  // this workaround and the fallback other code path.
  bool sampleHasPixelBufferOrIsMjpeg =
      CMSampleBufferGetImageBuffer(sampleBuffer) ||
      CMFormatDescriptionGetMediaSubType(CMSampleBufferGetFormatDescription(
          sampleBuffer)) == kCMVideoCodecType_JPEG_OpenDML;

  // If the SampleBufferTransformer is enabled, convert all possible capture
  // formats to an IOSurface-backed NV12 pixel buffer.
  // TODO(crbug.com/40747183): Refactor to not hijack the code paths
  // below the transformer code.
  if (_useGPUMemoryBuffer && sampleHasPixelBufferOrIsMjpeg) {
    _sampleBufferTransformer->Reconfigure(
        media::SampleBufferTransformer::GetBestTransformerForNv12Output(
            sampleBuffer),
        kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
        media::GetSampleBufferSize(sampleBuffer), _rotation,
        kPixelBufferPoolSize);
    base::apple::ScopedCFTypeRef<CVPixelBufferRef> pixelBuffer =
        _sampleBufferTransformer->Transform(sampleBuffer);
    if (!pixelBuffer) {
      [self logMessageLocked:
                "Failed to transform captured frame. Dropping frame."];
      return;
    }

#if BUILDFLAG(IS_MAC)
    base::apple::ScopedCFTypeRef<CVPixelBufferRef> final_pixel_buffer =
        pixelBuffer;
#else
    // The rotated_pixelBuffer might not be the same size as the source
    // pixelBuffer as it gets rotated by rotation_angle_. In order to restore
    // the original size, rotated_pixelBuffer need to scale it to its original
    // size by transforming it.
    base::apple::ScopedCFTypeRef<CVPixelBufferRef> rotated_pixelBuffer =
        _sampleBufferTransformer->Rotate(pixelBuffer.get());
    base::apple::ScopedCFTypeRef<CVPixelBufferRef> final_pixel_buffer =
        _sampleBufferTransformer->Transform(rotated_pixelBuffer.get());

#endif

    const media::VideoCaptureFormat captureFormat(
        gfx::Size(CVPixelBufferGetWidth(final_pixel_buffer.get()),
                  CVPixelBufferGetHeight(final_pixel_buffer.get())),
        _frameRate, media::PIXEL_FORMAT_NV12);
    // When the |pixelBuffer| is the result of a conversion (not camera
    // pass-through) then it originates from a CVPixelBufferPool and the color
    // space is not recognized by media::GetImageBufferColorSpace(). This
    // results in log spam and a default color space format is returned. To
    // avoid this, we pretend the color space is kColorSpaceRec709Apple which
    // triggers a path that avoids color space parsing inside of
    // processPixelBufferNV12IOSurface.
    // TODO(hbos): Investigate how to successfully parse and/or configure the
    // color space correctly. The implications of this hack is not fully
    // understood.
    [self processPixelBufferNV12IOSurface:final_pixel_buffer.get()
                            captureFormat:captureFormat
                               colorSpace:kColorSpaceRec709Apple
                                timestamp:timestamp
                       capture_begin_time:capture_begin_time];
    return;
  }

  // We have certain format expectation for capture output:
  // For MJPEG, |sampleBuffer| is expected to always be a CVBlockBuffer.
  // For other formats, |sampleBuffer| may be either CVBlockBuffer or
  // CVImageBuffer. CVBlockBuffer seems to be used in the context of CoreMedia
  // plugins/virtual cameras. In order to find out whether it is CVBlockBuffer
  // or CVImageBuffer we call CMSampleBufferGetImageBuffer() and check if the
  // return value is nil.
  const CMFormatDescriptionRef formatDescription =
      CMSampleBufferGetFormatDescription(sampleBuffer);
  const CMVideoDimensions dimensions =
      CMVideoFormatDescriptionGetDimensions(formatDescription);
  OSType sampleBufferPixelFormat =
      CMFormatDescriptionGetMediaSubType(formatDescription);
  media::VideoPixelFormat videoPixelFormat = [VideoCaptureDeviceAVFoundation
      FourCCToChromiumPixelFormat:sampleBufferPixelFormat];

  const media::VideoCaptureFormat captureFormat(
      gfx::Size(dimensions.width, dimensions.height), _frameRate,
      videoPixelFormat);

  if (CVPixelBufferRef pixelBuffer =
          CMSampleBufferGetImageBuffer(sampleBuffer)) {
    const gfx::ColorSpace colorSpace =
        media::GetImageBufferColorSpace(pixelBuffer);
    OSType pixelBufferPixelFormat =
        CVPixelBufferGetPixelFormatType(pixelBuffer);
    DCHECK_EQ(pixelBufferPixelFormat, sampleBufferPixelFormat);

    // First preference is to use an NV12 IOSurface as a GpuMemoryBuffer.
    if (_useGPUMemoryBuffer) {
      if (CVPixelBufferGetIOSurface(pixelBuffer) &&
          videoPixelFormat == media::PIXEL_FORMAT_NV12) {
        [self processPixelBufferNV12IOSurface:pixelBuffer
                                captureFormat:captureFormat
                                   colorSpace:colorSpace
                                    timestamp:timestamp
                           capture_begin_time:capture_begin_time];
        return;
      }
    }
    // Second preference is to read the CVPixelBuffer's planes.
    if ([self processPixelBufferPlanes:pixelBuffer
                         captureFormat:captureFormat
                            colorSpace:colorSpace
                             timestamp:timestamp
                    capture_begin_time:capture_begin_time]) {
      return;
    }
  }

  // Last preference is to read the CMSampleBuffer.
  gfx::ColorSpace colorSpace =
      media::GetFormatDescriptionColorSpace(formatDescription);
  [self processSample:sampleBuffer
           captureFormat:captureFormat
              colorSpace:colorSpace
               timestamp:timestamp
      capture_begin_time:capture_begin_time];
}

- (void)setIsPortraitEffectSupportedForTesting:
    (bool)isPortraitEffectSupportedForTesting {
  _isPortraitEffectSupportedForTesting = isPortraitEffectSupportedForTesting;
}

- (bool)isPortraitEffectSupported {
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
  if (_isPortraitEffectSupportedForTesting.has_value()) {
    return _isPortraitEffectSupportedForTesting.value();
  }
  if (@available(macOS 12.0, *)) {
    return _captureDevice.activeFormat.portraitEffectSupported;
  }
  return false;
}

- (void)setIsPortraitEffectActiveForTesting:
    (bool)isPortraitEffectActiveForTesting {
  if (_isPortraitEffectActiveForTesting.has_value() &&
      _isPortraitEffectActiveForTesting == isPortraitEffectActiveForTesting) {
    return;
  }
  _isPortraitEffectActiveForTesting = isPortraitEffectActiveForTesting;
  [self captureConfigurationChanged];
}

- (bool)isPortraitEffectActive {
  DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
  if (_isPortraitEffectActiveForTesting.has_value()) {
    return _isPortraitEffectActiveForTesting.value();
  }
  if (@available(macOS 12.0, *)) {
    return _captureDevice.portraitEffectActive;
  }
  return false;
}

- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context {
  if (@available(macOS 12.0, *)) {
    if ([keyPath isEqual:@"portraitEffectActive"]) {
      [self captureConfigurationChanged];
    }
  }
}

- (void)captureConfigurationChanged {
  base::AutoLock lock(_lock);
  if (_frameReceiver) {
    _frameReceiver->ReceiveCaptureConfigurationChanged();
  }
}

- (void)onVideoError:(NSNotification*)errorNotification {
  NSError* error = base::apple::ObjCCast<NSError>(
      errorNotification.userInfo[AVCaptureSessionErrorKey]);
  [self
      sendErrorString:[NSString stringWithFormat:@"%@: %@",
                                                 error.localizedDescription,
                                                 error.localizedFailureReason]];
}

- (void)sendErrorString:(NSString*)error {
  auto message = base::SysNSStringToUTF8(error);
  VLOG(1) << __func__ << " message " << message;
  base::AutoLock lock(_lock);
  if (_frameReceiver) {
    _frameReceiver->ReceiveError(
        media::VideoCaptureError::
            kMacAvFoundationReceivedAVCaptureSessionRuntimeErrorNotification,
        FROM_HERE, message);
  }
}

- (void)callLocked:(base::OnceClosure)lambda {
  base::AutoLock lock(_lock);
  std::move(lambda).Run();
}

@end