WebVideoFullscreenController.mm   [plain text]

/*
* 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> delegate;
@end

static id<WebAVPlayerViewDelegate> WebAVPlayerView_delegate(id aSelf, SEL)
{
void* delegate = nil;
object_getInstanceVariable(aSelf, "_webDelegate", &delegate);
return static_cast<id<WebAVPlayerViewDelegate>>(delegate);
}

static void WebAVPlayerView_setDelegate(id aSelf, SEL, id<WebAVPlayerViewDelegate> delegate)
{
object_setInstanceVariable(aSelf, "_webDelegate", delegate);
}

static BOOL WebAVPlayerView_isFullScreen(id aSelf, SEL)
{
WebAVPlayerView *playerView = aSelf;
return [playerView.delegate playerViewIsFullScreen:playerView];
}

static void WebAVPlayerView_enterFullScreen(id aSelf, SEL, id sender)
{
WebAVPlayerView *playerView = aSelf;
[playerView.delegate playerViewRequestEnterFullscreen:playerView];
}

static void WebAVPlayerView_exitFullScreen(id aSelf, SEL, id sender)
{
WebAVPlayerView *playerView = aSelf;
[playerView.delegate 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(setDelegate:), (IMP)WebAVPlayerView_setDelegate, "v@:@");
class_addMethod(theClass, @selector(delegate), (IMP)WebAVPlayerView_delegate, "@@:");
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).
NSWindow *window = [[WebCoreFullScreenWindow alloc] initWithContentRect:NSZeroRect styleMask:(NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskResizable) backing:NSBackingStoreBuffered defer:NO];
[window setCollectionBehavior:([window collectionBehavior] | NSWindowCollectionBehaviorFullScreenPrimary)];
window.delegate = self;
self = [super initWithWindow:window];
[window release];
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 = [[[WebVideoFullscreenOverlayLayer alloc] init] autorelease];
[_contentOverlay setWantsLayer:YES];
[_contentOverlay setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
[self windowDidLoad];

return self;
}
- (void)dealloc
{
ASSERT(!_backgroundFullscreenWindow);
ASSERT(!_fadeAnimation);
_playerView.delegate = 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.delegate = 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