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