| /* |
| * Copyright (C) 2009-2018 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. AND ITS CONTRIBUTORS ``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 ITS 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 "WebVideoFullscreenController.h" |
| |
| #if ENABLE(VIDEO) && PLATFORM(MAC) |
| |
| #import <AVFoundation/AVPlayer.h> |
| #import <WebCore/HTMLVideoElement.h> |
| #import <WebCore/PlaybackSessionInterfaceAVKit.h> |
| #import <WebCore/PlaybackSessionModelMediaElement.h> |
| #import <WebCore/WebAVPlayerController.h> |
| #import <WebCore/WebCoreFullScreenWindow.h> |
| #import <objc/message.h> |
| #import <objc/runtime.h> |
| #import <pal/spi/cocoa/AVKitSPI.h> |
| #import <pal/spi/mac/NSWindowSPI.h> |
| #import <wtf/RetainPtr.h> |
| |
| #import <pal/cf/CoreMediaSoftLink.h> |
| #import <pal/cocoa/AVFoundationSoftLink.h> |
| |
| SOFTLINK_AVKIT_FRAMEWORK() |
| SOFT_LINK_CLASS(AVKit, AVPlayerView) |
| |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| |
| @interface AVPlayerView (SecretStuff) |
| @property (nonatomic, assign) BOOL showsAudioOnlyIndicatorView; |
| @end |
| |
| @interface WebVideoFullscreenOverlayLayer : CALayer |
| @end |
| |
| @implementation WebVideoFullscreenOverlayLayer |
| - (void)layoutSublayers |
| { |
| for (CALayer* layer in self.sublayers) |
| layer.frame = self.bounds; |
| } |
| @end |
| |
| @class WebAVPlayerView; |
| |
| @protocol WebAVPlayerViewDelegate |
| - (BOOL)playerViewIsFullScreen:(WebAVPlayerView*)playerView; |
| - (void)playerViewRequestEnterFullscreen:(WebAVPlayerView*)playerView; |
| - (void)playerViewRequestExitFullscreen:(WebAVPlayerView*)playerView; |
| @end |
| |
| @interface WebAVPlayerView : AVPlayerView |
| @property (weak) id<WebAVPlayerViewDelegate> webDelegate; |
| @end |
| |
| static id<WebAVPlayerViewDelegate> WebAVPlayerView_webDelegate(id aSelf, SEL) |
| { |
| void* webDelegate = nil; |
| object_getInstanceVariable(aSelf, "_webDelegate", &webDelegate); |
| return static_cast<id<WebAVPlayerViewDelegate>>(webDelegate); |
| } |
| |
| static void WebAVPlayerView_setWebDelegate(id aSelf, SEL, id<WebAVPlayerViewDelegate> webDelegate) |
| { |
| object_setInstanceVariable(aSelf, "_webDelegate", webDelegate); |
| } |
| |
| static BOOL WebAVPlayerView_isFullScreen(id aSelf, SEL) |
| { |
| WebAVPlayerView *playerView = aSelf; |
| return [playerView.webDelegate playerViewIsFullScreen:playerView]; |
| } |
| |
| static void WebAVPlayerView_enterFullScreen(id aSelf, SEL, id sender) |
| { |
| WebAVPlayerView *playerView = aSelf; |
| [playerView.webDelegate playerViewRequestEnterFullscreen:playerView]; |
| } |
| |
| static void WebAVPlayerView_exitFullScreen(id aSelf, SEL, id sender) |
| { |
| WebAVPlayerView *playerView = aSelf; |
| [playerView.webDelegate playerViewRequestExitFullscreen:playerView]; |
| } |
| |
| static WebAVPlayerView *allocWebAVPlayerViewInstance() |
| { |
| static Class theClass = nil; |
| static dispatch_once_t onceToken; |
| dispatch_once(&onceToken, ^{ |
| ASSERT(getAVPlayerViewClass()); |
| Class aClass = objc_allocateClassPair(getAVPlayerViewClass(), "WebAVPlayerView", 0); |
| theClass = aClass; |
| class_addMethod(theClass, @selector(setWebDelegate:), (IMP)WebAVPlayerView_setWebDelegate, "v@:@"); |
| class_addMethod(theClass, @selector(webDelegate), (IMP)WebAVPlayerView_webDelegate, "@@:"); |
| class_addMethod(theClass, @selector(isFullScreen), (IMP)WebAVPlayerView_isFullScreen, "B@:"); |
| class_addMethod(theClass, @selector(enterFullScreen:), (IMP)WebAVPlayerView_enterFullScreen, "v@:@"); |
| class_addMethod(theClass, @selector(exitFullScreen:), (IMP)WebAVPlayerView_exitFullScreen, "v@:@"); |
| |
| class_addIvar(theClass, "_webDelegate", sizeof(id), log2(sizeof(id)), "@"); |
| class_addIvar(theClass, "_webIsFullScreen", sizeof(BOOL), log2(sizeof(BOOL)), "B"); |
| |
| objc_registerClassPair(theClass); |
| }); |
| return (WebAVPlayerView *)[theClass alloc]; |
| } |
| |
| @interface WebVideoFullscreenController () <WebAVPlayerViewDelegate, NSWindowDelegate> { |
| RefPtr<WebCore::PlaybackSessionModelMediaElement> _playbackModel; |
| RefPtr<WebCore::PlaybackSessionInterfaceAVKit> _playbackInterface; |
| RetainPtr<NSView> _contentOverlay; |
| BOOL _isFullScreen; |
| } |
| @property (readonly) WebCoreFullScreenWindow* fullscreenWindow; |
| @property (readonly) WebAVPlayerView* playerView; |
| @end |
| |
| @implementation WebVideoFullscreenController |
| |
| - (id)init |
| { |
| // Do not defer window creation, to make sure -windowNumber is created (needed by WebWindowScaleAnimation). |
| auto window = adoptNS([[WebCoreFullScreenWindow alloc] initWithContentRect:NSZeroRect styleMask:(NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskResizable) backing:NSBackingStoreBuffered defer:NO]); |
| [window setCollectionBehavior:([window collectionBehavior] | NSWindowCollectionBehaviorFullScreenPrimary)]; |
| [window setDelegate: self]; |
| self = [super initWithWindow:window.get()]; |
| if (!self) |
| return nil; |
| _playbackModel = WebCore::PlaybackSessionModelMediaElement::create(); |
| _playbackInterface = WebCore::PlaybackSessionInterfaceAVKit::create(*_playbackModel); |
| _contentOverlay = adoptNS([[NSView alloc] initWithFrame:NSZeroRect]); |
| _contentOverlay.get().layerContentsRedrawPolicy = NSViewLayerContentsRedrawNever; |
| _contentOverlay.get().layer = adoptNS([[WebVideoFullscreenOverlayLayer alloc] init]).get(); |
| [_contentOverlay setWantsLayer:YES]; |
| [_contentOverlay setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; |
| [self windowDidLoad]; |
| |
| return self; |
| } |
| - (void)dealloc |
| { |
| ASSERT(!_backgroundFullscreenWindow); |
| ASSERT(!_fadeAnimation); |
| _playerView.webDelegate = nil; |
| _playbackModel = nil; |
| [super dealloc]; |
| } |
| |
| - (WebCoreFullScreenWindow *)fullscreenWindow |
| { |
| return (WebCoreFullScreenWindow *)[super window]; |
| } |
| |
| - (void)windowDidLoad |
| { |
| auto window = [self fullscreenWindow]; |
| |
| [window setHasShadow:YES]; // This is nicer with a shadow. |
| [window setLevel:NSPopUpMenuWindowLevel-1]; |
| |
| _playerView = [allocWebAVPlayerViewInstance() initWithFrame:window.contentLayoutRect]; |
| _playerView.controlsStyle = AVPlayerViewControlsStyleNone; |
| _playerView.showsFullScreenToggleButton = YES; |
| _playerView.showsAudioOnlyIndicatorView = NO; |
| _playerView.webDelegate = self; |
| window.contentView = _playerView; |
| [_contentOverlay setFrame:_playerView.contentOverlayView.bounds]; |
| [_playerView.contentOverlayView addSubview:_contentOverlay.get()]; |
| |
| [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidResignActive:) name:NSApplicationDidResignActiveNotification object:NSApp]; |
| } |
| |
| - (NakedPtr<WebCore::HTMLVideoElement>)videoElement |
| { |
| return _videoElement.get(); |
| } |
| |
| // FIXME: This method is not really a setter. The caller relies on its side effects, and it's |
| // called once each time we enter full screen. So it should have a different name. |
| - (void)setVideoElement:(NakedPtr<WebCore::HTMLVideoElement>)videoElement |
| { |
| ASSERT(videoElement); |
| _videoElement = videoElement; |
| |
| if (![self isWindowLoaded]) |
| return; |
| |
| _playbackModel->setMediaElement(videoElement); |
| self.playerView.playerController = (AVPlayerController*)_playbackInterface->playerController(); |
| } |
| |
| - (void)enterFullscreen:(NSScreen *)screen |
| { |
| if (!_videoElement) |
| return; |
| [NSAnimationContext beginGrouping]; |
| _videoElement->setVideoFullscreenLayer(_contentOverlay.get().layer, [self, protectedSelf = retainPtr(self)] { |
| [self.fullscreenWindow setFrame:self.videoElementRect display:YES]; |
| [self.fullscreenWindow makeKeyAndOrderFront:self]; |
| [self.fullscreenWindow enterFullScreenMode:self]; |
| [NSAnimationContext endGrouping]; |
| }); |
| } |
| |
| - (void)exitFullscreen |
| { |
| [self.fullscreenWindow exitFullScreenMode:self]; |
| } |
| |
| - (NSRect)videoElementRect |
| { |
| return _videoElement->screenRect(); |
| } |
| |
| - (void)applicationDidResignActive:(NSNotification*)notification |
| { |
| UNUSED_PARAM(notification); |
| NSWindow* fullscreenWindow = [self fullscreenWindow]; |
| |
| // Replicate the QuickTime Player (X) behavior when losing active application status: |
| // Is the fullscreen screen the main screen? (Note: this covers the case where only a |
| // single screen is available.) Is the fullscreen screen on the current space? IFF so, |
| // then exit fullscreen mode. |
| if (fullscreenWindow.screen == [NSScreen screens][0] && fullscreenWindow.onActiveSpace) |
| [self _requestExit]; |
| } |
| |
| - (void)_requestExit |
| { |
| [self.fullscreenWindow exitFullScreenMode:self]; |
| } |
| |
| - (void)_requestEnter |
| { |
| if (_videoElement) |
| _videoElement->enterFullscreen(); |
| } |
| |
| - (void)cancelOperation:(id)sender |
| { |
| [self _requestExit]; |
| } |
| |
| - (BOOL)playerViewIsFullScreen:(WebAVPlayerView*)playerView |
| { |
| return _isFullScreen; |
| } |
| |
| - (void)playerViewRequestEnterFullscreen:(AVPlayerView*)playerView |
| { |
| [self _requestEnter]; |
| } |
| |
| - (void)playerViewRequestExitFullscreen:(AVPlayerView*)playerView |
| { |
| [self _requestExit]; |
| } |
| |
| - (nullable NSArray<NSWindow *> *)customWindowsToEnterFullScreenForWindow:(NSWindow *)window |
| { |
| return @[self.fullscreenWindow]; |
| } |
| |
| - (void)window:(NSWindow *)window startCustomAnimationToEnterFullScreenWithDuration:(NSTimeInterval)duration |
| { |
| [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { |
| context.allowsImplicitAnimation = YES; |
| context.duration = duration; |
| [window setFrame:window.screen.frame display:YES]; |
| } completionHandler:NULL]; |
| } |
| |
| - (nullable NSArray<NSWindow *> *)customWindowsToExitFullScreenForWindow:(NSWindow *)window |
| { |
| return @[self.fullscreenWindow]; |
| } |
| |
| - (void)window:(NSWindow *)window startCustomAnimationToExitFullScreenWithDuration:(NSTimeInterval)duration |
| { |
| [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { |
| context.allowsImplicitAnimation = YES; |
| context.duration = duration; |
| [window setFrame:self.videoElementRect display:YES]; |
| } completionHandler:NULL]; |
| } |
| |
| - (void)windowDidEnterFullScreen:(NSNotification *)notification |
| { |
| _playerView.controlsStyle = AVPlayerViewControlsStyleFloating; |
| [_playerView willChangeValueForKey:@"isFullScreen"]; |
| _isFullScreen = YES; |
| [_playerView didChangeValueForKey:@"isFullScreen"]; |
| if (_videoElement) |
| _videoElement->didBecomeFullscreenElement(); |
| } |
| |
| - (void)windowWillExitFullScreen:(NSNotification *)notification |
| { |
| _playerView.controlsStyle = AVPlayerViewControlsStyleNone; |
| } |
| |
| - (void)windowDidExitFullScreen:(NSNotification *)notification |
| { |
| [_playerView willChangeValueForKey:@"isFullScreen"]; |
| _isFullScreen = NO; |
| [_playerView didChangeValueForKey:@"isFullScreen"]; |
| |
| if (!_videoElement) { |
| [self.fullscreenWindow close]; |
| return; |
| } |
| |
| [NSAnimationContext beginGrouping]; |
| _videoElement->setVideoFullscreenLayer(nil, [self, protectedSelf = retainPtr(self)] { |
| [self.fullscreenWindow close]; |
| [NSAnimationContext endGrouping]; |
| }); |
| |
| if (_videoElement->isFullscreen()) |
| _videoElement->exitFullscreen(); |
| } |
| |
| @end |
| |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| #endif |