blob: 7a0e50688272c5d5648fad5f872eb4f0aa425127 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "media/capture/video/mac/video_capture_device_avfoundation_mac.h"
#import <AVFoundation/AVFoundation.h>
#import <CoreMedia/CoreMedia.h>
#import <CoreVideo/CoreVideo.h>
#include <stddef.h>
#include <stdint.h>
#include <sstream>
#include "base/debug/dump_without_crashing.h"
#include "base/location.h"
#include "base/mac/foundation_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "components/crash/core/common/crash_key.h"
#include "media/base/mac/color_space_util_mac.h"
#include "media/base/media_switches.h"
#include "media/base/timestamp_constants.h"
#include "media/base/video_types.h"
#import "media/capture/video/mac/video_capture_device_avfoundation_utils_mac.h"
#include "media/capture/video/mac/video_capture_device_factory_mac.h"
#include "media/capture/video/mac/video_capture_device_mac.h"
#import "media/capture/video/mac/video_capture_metrics_mac.h"
#include "media/capture/video_capture_types.h"
#include "services/video_capture/public/uma/video_capture_service_event.h"
#include "ui/gfx/geometry/size.h"
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 kTimeToWaitBeforeStoppingStillImageCaptureInSeconds = 60;
constexpr FourCharCode kDefaultFourCCPixelFormat =
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; // NV12 (a.k.a. 420v)
base::TimeDelta GetCMSampleBufferTimestamp(CMSampleBufferRef sampleBuffer) {
const CMTime cm_timestamp =
CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
const base::TimeDelta timestamp =
CMTIME_IS_VALID(cm_timestamp)
? base::Seconds(CMTimeGetSeconds(cm_timestamp))
: media::kNoTimestamp;
return timestamp;
}
constexpr size_t kPixelBufferPoolSize = 10;
} // anonymous namespace
namespace media {
const base::Feature kInCapturerScaling{"InCapturerScaling",
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]);
Float64 maxFrameRate = 0;
bool matchesFrameRate = false;
for (AVFrameRateRange* frameRateRange in
[captureFormat videoSupportedFrameRateRanges]) {
maxFrameRate = std::max(maxFrameRate, [frameRateRange maxFrameRate]);
matchesFrameRate |= [frameRateRange minFrameRate] <= frame_rate &&
frame_rate <= [frameRateRange maxFrameRate];
}
// 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;
// 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
#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::ThreadTaskRunnerHandle::Get();
_sampleQueue.reset(
dispatch_queue_create("org.chromium.VideoCaptureDeviceAVFoundation."
"SampleDeliveryDispatchQueue",
DISPATCH_QUEUE_SERIAL),
base::scoped_policy::ASSUME);
DCHECK(frameReceiver);
_capturedFirstFrame = false;
_weakPtrFactoryForTakePhoto =
std::make_unique<base::WeakPtrFactory<VideoCaptureDeviceAVFoundation>>(
self);
[self setFrameReceiver:frameReceiver];
_captureSession.reset([[AVCaptureSession alloc] init]);
_sampleBufferTransformer = media::SampleBufferTransformer::Create();
}
return self;
}
- (void)dealloc {
{
// To avoid races with concurrent callbacks, grab the lock before stopping
// capture and clearing all the variables.
base::AutoLock lock(_lock);
[self stopStillImageOutput];
[self stopCapture];
_frameReceiver = nullptr;
_sampleBufferTransformer.reset();
_weakPtrFactoryForTakePhoto = nullptr;
_mainThreadTaskRunner = nullptr;
_sampleQueue.reset();
}
{
// 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);
}
[super dealloc];
}
- (void)setFrameReceiver:
(media::VideoCaptureDeviceAVFoundationFrameReceiver*)frameReceiver {
base::AutoLock lock(_lock);
_frameReceiver = frameReceiver;
}
- (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 stopStillImageOutput];
if (_captureDeviceInput) {
DCHECK(_captureDevice);
[_captureSession stopRunning];
[_captureSession removeInput:_captureDeviceInput];
_captureDeviceInput.reset();
_captureDevice.reset();
}
return YES;
}
// Look for input device with requested name.
_captureDevice.reset([AVCaptureDevice deviceWithUniqueID:deviceId],
base::scoped_policy::RETAIN);
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.reset(
[AVCaptureDeviceInput deviceInputWithDevice:_captureDevice error:&error],
base::scoped_policy::RETAIN);
if (!_captureDeviceInput) {
_captureDevice.reset();
*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.reset([[AVCaptureVideoDataOutput alloc] init]);
if (!_captureVideoDataOutput) {
[_captureSession removeInput:_captureDeviceInput];
*outMessage = @"Could not create video data output.";
return NO;
}
[_captureVideoDataOutput setAlwaysDiscardsLateVideoFrames:true];
[_captureVideoDataOutput setSampleBufferDelegate:self queue:_sampleQueue];
[_captureSession addOutput:_captureVideoDataOutput];
return YES;
}
- (BOOL)setCaptureHeight:(int)height
width:(int)width
frameRate:(float)frameRate {
DCHECK(![_captureSession isRunning]);
DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
_frameWidth = width;
_frameHeight = height;
_frameRate = frameRate;
_bestCaptureFormat.reset(
media::FindBestCaptureFormat([_captureDevice formats], width, height,
frameRate),
base::scoped_policy::RETAIN);
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(crbugs.com/1124884): 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 MacOsX. AVVideoScalingModeKey() refers to letterboxing
// yes/no and preserve aspect ratio yes/no when scaling. Currently we set
// cropping and preservation.
NSDictionary* videoSettingsDictionary = @{
(id)kCVPixelBufferWidthKey : @(width),
(id)kCVPixelBufferHeightKey : @(height),
(id)kCVPixelBufferPixelFormatTypeKey : @(best_fourcc),
AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill
};
[_captureVideoDataOutput setVideoSettings:videoSettingsDictionary];
AVCaptureConnection* captureConnection =
[_captureVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
// Check selector existence, related to bugs http://crbug.com/327532 and
// http://crbug.com/328096.
// CMTimeMake accepts integer argumenst but |frameRate| is float, round it.
if ([captureConnection
respondsToSelector:@selector(isVideoMinFrameDurationSupported)] &&
[captureConnection isVideoMinFrameDurationSupported]) {
[captureConnection
setVideoMinFrameDuration:CMTimeMake(media::kFrameRatePrecision,
(int)(frameRate *
media::kFrameRatePrecision))];
}
if ([captureConnection
respondsToSelector:@selector(isVideoMaxFrameDurationSupported)] &&
[captureConnection isVideoMaxFrameDurationSupported]) {
[captureConnection
setVideoMaxFrameDuration:CMTimeMake(media::kFrameRatePrecision,
(int)(frameRate *
media::kFrameRatePrecision))];
}
return YES;
}
- (void)setScaledResolutions:(std::vector<gfx::Size>)resolutions {
if (!base::FeatureList::IsEnabled(media::kInCapturerScaling)) {
return;
}
// The lock is needed for |_scaledFrameTransformers|.
base::AutoLock lock(_lock);
bool reconfigureScaledFrameTransformers = false;
if (resolutions.size() != _scaledFrameTransformers.size()) {
reconfigureScaledFrameTransformers = true;
} else {
for (const auto& resolution : resolutions) {
bool resolutionHasTransformer = false;
for (const auto& scaledFrameTransformer : _scaledFrameTransformers) {
if (resolution == scaledFrameTransformer->destination_size()) {
resolutionHasTransformer = true;
break;
}
}
if (!resolutionHasTransformer) {
reconfigureScaledFrameTransformers = true;
break;
}
}
}
if (!reconfigureScaledFrameTransformers)
return;
std::stringstream str;
str << "[";
for (size_t i = 0; i < resolutions.size(); ++i) {
if (i != 0)
str << ", ";
str << resolutions[i].ToString();
}
str << "]";
VLOG(1) << "Configuring scaled resolutions: " << str.str();
_scaledFrameTransformers.clear();
for (size_t i = 0; i < resolutions.size(); ++i) {
DCHECK(i == 0 || resolutions[i - 1].height() >= resolutions[i].height());
// Configure the transformer to and from NV12 pixel buffers - we only want
// to pay scaling costs, not conversion costs.
auto scaledFrameTransformer = media::SampleBufferTransformer::Create();
scaledFrameTransformer->Reconfigure(
media::SampleBufferTransformer::
kBestTransformerForPixelBufferToNv12Output,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, resolutions[i],
kPixelBufferPoolSize);
_scaledFrameTransformers.push_back(std::move(scaledFrameTransformer));
}
}
- (BOOL)startCapture {
DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
if (!_captureSession) {
DLOG(ERROR) << "Video capture session not initialized.";
return NO;
}
// Connect the notifications.
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:@selector(onVideoError:)
name:AVCaptureSessionRuntimeErrorNotification
object:_captureSession];
[_captureSession startRunning];
// Update the active capture format once the capture session is running.
// Setting it before the capture session is running has no effect.
if (_bestCaptureFormat) {
if ([_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());
_weakPtrFactoryForStallCheck.reset();
[self stopStillImageOutput];
if ([_captureSession isRunning])
[_captureSession stopRunning]; // Synchronous.
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)takePhoto {
DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
DCHECK([_captureSession isRunning]);
++_takePhotoStartedCount;
// Ready to take a photo immediately?
if (_stillImageOutput && _stillImageOutputWarmupCompleted) {
[self takePhotoInternal];
return;
}
// Lazily instantiate the |_stillImageOutput| the first time takePhoto() is
// called. When takePhoto() isn't called, this avoids JPEG compession work for
// every frame. This can save a lot of CPU in some cases (see
// https://crbug.com/1116241). However because it can take a couple of second
// for the 3A to stabilize, lazily instantiating like may result in noticeable
// delays. To avoid delays in future takePhoto() calls we don't delete
// |_stillImageOutput| until takePhoto() has not been called for 60 seconds.
if (!_stillImageOutput) {
// We use AVCaptureStillImageOutput for historical reasons, but note that it
// has been deprecated in macOS 10.15[1] in favor of
// AVCapturePhotoOutput[2].
//
// [1]
// https://developer.apple.com/documentation/avfoundation/avcapturestillimageoutput
// [2]
// https://developer.apple.com/documentation/avfoundation/avcapturephotooutput
// TODO(https://crbug.com/1124322): Migrate to the new API.
_stillImageOutput.reset([[AVCaptureStillImageOutput alloc] init]);
if (!_stillImageOutput ||
![_captureSession canAddOutput:_stillImageOutput]) {
// Complete this started photo as error.
++_takePhotoPendingCount;
{
base::AutoLock lock(_lock);
if (_frameReceiver) {
_frameReceiver->OnPhotoError();
}
}
[self takePhotoCompleted];
return;
}
[_captureSession addOutput:_stillImageOutput];
// 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<VideoCaptureDeviceAVFoundation> weakSelf) {
[weakSelf.get() takePhotoInternal];
},
_weakPtrFactoryForTakePhoto->GetWeakPtr()),
base::Seconds(3));
}
}
- (void)setOnStillImageOutputStoppedForTesting:
(base::RepeatingCallback<void()>)onStillImageOutputStopped {
DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
_onStillImageOutputStopped = onStillImageOutputStopped;
}
#pragma mark Private methods
- (void)takePhotoInternal {
DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
// stopStillImageOutput invalidates all weak ptrs, meaning in-flight
// operations are affectively cancelled. So if this method is running, still
// image output must be good to go.
DCHECK([_captureSession isRunning]);
DCHECK(_stillImageOutput);
DCHECK([[_stillImageOutput connections] count] == 1);
AVCaptureConnection* const connection =
[[_stillImageOutput connections] firstObject];
DCHECK(connection);
_stillImageOutputWarmupCompleted = true;
// For all photos started that are not yet pending, take photos.
while (_takePhotoPendingCount < _takePhotoStartedCount) {
++_takePhotoPendingCount;
const auto handler = ^(CMSampleBufferRef sampleBuffer, NSError* error) {
{
base::AutoLock lock(_lock);
if (_frameReceiver) {
if (error != nil) {
_frameReceiver->OnPhotoError();
} else {
// Recommended compressed pixel format is JPEG, we don't expect
// surprises.
// TODO(mcasas): Consider using [1] for merging EXIF output
// information:
// [1]
// +(NSData*)jpegStillImageNSDataRepresentation:jpegSampleBuffer;
DCHECK_EQ(kCMVideoCodecType_JPEG,
CMFormatDescriptionGetMediaSubType(
CMSampleBufferGetFormatDescription(sampleBuffer)));
char* baseAddress = 0;
size_t length = 0;
media::ExtractBaseAddressAndLength(&baseAddress, &length,
sampleBuffer);
_frameReceiver->OnPhotoTaken(
reinterpret_cast<uint8_t*>(baseAddress), length, "image/jpeg");
}
}
}
// Called both on success and failure.
_mainThreadTaskRunner->PostTask(
FROM_HERE,
base::BindOnce(
[](base::WeakPtr<VideoCaptureDeviceAVFoundation> weakSelf) {
[weakSelf.get() takePhotoCompleted];
},
_weakPtrFactoryForTakePhoto->GetWeakPtr()));
};
[_stillImageOutput captureStillImageAsynchronouslyFromConnection:connection
completionHandler:handler];
}
}
- (void)takePhotoCompleted {
DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
++_takePhotoCompletedCount;
if (_takePhotoStartedCount != _takePhotoCompletedCount)
return;
// All pending takePhoto()s have completed. If no more photos are taken
// within 60 seconds, stop still image output to avoid expensive MJPEG
// conversions going forward.
_mainThreadTaskRunner->PostDelayedTask(
FROM_HERE,
base::BindOnce(
[](base::WeakPtr<VideoCaptureDeviceAVFoundation> weakSelf,
size_t takePhotoCount) {
VideoCaptureDeviceAVFoundation* strongSelf = weakSelf.get();
if (!strongSelf)
return;
// Don't stop the still image output if takePhoto() was called
// while the task was pending.
if (strongSelf->_takePhotoStartedCount != takePhotoCount)
return;
[strongSelf stopStillImageOutput];
},
_weakPtrFactoryForTakePhoto->GetWeakPtr(), _takePhotoStartedCount),
base::Seconds(kTimeToWaitBeforeStoppingStillImageCaptureInSeconds));
}
- (void)stopStillImageOutput {
DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
if (!_stillImageOutput) {
// Already stopped.
return;
}
if (_captureSession) {
[_captureSession removeOutput:_stillImageOutput];
}
_stillImageOutput.reset();
_stillImageOutputWarmupCompleted = false;
// Cancel all in-flight operations.
_weakPtrFactoryForTakePhoto->InvalidateWeakPtrs();
// Report error for all pending calls that were stopped.
size_t pendingCalls = _takePhotoStartedCount - _takePhotoCompletedCount;
_takePhotoCompletedCount = _takePhotoPendingCount = _takePhotoStartedCount;
{
base::AutoLock lock(_lock);
if (_frameReceiver) {
for (size_t i = 0; i < pendingCalls; ++i) {
_frameReceiver->OnPhotoError();
}
}
}
if (_onStillImageOutputStopped) {
// Callback used by tests.
_onStillImageOutputStopped.Run();
}
}
- (void)processSample:(CMSampleBufferRef)sampleBuffer
captureFormat:(const media::VideoCaptureFormat&)captureFormat
colorSpace:(const gfx::ColorSpace&)colorSpace
timestamp:(const base::TimeDelta)timestamp {
VLOG(3) << __func__;
// Trust |_frameReceiver| to do decompression.
char* baseAddress = 0;
size_t frameSize = 0;
media::ExtractBaseAddressAndLength(&baseAddress, &frameSize, sampleBuffer);
_lock.AssertAcquired();
_frameReceiver->ReceiveFrame(reinterpret_cast<const uint8_t*>(baseAddress),
frameSize, captureFormat, colorSpace, 0, 0,
timestamp);
}
- (BOOL)processPixelBufferPlanes:(CVImageBufferRef)pixelBuffer
captureFormat:(const media::VideoCaptureFormat&)captureFormat
colorSpace:(const gfx::ColorSpace&)colorSpace
timestamp:(const base::TimeDelta)timestamp {
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();
_frameReceiver->ReceiveFrame(
packedBufferCopy.empty() ? pixelBufferAddresses[0]
: packedBufferCopy.data(),
frameSize, captureFormat, colorSpace, 0, 0, timestamp);
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 {
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 |_scaledFrameTransformers| and |_frameReceiver|.
_lock.AssertAcquired();
// References to any scaled pixel buffers need to be retained until after
// ReceiveExternalGpuMemoryBufferFrame().
std::vector<base::ScopedCFTypeRef<CVPixelBufferRef>> scaledPixelBuffers;
std::vector<media::CapturedExternalVideoBuffer> scaledExternalBuffers;
scaledPixelBuffers.reserve(_scaledFrameTransformers.size());
scaledExternalBuffers.reserve(_scaledFrameTransformers.size());
for (auto& scaledFrameTransformer : _scaledFrameTransformers) {
gfx::Size scaledFrameSize = scaledFrameTransformer->destination_size();
// Only proceed if this results in downscaling in one or both dimensions.
//
// It is not clear that we want to continue to allow changing the aspect
// ratio like this since this causes visible stretching in the image if the
// stretch is significantly large.
// TODO(https://crbug.com/1157072): When we know what to do about aspect
// ratios, consider adding a DCHECK here or otherwise ignore wrong aspect
// ratios (within some fault tolerance).
if (scaledFrameSize.width() > captureFormat.frame_size.width() ||
scaledFrameSize.height() > captureFormat.frame_size.height() ||
scaledFrameSize == captureFormat.frame_size) {
continue;
}
CVPixelBufferRef bufferToScale =
!scaledPixelBuffers.empty() ? scaledPixelBuffers.back() : pixelBuffer;
base::ScopedCFTypeRef<CVPixelBufferRef> scaledPixelBuffer =
scaledFrameTransformer->Transform(bufferToScale);
if (!scaledPixelBuffer) {
LOG(ERROR) << "Failed to downscale frame, skipping resolution "
<< scaledFrameSize.ToString();
continue;
}
scaledPixelBuffers.push_back(scaledPixelBuffer);
IOSurfaceRef scaledIoSurface = CVPixelBufferGetIOSurface(scaledPixelBuffer);
media::VideoCaptureFormat scaledCaptureFormat = captureFormat;
scaledCaptureFormat.frame_size = scaledFrameSize;
scaledExternalBuffers.push_back([self
capturedExternalVideoBufferFromNV12IOSurface:scaledIoSurface
captureFormat:scaledCaptureFormat
colorSpace:colorSpace]);
}
_frameReceiver->ReceiveExternalGpuMemoryBufferFrame(
std::move(externalBuffer), std::move(scaledExternalBuffers), timestamp);
}
- (media::CapturedExternalVideoBuffer)
capturedExternalVideoBufferFromNV12IOSurface:(IOSurfaceRef)ioSurface
captureFormat:
(const media::VideoCaptureFormat&)
captureFormat
colorSpace:
(const gfx::ColorSpace&)colorSpace {
DCHECK(ioSurface);
gfx::GpuMemoryBufferHandle handle;
handle.id.id = -1;
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) {
overriddenColorSpace = gfx::ColorSpace(
gfx::ColorSpace::PrimaryID::BT709,
gfx::ColorSpace::TransferID::IEC61966_2_1,
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.
if (_capturedFrameSinceLastStallCheck)
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.
if (!_weakPtrFactoryForStallCheck) {
_weakPtrFactoryForStallCheck = std::make_unique<
base::WeakPtrFactory<VideoCaptureDeviceAVFoundation>>(self);
}
constexpr base::TimeDelta kStallCheckInterval = base::Seconds(1);
auto callback_lambda =
[](base::WeakPtr<VideoCaptureDeviceAVFoundation> weakSelf,
int failedCheckCount) {
VideoCaptureDeviceAVFoundation* strongSelf = weakSelf.get();
if (!strongSelf)
return;
[strongSelf doStallCheck:failedCheckCount];
};
_mainThreadTaskRunner->PostDelayedTask(
FROM_HERE,
base::BindOnce(callback_lambda,
_weakPtrFactoryForStallCheck->GetWeakPtr(),
nextFailedCheckCount),
kStallCheckInterval);
} else {
// Capture appears to be stalled. Restart it.
LOG(ERROR) << "Capture appears to have stalled, restarting.";
[self stopCapture];
[self startCapture];
}
}
// |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)
return;
const base::TimeDelta timestamp = GetCMSampleBufferTimestamp(sampleBuffer);
bool logUma = !std::exchange(_capturedFirstFrame, true);
if (logUma) {
media::LogFirstCapturedVideoFrame(_bestCaptureFormat, sampleBuffer);
}
// 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(https://crbug.com/1160315): 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(https://crbug.com/1175142): Refactor to not hijack the code paths
// below the transformer code.
if (sampleHasPixelBufferOrIsMjpeg) {
_sampleBufferTransformer->Reconfigure(
media::SampleBufferTransformer::GetBestTransformerForNv12Output(
sampleBuffer),
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
media::GetSampleBufferSize(sampleBuffer), kPixelBufferPoolSize);
base::ScopedCFTypeRef<CVPixelBufferRef> pixelBuffer =
_sampleBufferTransformer->Transform(sampleBuffer);
if (!pixelBuffer) {
LOG(ERROR) << "Failed to transform captured frame. Dropping frame.";
return;
}
const media::VideoCaptureFormat captureFormat(
gfx::Size(CVPixelBufferGetWidth(pixelBuffer),
CVPixelBufferGetHeight(pixelBuffer)),
_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:pixelBuffer
captureFormat:captureFormat
colorSpace:kColorSpaceRec709Apple
timestamp:timestamp];
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 (CVPixelBufferGetIOSurface(pixelBuffer) &&
videoPixelFormat == media::PIXEL_FORMAT_NV12) {
[self processPixelBufferNV12IOSurface:pixelBuffer
captureFormat:captureFormat
colorSpace:colorSpace
timestamp:timestamp];
return;
}
// Second preference is to read the CVPixelBuffer's planes.
if ([self processPixelBufferPlanes:pixelBuffer
captureFormat:captureFormat
colorSpace:colorSpace
timestamp:timestamp]) {
return;
}
}
// Last preference is to read the CMSampleBuffer.
gfx::ColorSpace colorSpace =
media::GetFormatDescriptionColorSpace(formatDescription);
[self processSample:sampleBuffer
captureFormat:captureFormat
colorSpace:colorSpace
timestamp:timestamp];
}
- (void)onVideoError:(NSNotification*)errorNotification {
NSError* error = base::mac::ObjCCast<NSError>(
[errorNotification userInfo][AVCaptureSessionErrorKey]);
[self sendErrorString:[NSString
stringWithFormat:@"%@: %@",
[error localizedDescription],
[error localizedFailureReason]]];
}
- (void)sendErrorString:(NSString*)error {
DLOG(ERROR) << base::SysNSStringToUTF8(error);
base::AutoLock lock(_lock);
if (_frameReceiver)
_frameReceiver->ReceiveError(
media::VideoCaptureError::
kMacAvFoundationReceivedAVCaptureSessionRuntimeErrorNotification,
FROM_HERE, base::SysNSStringToUTF8(error));
}
- (void)callLocked:(base::OnceClosure)lambda {
base::AutoLock lock(_lock);
std::move(lambda).Run();
}
@end