/* * Copyright (C) 2020 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "LocalSampleBufferDisplayLayer.h" #if ENABLE(MEDIA_STREAM) && USE(AVFOUNDATION) #import "Color.h" #import "IntSize.h" #import "Logging.h" #import "MediaUtilities.h" #import #import #import #import #import #import #import #import #import #import using namespace WebCore; @interface WebAVSampleBufferStatusChangeListener : NSObject { WeakPtr _parent; } - (id)initWithParent:(LocalSampleBufferDisplayLayer*)callback; - (void)invalidate; - (void)begin; - (void)stop; @end @implementation WebAVSampleBufferStatusChangeListener - (id)initWithParent:(LocalSampleBufferDisplayLayer*)parent { if (!(self = [super init])) return nil; _parent = parent; return self; } - (void)dealloc { [self invalidate]; [super dealloc]; } - (void)invalidate { [self stop]; _parent = nullptr; } - (void)begin { ASSERT(_parent); ASSERT(_parent->displayLayer()); ASSERT(_parent->rootLayer()); [_parent->displayLayer() addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; [_parent->displayLayer() addObserver:self forKeyPath:@"error" options:NSKeyValueObservingOptionNew context:nil]; } - (void)stop { if (!_parent) return; if (_parent->displayLayer()) { [_parent->displayLayer() removeObserver:self forKeyPath:@"status"]; [_parent->displayLayer() removeObserver:self forKeyPath:@"error"]; } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { UNUSED_PARAM(context); UNUSED_PARAM(keyPath); UNUSED_PARAM(change); if (![object isKindOfClass:PAL::getAVSampleBufferDisplayLayerClass()]) return; if ([keyPath isEqualToString:@"status"]) { callOnMainThread([protectedSelf = RetainPtr(self)] { if (protectedSelf->_parent) protectedSelf->_parent->layerStatusDidChange(); }); return; } if ([keyPath isEqualToString:@"error"]) { callOnMainThread([protectedSelf = RetainPtr(self)] { if (protectedSelf->_parent) protectedSelf->_parent->layerErrorDidChange(); }); return; } } @end namespace WebCore { static void runWithoutAnimations(const WTF::Function& function) { [CATransaction begin]; [CATransaction setAnimationDuration:0]; [CATransaction setDisableActions:YES]; function(); [CATransaction commit]; } std::unique_ptr LocalSampleBufferDisplayLayer::create(Client& client) { auto sampleBufferDisplayLayer = adoptNS([PAL::allocAVSampleBufferDisplayLayerInstance() init]); if (!sampleBufferDisplayLayer) return nullptr; return makeUnique(WTFMove(sampleBufferDisplayLayer), client); } LocalSampleBufferDisplayLayer::LocalSampleBufferDisplayLayer(RetainPtr&& sampleBufferDisplayLayer, Client& client) : SampleBufferDisplayLayer(client) , m_statusChangeListener(adoptNS([[WebAVSampleBufferStatusChangeListener alloc] initWithParent:this])) , m_sampleBufferDisplayLayer(WTFMove(sampleBufferDisplayLayer)) , m_processingQueue(WorkQueue::create("LocalSampleBufferDisplayLayer queue")) #if !RELEASE_LOG_DISABLED , m_frameRateMonitor([this](auto info) { onIrregularFrameRateNotification(info.frameTime, info.lastFrameTime); }) #endif { ASSERT(isMainThread()); } void LocalSampleBufferDisplayLayer::initialize(bool hideRootLayer, IntSize size, CompletionHandler&& callback) { m_sampleBufferDisplayLayer.get().anchorPoint = { .5, .5 }; m_sampleBufferDisplayLayer.get().needsDisplayOnBoundsChange = YES; m_sampleBufferDisplayLayer.get().videoGravity = AVLayerVideoGravityResizeAspectFill; m_rootLayer = adoptNS([[CALayer alloc] init]); m_rootLayer.get().hidden = hideRootLayer; m_rootLayer.get().needsDisplayOnBoundsChange = YES; m_rootLayer.get().bounds = CGRectMake(0, 0, size.width(), size.height()); [m_statusChangeListener begin]; [m_rootLayer addSublayer:m_sampleBufferDisplayLayer.get()]; #ifndef NDEBUG [m_sampleBufferDisplayLayer setName:@"LocalSampleBufferDisplayLayer AVSampleBufferDisplayLayer"]; [m_rootLayer setName:@"LocalSampleBufferDisplayLayer AVSampleBufferDisplayLayer parent"]; #endif callback(true); } LocalSampleBufferDisplayLayer::~LocalSampleBufferDisplayLayer() { ASSERT(isMainThread()); m_processingQueue->dispatchSync([] { }); m_processingQueue = nullptr; [m_statusChangeListener stop]; m_pendingVideoFrameQueue.clear(); [m_sampleBufferDisplayLayer stopRequestingMediaData]; [m_sampleBufferDisplayLayer flush]; m_sampleBufferDisplayLayer = nullptr; m_rootLayer = nullptr; } void LocalSampleBufferDisplayLayer::layerStatusDidChange() { ASSERT(isMainThread()); if (m_client && m_sampleBufferDisplayLayer.get().status == AVQueuedSampleBufferRenderingStatusFailed) m_client->sampleBufferDisplayLayerStatusDidFail(); } void LocalSampleBufferDisplayLayer::layerErrorDidChange() { ASSERT(isMainThread()); // FIXME: Log error. } PlatformLayer* LocalSampleBufferDisplayLayer::displayLayer() { return m_sampleBufferDisplayLayer.get(); } PlatformLayer* LocalSampleBufferDisplayLayer::rootLayer() { return m_rootLayer.get(); } bool LocalSampleBufferDisplayLayer::didFail() const { return [m_sampleBufferDisplayLayer status] == AVQueuedSampleBufferRenderingStatusFailed; } void LocalSampleBufferDisplayLayer::updateDisplayMode(bool hideDisplayLayer, bool hideRootLayer) { if (m_rootLayer.get().hidden == hideRootLayer && m_sampleBufferDisplayLayer.get().hidden == hideDisplayLayer) return; runWithoutAnimations([&] { if (hideDisplayLayer && !hideRootLayer) m_rootLayer.get().backgroundColor = cachedCGColor(Color::black).get(); else m_rootLayer.get().backgroundColor = nil; m_sampleBufferDisplayLayer.get().hidden = hideDisplayLayer; m_rootLayer.get().hidden = hideRootLayer; }); } CGRect LocalSampleBufferDisplayLayer::bounds() const { return m_rootLayer.get().bounds; } void LocalSampleBufferDisplayLayer::updateRootLayerAffineTransform(CGAffineTransform transform) { runWithoutAnimations([&] { m_rootLayer.get().affineTransform = transform; }); } void LocalSampleBufferDisplayLayer::updateAffineTransform(CGAffineTransform transform) { runWithoutAnimations([&] { m_sampleBufferDisplayLayer.get().affineTransform = transform; }); } void LocalSampleBufferDisplayLayer::updateBoundsAndPosition(CGRect bounds, VideoFrame::Rotation rotation) { updateRootLayerBoundsAndPosition(bounds, rotation, ShouldUpdateRootLayer::No); } void LocalSampleBufferDisplayLayer::setRootLayerBoundsAndPositions(CGRect bounds, VideoFrame::Rotation rotation) { CGPoint position = { bounds.size.width / 2, bounds.size.height / 2}; if (rotation == VideoFrame::Rotation::Right || rotation == VideoFrame::Rotation::Left) std::swap(bounds.size.width, bounds.size.height); m_rootLayer.get().position = position; m_rootLayer.get().bounds = bounds; } void LocalSampleBufferDisplayLayer::updateRootLayerBoundsAndPosition(CGRect bounds, VideoFrame::Rotation rotation, ShouldUpdateRootLayer shouldUpdateRootLayer) { runWithoutAnimations([&] { if (shouldUpdateRootLayer == ShouldUpdateRootLayer::Yes) setRootLayerBoundsAndPositions(bounds, rotation); if (rotation == VideoFrame::Rotation::Right || rotation == VideoFrame::Rotation::Left) std::swap(bounds.size.width, bounds.size.height); CGPoint position = { bounds.size.width / 2, bounds.size.height / 2}; m_sampleBufferDisplayLayer.get().position = position; m_sampleBufferDisplayLayer.get().bounds = bounds; }); } void LocalSampleBufferDisplayLayer::flush() { m_processingQueue->dispatch([this] { [m_sampleBufferDisplayLayer flush]; }); } void LocalSampleBufferDisplayLayer::flushAndRemoveImage() { m_processingQueue->dispatch([this] { [m_sampleBufferDisplayLayer flushAndRemoveImage]; }); } static const double rendererLatency = 0.02; void LocalSampleBufferDisplayLayer::enqueueVideoFrame(VideoFrame& videoFrame) { if (m_paused) { #if !RELEASE_LOG_DISABLED m_frameRateMonitor.update(); #endif return; } m_processingQueue->dispatch([this, videoFrame = Ref { videoFrame }]() mutable { if (![m_sampleBufferDisplayLayer isReadyForMoreMediaData]) { RELEASE_LOG(WebRTC, "LocalSampleBufferDisplayLayer::enqueueSample (%{public}s) not ready for more media data", m_logIdentifier.utf8().data()); addVideoFrameToPendingQueue(WTFMove(videoFrame)); requestNotificationWhenReadyForVideoData(); return; } enqueueBuffer(videoFrame->pixelBuffer(), videoFrame->presentationTime()); }); } static void setSampleBufferAsDisplayImmediately(CMSampleBufferRef sampleBuffer) { CFArrayRef attachmentsArray = PAL::CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true); for (CFIndex i = 0; i < CFArrayGetCount(attachmentsArray); ++i) { CFMutableDictionaryRef attachments = checked_cf_cast(CFArrayGetValueAtIndex(attachmentsArray, i)); CFDictionarySetValue(attachments, PAL::kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue); } } void LocalSampleBufferDisplayLayer::enqueueBuffer(CVPixelBufferRef pixelBuffer, MediaTime presentationTime) { ASSERT(!isMainThread()); auto sampleBuffer = createVideoSampleBuffer(pixelBuffer, PAL::toCMTime(presentationTime)); auto now = MediaTime::createWithDouble(MonotonicTime::now().secondsSinceEpoch().value() + rendererLatency); if (m_renderPolicy == RenderPolicy::Immediately || now >= presentationTime) setSampleBufferAsDisplayImmediately(sampleBuffer.get()); [m_sampleBufferDisplayLayer enqueueSampleBuffer:sampleBuffer.get()]; #if !RELEASE_LOG_DISABLED constexpr size_t frameCountPerLog = 1800; // log every minute at 30 fps if (!(m_frameRateMonitor.frameCount() % frameCountPerLog)) { if (auto* metrics = [m_sampleBufferDisplayLayer videoPerformanceMetrics]) RELEASE_LOG(WebRTC, "LocalSampleBufferDisplayLayer (%{public}s) metrics, total=%lu, dropped=%lu, corrupted=%lu, display-composited=%lu, non-display-composited=%lu (pending=%lu)", m_logIdentifier.utf8().data(), metrics.totalNumberOfVideoFrames, metrics.numberOfDroppedVideoFrames, metrics.numberOfCorruptedVideoFrames, metrics.numberOfDisplayCompositedVideoFrames, metrics.numberOfNonDisplayCompositedVideoFrames, m_pendingVideoFrameQueue.size()); } m_frameRateMonitor.update(); #endif } #if !RELEASE_LOG_DISABLED void LocalSampleBufferDisplayLayer::onIrregularFrameRateNotification(MonotonicTime frameTime, MonotonicTime lastFrameTime) { callOnMainThread([frameTime = frameTime.secondsSinceEpoch().value(), lastFrameTime = lastFrameTime.secondsSinceEpoch().value(), observedFrameRate = m_frameRateMonitor.observedFrameRate(), frameCount = m_frameRateMonitor.frameCount(), weakThis = WeakPtr { *this }] { if (!weakThis) return; RELEASE_LOG(WebRTC, "LocalSampleBufferDisplayLayer::enqueueVideoFrame (%{public}s) at %f, previous frame was at %f, observed frame rate is %f, delay since last frame is %f ms, frame count is %lu", weakThis->m_logIdentifier.utf8().data(), frameTime, lastFrameTime, observedFrameRate, (frameTime - lastFrameTime) * 1000, frameCount); }); } #endif void LocalSampleBufferDisplayLayer::removeOldVideoFramesFromPendingQueue() { ASSERT(!isMainThread()); if (m_pendingVideoFrameQueue.isEmpty()) return; if (m_renderPolicy == RenderPolicy::Immediately) { m_pendingVideoFrameQueue.clear(); return; } auto now = MediaTime::createWithDouble(MonotonicTime::now().secondsSinceEpoch().value()); while (!m_pendingVideoFrameQueue.isEmpty()) { auto presentationTime = m_pendingVideoFrameQueue.first()->presentationTime(); if (presentationTime.isValid() && presentationTime > now) break; m_pendingVideoFrameQueue.removeFirst(); } } void LocalSampleBufferDisplayLayer::addVideoFrameToPendingQueue(Ref&& videoFrame) { ASSERT(!isMainThread()); removeOldVideoFramesFromPendingQueue(); m_pendingVideoFrameQueue.append(WTFMove(videoFrame)); } void LocalSampleBufferDisplayLayer::clearVideoFrames() { m_processingQueue->dispatch([this] { m_pendingVideoFrameQueue.clear(); }); } void LocalSampleBufferDisplayLayer::requestNotificationWhenReadyForVideoData() { WeakPtr weakThis { *this }; [m_sampleBufferDisplayLayer requestMediaDataWhenReadyOnQueue:dispatch_get_main_queue() usingBlock:^{ if (!weakThis) return; m_processingQueue->dispatch([this] { [m_sampleBufferDisplayLayer stopRequestingMediaData]; while (!m_pendingVideoFrameQueue.isEmpty()) { if (![m_sampleBufferDisplayLayer isReadyForMoreMediaData]) { requestNotificationWhenReadyForVideoData(); return; } auto videoFrame = m_pendingVideoFrameQueue.takeFirst(); enqueueBuffer(videoFrame->pixelBuffer(), videoFrame->presentationTime()); } }); }]; } } #endif