23aac34426ba03ab93b4e963f1e82f851404a1cb
[WebKit-https.git] / Source / WebKitLegacy / mac / WebView / WebVideoFullscreenController.mm
1 /*
2  * Copyright (C) 2009-2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "WebVideoFullscreenController.h"
27
28 #if ENABLE(VIDEO) && PLATFORM(MAC)
29
30 #import "WebVideoFullscreenHUDWindowController.h"
31 #import "WebWindowAnimation.h"
32 #import <AVFoundation/AVPlayer.h>
33 #import <AVFoundation/AVPlayerLayer.h>
34 #import <Carbon/Carbon.h>
35 #import <WebCore/HTMLVideoElement.h>
36 #import <objc/runtime.h>
37 #import <pal/system/SleepDisabler.h>
38 #import <wtf/RetainPtr.h>
39
40 #import <pal/cocoa/AVFoundationSoftLink.h>
41
42 ALLOW_DEPRECATED_DECLARATIONS_BEGIN
43
44 @interface WebVideoFullscreenWindow : NSWindow<NSAnimationDelegate> {
45     SEL _controllerActionOnAnimationEnd;
46     WebWindowScaleAnimation *_fullscreenAnimation; // (retain)
47 }
48 - (void)animateFromRect:(NSRect)startRect toRect:(NSRect)endRect withSubAnimation:(NSAnimation *)subAnimation controllerAction:(SEL)controllerAction;
49 @end
50
51 @interface WebVideoFullscreenController () <WebVideoFullscreenHUDWindowControllerDelegate>
52 @end
53
54 @implementation WebVideoFullscreenController
55
56 - (id)init
57 {
58     // Do not defer window creation, to make sure -windowNumber is created (needed by WebWindowScaleAnimation).
59     NSWindow *window = [[WebVideoFullscreenWindow alloc] initWithContentRect:NSZeroRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
60     self = [super initWithWindow:window];
61     [window release];
62     if (!self)
63         return nil;
64     [self windowDidLoad];
65     return self;
66     
67 }
68 - (void)dealloc
69 {
70     ASSERT(!_backgroundFullscreenWindow);
71     ASSERT(!_fadeAnimation);
72     [[NSNotificationCenter defaultCenter] removeObserver:self];
73     [super dealloc];
74 }
75
76 - (WebVideoFullscreenWindow *)fullscreenWindow
77 {
78     return (WebVideoFullscreenWindow *)[super window];
79 }
80
81 - (void)windowDidLoad
82 {
83     auto window = [self fullscreenWindow];
84     auto contentView = [window contentView];
85
86     [window setHasShadow:YES]; // This is nicer with a shadow.
87     [window setLevel:NSPopUpMenuWindowLevel-1];
88
89     [contentView setLayer:[CALayer layer]];
90     [contentView setWantsLayer:YES];
91
92     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidResignActive:) name:NSApplicationDidResignActiveNotification object:NSApp];
93     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidChangeScreenParameters:) name:NSApplicationDidChangeScreenParametersNotification object:NSApp];
94 }
95
96 - (WebCore::HTMLVideoElement *)videoElement
97 {
98     return _videoElement.get();
99 }
100
101 // FIXME: This method is not really a setter. The caller relies on its side effects, and it's
102 // called once each time we enter full screen. So it should have a different name.
103 - (void)setVideoElement:(WebCore::HTMLVideoElement *)videoElement
104 {
105     ASSERT(videoElement);
106     _videoElement = videoElement;
107
108     if (![self isWindowLoaded])
109         return;
110     auto corePlayer = videoElement->player();
111     if (!corePlayer)
112         return;
113     auto player = corePlayer->objCAVFoundationAVPlayer();
114     if (!player)
115         return;
116
117     auto contentView = [[self fullscreenWindow] contentView];
118
119     auto layer = adoptNS([PAL::allocAVPlayerLayerInstance() init]);
120     [layer setPlayer:player];
121
122     [contentView setLayer:layer.get()];
123
124     // FIXME: The windowDidLoad method already called this, so it should
125     // not be necessary to do it again here.
126     [contentView setWantsLayer:YES];
127
128     // FIXME: This can be called multiple times, and won't necessarily be
129     // balanced by calls to windowDidExitFullscreen in some cases, so it
130     // would be better to change things so the observer is reliably added
131     // only once and guaranteed removed even in unusual edge cases.
132     [player addObserver:self forKeyPath:@"rate" options:0 context:nullptr];
133 }
134
135 - (CGFloat)clearFadeAnimation
136 {
137     [_fadeAnimation stopAnimation];
138     CGFloat previousAlpha = [_fadeAnimation currentAlpha];
139     [_fadeAnimation setWindow:nil];
140     [_fadeAnimation release];
141     _fadeAnimation = nil;
142     return previousAlpha;
143 }
144
145 - (void)windowDidExitFullscreen
146 {
147     CALayer *layer = [[[self window] contentView] layer];
148     if ([layer isKindOfClass:PAL::getAVPlayerLayerClass()])
149         [[(AVPlayerLayer *)layer player] removeObserver:self forKeyPath:@"rate"];
150
151     [self clearFadeAnimation];
152     [[self window] close];
153     [self setWindow:nil];
154     [self updateMenuAndDockForFullscreen];   
155     [_hudController setDelegate:nil];
156     [_hudController release];
157     _hudController = nil;
158     [_backgroundFullscreenWindow close];
159     [_backgroundFullscreenWindow release];
160     _backgroundFullscreenWindow = nil;
161     
162     [self autorelease]; // Associated -retain is in -exitFullscreen.
163     _isEndingFullscreen = NO;
164 }
165
166 - (void)windowDidEnterFullscreen
167 {
168     [self clearFadeAnimation];
169
170     ASSERT(!_hudController);
171     _hudController = [[WebVideoFullscreenHUDWindowController alloc] init];
172     [_hudController setDelegate:self];
173
174     [self updateMenuAndDockForFullscreen];
175     [NSCursor setHiddenUntilMouseMoves:YES];
176     
177     // Give the HUD keyboard focus initially
178     [_hudController fadeWindowIn];
179 }
180
181 - (NSRect)videoElementRect
182 {
183     return _videoElement->screenRect();
184 }
185
186 - (void)applicationDidResignActive:(NSNotification*)notification
187 {   
188     UNUSED_PARAM(notification);
189     NSWindow* fullscreenWindow = [self fullscreenWindow];
190
191     // Replicate the QuickTime Player (X) behavior when losing active application status:
192     // Is the fullscreen screen the main screen? (Note: this covers the case where only a 
193     // single screen is available.)  Is the fullscreen screen on the current space? IFF so, 
194     // then exit fullscreen mode.    
195     if (fullscreenWindow.screen == [NSScreen screens][0] && fullscreenWindow.onActiveSpace)
196         [self requestExitFullscreenWithAnimation:NO];
197 }
198
199
200 // MARK: -
201 // MARK: Exposed Interface
202
203 static NSRect frameExpandedToRatioOfFrame(NSRect frameToExpand, NSRect frame)
204 {
205     // Keep a constrained aspect ratio for the destination window
206     NSRect result = frameToExpand;
207     CGFloat newRatio = frame.size.width / frame.size.height;
208     CGFloat originalRatio = frameToExpand.size.width / frameToExpand.size.height;
209     if (newRatio > originalRatio) {
210         CGFloat newWidth = newRatio * frameToExpand.size.height;
211         CGFloat diff = newWidth - frameToExpand.size.width;
212         result.size.width = newWidth;
213         result.origin.x -= diff / 2;
214     } else {
215         CGFloat newHeight = frameToExpand.size.width / newRatio;
216         CGFloat diff = newHeight - frameToExpand.size.height;
217         result.size.height = newHeight;
218         result.origin.y -= diff / 2;
219     }
220     return result;
221 }
222
223 static NSWindow *createBackgroundFullscreenWindow(NSRect frame, int level)
224 {
225     NSWindow *window = [[NSWindow alloc] initWithContentRect:frame styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
226     [window setOpaque:YES];
227     [window setBackgroundColor:[NSColor blackColor]];
228     [window setLevel:level];
229     [window setReleasedWhenClosed:NO];
230     return window;
231 }
232
233 - (void)setupFadeAnimationIfNeededAndFadeIn:(BOOL)fadeIn
234 {
235     CGFloat initialAlpha = fadeIn ? 0 : 1;
236     if (_fadeAnimation) {
237         // Make sure we support queuing animation if the previous one isn't over yet
238         initialAlpha = [self clearFadeAnimation];
239     }
240     if (!_forceDisableAnimation)
241         _fadeAnimation = [[WebWindowFadeAnimation alloc] initWithDuration:0.2 window:_backgroundFullscreenWindow initialAlpha:initialAlpha finalAlpha:fadeIn ? 1 : 0];
242 }
243
244 - (void)enterFullscreen:(NSScreen *)screen
245 {
246     if (!screen)
247         screen = [NSScreen mainScreen];
248
249     NSRect endFrame = [screen frame];
250     NSRect frame = frameExpandedToRatioOfFrame([self videoElementRect], endFrame);
251
252     // Create a black window if needed
253     if (!_backgroundFullscreenWindow)
254         _backgroundFullscreenWindow = createBackgroundFullscreenWindow([screen frame], [[self window] level]-1);
255     else
256         [_backgroundFullscreenWindow setFrame:[screen frame] display:NO];
257
258     [self setupFadeAnimationIfNeededAndFadeIn:YES];
259     if (_forceDisableAnimation) {
260         // This will disable scale animation
261         frame = NSZeroRect;
262     }
263     [[self fullscreenWindow] animateFromRect:frame toRect:endFrame withSubAnimation:_fadeAnimation controllerAction:@selector(windowDidEnterFullscreen)];
264
265     [_backgroundFullscreenWindow orderWindow:NSWindowBelow relativeTo:[[self fullscreenWindow] windowNumber]];
266 }
267
268 - (void)exitFullscreen
269 {
270     if (_isEndingFullscreen)
271         return;
272     _isEndingFullscreen = YES;
273     [_hudController closeWindow];
274
275     NSRect endFrame = [self videoElementRect];
276
277     [self setupFadeAnimationIfNeededAndFadeIn:NO];
278     if (_forceDisableAnimation) {
279         // This will disable scale animation
280         endFrame = NSZeroRect;
281     }
282     
283     // We have to retain ourselves because we want to be alive for the end of the animation.
284     // If our owner releases us we could crash if this is not the case.
285     // Balanced in windowDidExitFullscreen
286     [self retain];    
287
288     NSRect startFrame = [[self window] frame];
289     endFrame = frameExpandedToRatioOfFrame(endFrame, startFrame);
290
291     [[self fullscreenWindow] animateFromRect:startFrame toRect:endFrame withSubAnimation:_fadeAnimation controllerAction:@selector(windowDidExitFullscreen)];
292 }
293
294 - (void)applicationDidChangeScreenParameters:(NSNotification*)notification
295 {
296     UNUSED_PARAM(notification);
297     // The user may have changed the main screen by moving the menu bar, or they may have changed
298     // the Dock's size or location, or they may have changed the fullscreen screen's dimensions.  
299     // Update our presentation parameters, and ensure that the full screen window occupies the 
300     // entire screen:
301     [self updateMenuAndDockForFullscreen];
302     [[self window] setFrame:[[[self window] screen] frame] display:YES];
303 }
304
305 - (void)updateMenuAndDockForFullscreen
306 {
307     // NSApplicationPresentationOptions is available on > 10.6 only:
308     NSApplicationPresentationOptions options = NSApplicationPresentationDefault;
309     NSScreen* fullscreenScreen = [[self window] screen];
310
311     if (!_isEndingFullscreen) {
312         // Auto-hide the menu bar if the fullscreenScreen contains the menu bar:
313         // NOTE: if the fullscreenScreen contains the menu bar but not the dock, we must still 
314         // auto-hide the dock, or an exception will be thrown.
315         if ([[NSScreen screens] objectAtIndex:0] == fullscreenScreen)
316             options |= (NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock);
317         // Check if the current screen contains the dock by comparing the screen's frame to its
318         // visibleFrame; if a dock is present, the visibleFrame will differ. If the current screen
319         // contains the dock, hide it.
320         else if (!NSEqualRects([fullscreenScreen frame], [fullscreenScreen visibleFrame]))
321             options |= NSApplicationPresentationAutoHideDock;
322     }
323
324     NSApp.presentationOptions = options;
325 }
326
327 // MARK: -
328 // MARK: Window callback
329
330 - (void)_requestExit
331 {
332     if (_videoElement)
333         _videoElement->exitFullscreen();
334     _forceDisableAnimation = NO;
335 }
336
337 - (void)requestExitFullscreenWithAnimation:(BOOL)animation
338 {
339     if (_isEndingFullscreen)
340         return;
341
342     _forceDisableAnimation = !animation;
343     [self performSelector:@selector(_requestExit) withObject:nil afterDelay:0];
344
345 }
346
347 - (void)requestExitFullscreen
348 {
349     [self requestExitFullscreenWithAnimation:YES];
350 }
351
352 - (void)fadeHUDIn
353 {
354     [_hudController fadeWindowIn];
355 }
356
357 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
358 {
359     UNUSED_PARAM(object);
360     UNUSED_PARAM(change);
361     UNUSED_PARAM(context);
362
363     if ([keyPath isEqualTo:@"rate"])
364         [self rateChanged:nil];
365 }
366
367 - (void)rateChanged:(NSNotification *)unusedNotification
368 {
369     UNUSED_PARAM(unusedNotification);
370     [_hudController updateRate];
371 }
372
373 @end
374
375 @implementation WebVideoFullscreenWindow
376
377 - (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
378 {
379     UNUSED_PARAM(aStyle);
380     self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];
381     if (!self)
382         return nil;
383     [self setOpaque:NO];
384     [self setBackgroundColor:[NSColor clearColor]];
385     [self setIgnoresMouseEvents:NO];
386     [self setAcceptsMouseMovedEvents:YES];
387     return self;
388 }
389
390 - (void)dealloc
391 {
392     ASSERT(!_fullscreenAnimation);
393     [super dealloc];
394 }
395
396 - (BOOL)resignFirstResponder
397 {
398     return NO;
399 }
400
401 - (BOOL)canBecomeKeyWindow
402 {
403     return NO;
404 }
405
406 - (void)mouseDown:(NSEvent *)event
407 {
408     UNUSED_PARAM(event);
409 }
410
411 - (void)cancelOperation:(id)sender
412 {
413     UNUSED_PARAM(sender);
414     [[self windowController] requestExitFullscreen];
415 }
416
417 - (void)animatedResizeDidEnd
418 {
419     if (_controllerActionOnAnimationEnd)
420         [[self windowController] performSelector:_controllerActionOnAnimationEnd];
421     _controllerActionOnAnimationEnd = NULL;
422 }
423
424 //
425 // This function will animate a change of frame rectangle
426 // We support queuing animation, that means that we'll correctly
427 // interrupt the running animation, and queue the next one.
428 //
429 - (void)animateFromRect:(NSRect)startRect toRect:(NSRect)endRect withSubAnimation:(NSAnimation *)subAnimation controllerAction:(SEL)controllerAction
430 {
431     _controllerActionOnAnimationEnd = controllerAction;
432
433     BOOL wasAnimating = NO;
434     if (_fullscreenAnimation) {
435         wasAnimating = YES;
436
437         // Interrupt any running animation.
438         [_fullscreenAnimation stopAnimation];
439
440         // Save the current rect to ensure a smooth transition.
441         startRect = [_fullscreenAnimation currentFrame];
442         [_fullscreenAnimation release];
443         _fullscreenAnimation = nil;
444     }
445     
446     if (NSIsEmptyRect(startRect) || NSIsEmptyRect(endRect)) {
447         // Fakely end the subanimation.
448         [subAnimation setCurrentProgress:1];
449         // And remove the weak link to the window.
450         [subAnimation stopAnimation];
451
452         [self setFrame:endRect display:NO];
453         [self makeKeyAndOrderFront:self];
454         [self animatedResizeDidEnd];
455         return;
456     }
457
458     if (!wasAnimating) {
459         // We'll downscale the window during the animation based on the higher resolution rect
460         BOOL higherResolutionIsEndRect = startRect.size.width < endRect.size.width && startRect.size.height < endRect.size.height;
461         [self setFrame:higherResolutionIsEndRect ? endRect : startRect display:NO];
462     }
463     
464     ASSERT(!_fullscreenAnimation);
465     _fullscreenAnimation = [[WebWindowScaleAnimation alloc] initWithHintedDuration:0.2 window:self initalFrame:startRect finalFrame:endRect];
466     [_fullscreenAnimation setSubAnimation:subAnimation];
467     [_fullscreenAnimation setDelegate:self];
468     
469     // Make sure the animation has scaled the window before showing it.
470     [_fullscreenAnimation setCurrentProgress:0];
471     [self makeKeyAndOrderFront:self];
472
473     [_fullscreenAnimation startAnimation];
474 }
475
476 - (void)animationDidEnd:(NSAnimation *)animation
477 {
478     if (![NSThread isMainThread]) {
479         [self performSelectorOnMainThread:@selector(animationDidEnd:) withObject:animation waitUntilDone:NO];
480         return;
481     }
482     if (animation != _fullscreenAnimation)
483         return;
484
485     // The animation is not really over and was interrupted
486     // Don't send completion events.
487     if ([animation currentProgress] < 1.0)
488         return;
489
490     // Ensure that animation (and subanimation) don't keep
491     // the weak reference to the window ivar that may be destroyed from
492     // now on.
493     [_fullscreenAnimation setWindow:nil];
494
495     [_fullscreenAnimation autorelease];
496     _fullscreenAnimation = nil;
497
498     [self animatedResizeDidEnd];
499 }
500
501 - (void)mouseMoved:(NSEvent *)event
502 {
503     UNUSED_PARAM(event);
504     [[self windowController] fadeHUDIn];
505 }
506
507 @end
508
509 ALLOW_DEPRECATED_DECLARATIONS_END
510
511 #endif