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