summaryrefslogtreecommitdiff
path: root/chromium/media/capture/video/mac/video_capture_device_avfoundation_mac.mm
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/media/capture/video/mac/video_capture_device_avfoundation_mac.mm')
-rw-r--r--chromium/media/capture/video/mac/video_capture_device_avfoundation_mac.mm290
1 files changed, 240 insertions, 50 deletions
diff --git a/chromium/media/capture/video/mac/video_capture_device_avfoundation_mac.mm b/chromium/media/capture/video/mac/video_capture_device_avfoundation_mac.mm
index 31d5516bf2e..6dd77ee0eb9 100644
--- a/chromium/media/capture/video/mac/video_capture_device_avfoundation_mac.mm
+++ b/chromium/media/capture/video/mac/video_capture_device_avfoundation_mac.mm
@@ -10,13 +10,15 @@
#include <stddef.h>
#include <stdint.h>
+#include "base/debug/dump_without_crashing.h"
#include "base/location.h"
#include "base/mac/foundation_util.h"
-#include "base/mac/mac_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"
@@ -24,6 +26,7 @@
#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"
@@ -54,10 +57,15 @@ base::TimeDelta GetCMSampleBufferTimestamp(CMSampleBufferRef sampleBuffer) {
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,
@@ -163,14 +171,14 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
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::CreateIfAutoReconfigureEnabled();
- if (_sampleBufferTransformer) {
+ if (base::FeatureList::IsEnabled(media::kInCaptureConvertToNv12)) {
+ _sampleBufferTransformer = media::SampleBufferTransformer::Create();
VLOG(1) << "Capturing with SampleBufferTransformer enabled";
}
}
@@ -268,7 +276,6 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
media::FindBestCaptureFormat([_captureDevice formats], width, height,
frameRate),
base::scoped_policy::RETAIN);
- // Default to NV12, a pixel format commonly supported by web cameras.
FourCharCode best_fourcc = kDefaultFourCCPixelFormat;
if (_bestCaptureFormat) {
best_fourcc = CMFormatDescriptionGetMediaSubType(
@@ -328,6 +335,39 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
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 (size_t i = 0; i < resolutions.size(); ++i) {
+ if (resolutions[i] != _scaledFrameTransformers[i]->destination_size()) {
+ reconfigureScaledFrameTransformers = true;
+ break;
+ }
+ }
+ }
+ if (!reconfigureScaledFrameTransformers)
+ return;
+ _scaledFrameTransformers.clear();
+ for (const auto& resolution : resolutions) {
+ // 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, resolution,
+ kPixelBufferPoolSize);
+ _scaledFrameTransformers.push_back(std::move(scaledFrameTransformer));
+ }
+}
+
- (BOOL)startCapture {
DCHECK(_mainThreadTaskRunner->BelongsToCurrentThread());
if (!_captureSession) {
@@ -351,11 +391,18 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
}
}
+ {
+ 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.
@@ -557,10 +604,10 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
timestamp);
}
-- (BOOL)processPixelBuffer:(CVImageBufferRef)pixelBuffer
- captureFormat:(const media::VideoCaptureFormat&)captureFormat
- colorSpace:(const gfx::ColorSpace&)colorSpace
- timestamp:(const base::TimeDelta)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) {
@@ -620,20 +667,40 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
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;
- CHECK(pixelBufferHeights == packedHeights);
std::vector<uint8_t> packedBufferCopy;
if (needsCopyToPackedBuffer) {
- CHECK(pixelBufferHeights == packedHeights);
- packedBufferCopy.resize(packedBufferSize);
+ packedBufferCopy.resize(packedBufferSize, 0);
uint8_t* dstAddr = packedBufferCopy.data();
for (size_t plane = 0; plane < numPlanes; ++plane) {
uint8_t* srcAddr = pixelBufferAddresses[plane];
- for (size_t row = 0; row < packedHeights[plane]; ++row) {
- memcpy(dstAddr, srcAddr, packedBytesPerRows[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];
}
@@ -649,12 +716,68 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
return YES;
}
-- (void)processNV12IOSurface:(IOSurfaceRef)ioSurface
- captureFormat:(const media::VideoCaptureFormat&)captureFormat
- colorSpace:(const gfx::ColorSpace&)colorSpace
- timestamp:(const base::TimeDelta)timestamp {
+- (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();
+ std::vector<media::CapturedExternalVideoBuffer> scaledExternalBuffers;
+ 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;
+ }
+ base::ScopedCFTypeRef<CVPixelBufferRef> scaledPixelBuffer =
+ scaledFrameTransformer->Transform(pixelBuffer);
+ if (!scaledPixelBuffer) {
+ LOG(ERROR) << "Failed to downscale frame, skipping resolution "
+ << scaledFrameSize.ToString();
+ continue;
+ }
+ 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;
@@ -673,9 +796,63 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
kCGColorSpaceSRGB);
}
- _lock.AssertAcquired();
- _frameReceiver->ReceiveExternalGpuMemoryBufferFrame(
- std::move(handle), captureFormat, overriddenColorSpace, timestamp);
+ 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::TimeDelta::FromSeconds(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.
@@ -689,10 +866,15 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
// Concurrent calls into |_frameReceiver| are not supported, so take |_lock|
// before any of the subsequent paths.
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
@@ -700,26 +882,30 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
// TODO(https://crbug.com/1160315): When the SampleBufferTransformer is
// patched to support non-MJPEG-and-non-pixel-buffer sample buffers, remove
// this workaround.
- bool sampleBufferLacksPixelBufferAndIsNotMjpeg =
- !CMSampleBufferGetImageBuffer(sampleBuffer) &&
+ bool sampleHasPixelBufferOrIsMjpeg =
+ CMSampleBufferGetImageBuffer(sampleBuffer) ||
CMFormatDescriptionGetMediaSubType(CMSampleBufferGetFormatDescription(
- sampleBuffer)) != kCMVideoCodecType_JPEG_OpenDML;
+ sampleBuffer)) == kCMVideoCodecType_JPEG_OpenDML;
// If the SampleBufferTransformer is enabled, convert all possible capture
// formats to an IOSurface-backed NV12 pixel buffer.
- // TODO(hbos): If |_sampleBufferTransformer| gets shipped 100%, delete the
+ // TODO(https://crbug.com/1175142): Update this code path so that it is
+ // possible to turn on/off the kAVFoundationCaptureV2ZeroCopy feature and the
+ // kInCaptureConvertToNv12 feature separately.
+ // TODO(hbos): When |_sampleBufferTransformer| gets shipped 100%, delete the
// other code paths.
- if (_sampleBufferTransformer && !sampleBufferLacksPixelBufferAndIsNotMjpeg) {
+ if (_sampleBufferTransformer && sampleHasPixelBufferOrIsMjpeg) {
+ _sampleBufferTransformer->Reconfigure(
+ media::SampleBufferTransformer::GetBestTransformerForNv12Output(
+ sampleBuffer),
+ kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
+ media::GetSampleBufferSize(sampleBuffer), kPixelBufferPoolSize);
base::ScopedCFTypeRef<CVPixelBufferRef> pixelBuffer =
- _sampleBufferTransformer->AutoReconfigureAndTransform(sampleBuffer);
+ _sampleBufferTransformer->Transform(sampleBuffer);
if (!pixelBuffer) {
LOG(ERROR) << "Failed to transform captured frame. Dropping frame.";
return;
}
- IOSurfaceRef ioSurface = CVPixelBufferGetIOSurface(pixelBuffer);
- CHECK(ioSurface);
- CHECK_EQ(CVPixelBufferGetPixelFormatType(pixelBuffer),
- kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange); // NV12
const media::VideoCaptureFormat captureFormat(
gfx::Size(CVPixelBufferGetWidth(pixelBuffer),
CVPixelBufferGetHeight(pixelBuffer)),
@@ -730,14 +916,14 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
// 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
- // processNV12IOSurface.
+ // 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 processNV12IOSurface:ioSurface
- captureFormat:captureFormat
- colorSpace:kColorSpaceRec709Apple
- timestamp:timestamp];
+ [self processPixelBufferNV12IOSurface:pixelBuffer
+ captureFormat:captureFormat
+ colorSpace:kColorSpaceRec709Apple
+ timestamp:timestamp];
return;
}
@@ -773,29 +959,28 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
static const bool kEnableGpuMemoryBuffers =
base::FeatureList::IsEnabled(media::kAVFoundationCaptureV2ZeroCopy);
if (kEnableGpuMemoryBuffers) {
- IOSurfaceRef ioSurface = CVPixelBufferGetIOSurface(pixelBuffer);
- if (ioSurface && videoPixelFormat == media::PIXEL_FORMAT_NV12) {
- [self processNV12IOSurface:ioSurface
- captureFormat:captureFormat
- colorSpace:colorSpace
- timestamp:timestamp];
+ 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.
- if ([self processPixelBuffer:pixelBuffer
- captureFormat:captureFormat
- colorSpace:colorSpace
- timestamp:timestamp]) {
+ // 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;
- if (@available(macOS 10.11, *))
- colorSpace = media::GetFormatDescriptionColorSpace(formatDescription);
+ gfx::ColorSpace colorSpace =
+ media::GetFormatDescriptionColorSpace(formatDescription);
[self processSample:sampleBuffer
captureFormat:captureFormat
colorSpace:colorSpace
@@ -821,4 +1006,9 @@ AVCaptureDeviceFormat* FindBestCaptureFormat(
FROM_HERE, base::SysNSStringToUTF8(error));
}
+- (void)callLocked:(base::OnceClosure)lambda {
+ base::AutoLock lock(_lock);
+ std::move(lambda).Run();
+}
+
@end