2 * Copyright (C) 2016 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 #import "WebVideoFullscreenInterfaceMac.h"
29 #if PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE)
34 #import "MediaTimeAVFoundation.h"
36 #import "TimeRanges.h"
37 #import "WebPlaybackSessionInterfaceMac.h"
38 #import "WebVideoFullscreenChangeObserver.h"
39 #import "WebVideoFullscreenModel.h"
40 #import <AVFoundation/AVTime.h>
42 #import "CoreMediaSoftLink.h"
44 SOFT_LINK_FRAMEWORK_OPTIONAL(AVKit)
45 SOFT_LINK_CLASS_OPTIONAL(AVKit, AVValueTiming)
47 SOFT_LINK_PRIVATE_FRAMEWORK_OPTIONAL(PIP)
48 SOFT_LINK_CLASS_OPTIONAL(PIP, PIPViewController)
50 using namespace WebCore;
52 @class WebVideoViewContainer;
54 @protocol WebVideoViewContainerDelegate <NSObject>
56 - (void)boundsDidChangeForVideoViewContainer:(WebVideoViewContainer *)videoViewContainer;
57 - (void)superviewDidChangeForVideoViewContainer:(WebVideoViewContainer *)videoViewContainer;
61 @interface WebVideoViewContainer : NSView {
62 id <WebVideoViewContainerDelegate> _videoViewContainerDelegate;
65 @property (nonatomic, assign) id <WebVideoViewContainerDelegate> videoViewContainerDelegate;
69 @implementation WebVideoViewContainer
71 @synthesize videoViewContainerDelegate=_videoViewContainerDelegate;
73 - (void)resizeWithOldSuperviewSize:(NSSize)oldBoundsSize
75 [super resizeWithOldSuperviewSize:oldBoundsSize];
77 [_videoViewContainerDelegate boundsDidChangeForVideoViewContainer:self];
80 - (void)viewDidMoveToSuperview
82 [super viewDidMoveToSuperview];
84 [_videoViewContainerDelegate superviewDidChangeForVideoViewContainer:self];
95 @interface WebVideoFullscreenInterfaceMacObjC : NSObject <PIPViewControllerDelegate, WebVideoViewContainerDelegate> {
96 WebCore::WebVideoFullscreenInterfaceMac* _webVideoFullscreenInterfaceMac;
97 NSSize _videoDimensions;
98 RetainPtr<PIPViewController> _pipViewController;
99 RetainPtr<NSViewController> _videoViewContainerController;
100 RetainPtr<WebVideoViewContainer> _videoViewContainer;
102 RetainPtr<NSWindow> _returningWindow;
103 NSRect _returningRect;
105 BOOL _didRequestExitingPIP;
106 BOOL _exitingToStandardFullscreen;
109 - (instancetype)initWithWebVideoFullscreenInterfaceMac:(WebCore::WebVideoFullscreenInterfaceMac*)webVideoFullscreenInterfaceMac;
110 - (void)invalidateFullscreenState;
113 // Tracking video playback state
114 @property (nonatomic) NSSize videoDimensions;
115 @property (nonatomic, getter=isPlaying) BOOL playing;
116 - (void)updateIsPlaying:(BOOL)isPlaying newPlaybackRate:(float)playbackRate;
118 // Handling PIP transitions
119 @property (nonatomic, readonly) BOOL didRequestExitingPIP;
120 @property (nonatomic, getter=isExitingToStandardFullscreen) BOOL exitingToStandardFullscreen;
122 - (void)setUpPIPForVideoView:(NSView *)videoView withFrame:(NSRect)frame inWindow:(NSWindow *)window;
125 - (void)exitPIPAnimatingToRect:(NSRect)rect inWindow:(NSWindow *)window;
129 @implementation WebVideoFullscreenInterfaceMacObjC
131 @synthesize playing=_playing;
132 @synthesize videoDimensions=_videoDimensions;
133 @synthesize didRequestExitingPIP=_didRequestExitingPIP;
134 @synthesize exitingToStandardFullscreen=_exitingToStandardFullscreen;
136 - (instancetype)initWithWebVideoFullscreenInterfaceMac:(WebCore::WebVideoFullscreenInterfaceMac*)webVideoFullscreenInterfaceMac
138 if (!(self = [super init]))
141 _webVideoFullscreenInterfaceMac = webVideoFullscreenInterfaceMac;
142 _pipState = PIPState::NotInPIP;
147 - (void)invalidateFullscreenState
149 [_pipViewController setDelegate:nil];
150 _pipViewController = nil;
151 [_videoViewContainer removeFromSuperview];
152 [_videoViewContainer setVideoViewContainerDelegate:nil];
153 _videoViewContainer = nil;
154 _videoViewContainerController = nil;
155 _pipState = PIPState::NotInPIP;
156 _didRequestExitingPIP = NO;
157 _exitingToStandardFullscreen = NO;
158 _returningWindow = nil;
159 _returningRect = NSZeroRect;
164 [self invalidateFullscreenState];
165 _webVideoFullscreenInterfaceMac = nullptr;
166 _videoDimensions = NSZeroSize;
169 - (void)updateIsPlaying:(BOOL)isPlaying newPlaybackRate:(float)playbackRate
171 _playing = isPlaying && playbackRate;
173 if ([_pipViewController respondsToSelector:@selector(setPlaying:)])
174 [_pipViewController setPlaying:_playing];
177 - (void)setVideoDimensions:(NSSize)videoDimensions
179 _videoDimensions = videoDimensions;
181 if ([_pipViewController respondsToSelector:@selector(setAspectRatio:)])
182 [_pipViewController setAspectRatio:_videoDimensions];
185 - (void)setUpPIPForVideoView:(NSView *)videoView withFrame:(NSRect)frame inWindow:(NSWindow *)window
187 ASSERT(!_pipViewController);
188 ASSERT(!_videoViewContainerController);
189 ASSERT(!_videoViewContainer);
191 _pipViewController = adoptNS([[getPIPViewControllerClass() alloc] init]);
192 [_pipViewController setDelegate:self];
193 if ([_pipViewController respondsToSelector:@selector(setUserCanResize:)])
194 [_pipViewController setUserCanResize:YES];
195 if ([_pipViewController respondsToSelector:@selector(setPlaying:)])
196 [_pipViewController setPlaying:_playing];
197 [self setVideoDimensions:NSEqualSizes(_videoDimensions, NSZeroSize) ? frame.size : _videoDimensions];
198 if (_webVideoFullscreenInterfaceMac && _webVideoFullscreenInterfaceMac->webVideoFullscreenModel())
199 _webVideoFullscreenInterfaceMac->webVideoFullscreenModel()->setVideoLayerGravity(WebVideoFullscreenModel::VideoGravityResizeAspectFill);
201 _videoViewContainer = adoptNS([[WebVideoViewContainer alloc] initWithFrame:frame]);
202 [_videoViewContainer setVideoViewContainerDelegate:self];
203 [_videoViewContainer addSubview:videoView];
204 videoView.frame = [_videoViewContainer bounds];
205 videoView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
207 _videoViewContainerController = adoptNS([[NSViewController alloc] init]);
208 [_videoViewContainerController setView:_videoViewContainer.get()];
209 [window.contentView addSubview:_videoViewContainer.get() positioned:NSWindowAbove relativeTo:nil];
214 if (_pipState == PIPState::InPIP)
217 [_videoViewContainerController view].layer.backgroundColor = CGColorGetConstantColor(kCGColorBlack);
218 [_pipViewController presentViewControllerAsPictureInPicture:_videoViewContainerController.get()];
219 _pipState = PIPState::InPIP;
224 if (_pipState != PIPState::InPIP || !_pipViewController || !_videoViewContainerController)
227 _didRequestExitingPIP = YES;
228 [_videoViewContainerController view].layer.backgroundColor = CGColorGetConstantColor(kCGColorClear);
229 [_pipViewController dismissViewController:_videoViewContainerController.get()];
230 _pipState = PIPState::ExitingPIP;
233 - (void)exitPIPAnimatingToRect:(NSRect)rect inWindow:(NSWindow *)window
235 _returningWindow = window;
236 _returningRect = rect;
238 [_pipViewController setReplacementRect:rect];
239 [_pipViewController setReplacementWindow:window];
244 // WebVideoViewContainerDelegate
246 - (void)boundsDidChangeForVideoViewContainer:(WebVideoViewContainer *)videoViewContainer
248 if (!_videoViewContainer || !_pipViewController)
251 ASSERT_UNUSED(videoViewContainer, videoViewContainer == _videoViewContainer);
253 if (_webVideoFullscreenInterfaceMac && _webVideoFullscreenInterfaceMac->webVideoFullscreenModel())
254 _webVideoFullscreenInterfaceMac->webVideoFullscreenModel()->setVideoLayerFrame([_videoViewContainer bounds]);
257 - (void)superviewDidChangeForVideoViewContainer:(WebVideoViewContainer *)videoViewContainer
259 if (!_videoViewContainer || !_pipViewController)
262 ASSERT(videoViewContainer == _videoViewContainer);
264 if (![videoViewContainer isDescendantOf:[_pipViewController view]])
267 // Once the view is moved into the pip view, make sure it resizes with the pip view.
268 videoViewContainer.frame = [videoViewContainer superview].bounds;
269 videoViewContainer.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
272 // PIPViewControllerDelegate
274 - (BOOL)pipShouldClose:(PIPViewController *)pip
276 ASSERT_UNUSED(pip, pip == _pipViewController);
278 if (!_webVideoFullscreenInterfaceMac || !_webVideoFullscreenInterfaceMac->webVideoFullscreenChangeObserver())
281 _didRequestExitingPIP = YES;
282 _webVideoFullscreenInterfaceMac->webVideoFullscreenChangeObserver()->fullscreenMayReturnToInline();
287 - (void)pipDidClose:(PIPViewController *)pip
289 ASSERT_UNUSED(pip, pip == _pipViewController);
291 if (_webVideoFullscreenInterfaceMac && _webVideoFullscreenInterfaceMac->webVideoFullscreenModel() && _videoViewContainer && _returningWindow && !NSEqualRects(_returningRect, NSZeroRect)) {
292 [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
293 context.allowsImplicitAnimation = NO;
294 [_videoViewContainer setFrame:_returningRect];
295 _webVideoFullscreenInterfaceMac->webVideoFullscreenModel()->setVideoLayerFrame([_videoViewContainer bounds]);
296 _webVideoFullscreenInterfaceMac->webVideoFullscreenModel()->setVideoLayerGravity(WebVideoFullscreenModel::VideoGravityResizeAspect);
298 [[_returningWindow contentView] addSubview:_videoViewContainer.get() positioned:NSWindowAbove relativeTo:nil];
299 } completionHandler:nil];
302 if (_webVideoFullscreenInterfaceMac) {
303 if (!self.isExitingToStandardFullscreen) {
304 if (WebVideoFullscreenModel* videoFullscreenModel = _webVideoFullscreenInterfaceMac->webVideoFullscreenModel()) {
305 videoFullscreenModel->requestFullscreenMode(HTMLMediaElementEnums::VideoFullscreenModeNone);
306 videoFullscreenModel->setVideoLayerGravity(WebVideoFullscreenModel::VideoGravityResizeAspect);
310 _webVideoFullscreenInterfaceMac->clearMode(HTMLMediaElementEnums::VideoFullscreenModePictureInPicture);
312 if (WebVideoFullscreenChangeObserver* fullscreenChangeObserver = _webVideoFullscreenInterfaceMac->webVideoFullscreenChangeObserver())
313 fullscreenChangeObserver->didExitFullscreen();
317 - (void)pipActionPlay:(PIPViewController *)pip
319 ASSERT_UNUSED(pip, pip == _pipViewController);
321 if (_webVideoFullscreenInterfaceMac && _webVideoFullscreenInterfaceMac->webPlaybackSessionModel())
322 _webVideoFullscreenInterfaceMac->webPlaybackSessionModel()->play();
325 - (void)pipActionPause:(PIPViewController *)pip
327 ASSERT_UNUSED(pip, pip == _pipViewController);
329 if (_webVideoFullscreenInterfaceMac && _webVideoFullscreenInterfaceMac->webPlaybackSessionModel())
330 _webVideoFullscreenInterfaceMac->webPlaybackSessionModel()->pause();
333 - (void)pipActionStop:(PIPViewController *)pip
335 ASSERT_UNUSED(pip, pip == _pipViewController);
337 if (!_webVideoFullscreenInterfaceMac)
340 if (WebPlaybackSessionModel* playbackSessionModel = _webVideoFullscreenInterfaceMac->webPlaybackSessionModel())
341 playbackSessionModel->pause();
343 // FIXME 25096170: Should animate only if the page with the video is unobscured. For now, always close without animation.
351 WebVideoFullscreenInterfaceMac::WebVideoFullscreenInterfaceMac(WebPlaybackSessionInterfaceMac& playbackSessionInterface)
352 : m_playbackSessionInterface(playbackSessionInterface)
354 m_playbackSessionInterface->setClient(this);
357 WebVideoFullscreenInterfaceMac::~WebVideoFullscreenInterfaceMac()
359 if (m_playbackSessionInterface->client() == this)
360 m_playbackSessionInterface->setClient(nullptr);
363 void WebVideoFullscreenInterfaceMac::setWebVideoFullscreenModel(WebVideoFullscreenModel* model)
365 m_videoFullscreenModel = model;
368 void WebVideoFullscreenInterfaceMac::setWebVideoFullscreenChangeObserver(WebVideoFullscreenChangeObserver* observer)
370 m_fullscreenChangeObserver = observer;
373 void WebVideoFullscreenInterfaceMac::setMode(HTMLMediaElementEnums::VideoFullscreenMode mode)
375 HTMLMediaElementEnums::VideoFullscreenMode newMode = m_mode | mode;
376 if (m_mode == newMode)
380 if (m_videoFullscreenModel)
381 m_videoFullscreenModel->fullscreenModeChanged(m_mode);
384 void WebVideoFullscreenInterfaceMac::clearMode(HTMLMediaElementEnums::VideoFullscreenMode mode)
386 HTMLMediaElementEnums::VideoFullscreenMode newMode = m_mode & ~mode;
387 if (m_mode == newMode)
391 if (m_videoFullscreenModel)
392 m_videoFullscreenModel->fullscreenModeChanged(m_mode);
395 void WebVideoFullscreenInterfaceMac::setDuration(double duration)
397 m_playbackSessionInterface->setDuration(duration);
400 void WebVideoFullscreenInterfaceMac::setCurrentTime(double currentTime, double anchorTime)
402 m_playbackSessionInterface->setCurrentTime(currentTime, anchorTime);
405 void WebVideoFullscreenInterfaceMac::setRate(bool isPlaying, float playbackRate)
407 m_playbackSessionInterface->setRate(isPlaying, playbackRate);
410 void WebVideoFullscreenInterfaceMac::rateChanged(bool isPlaying, float playbackRate)
412 [videoFullscreenInterfaceObjC() updateIsPlaying:isPlaying newPlaybackRate:playbackRate];
415 void WebVideoFullscreenInterfaceMac::setSeekableRanges(const TimeRanges& timeRanges)
417 m_playbackSessionInterface->setSeekableRanges(timeRanges);
420 void WebVideoFullscreenInterfaceMac::setAudioMediaSelectionOptions(const Vector<WTF::String>& options, uint64_t selectedIndex)
422 m_playbackSessionInterface->setAudioMediaSelectionOptions(options, selectedIndex);
425 void WebVideoFullscreenInterfaceMac::setLegibleMediaSelectionOptions(const Vector<WTF::String>& options, uint64_t selectedIndex)
427 m_playbackSessionInterface->setLegibleMediaSelectionOptions(options, selectedIndex);
430 void WebVideoFullscreenInterfaceMac::ensureControlsManager()
432 m_playbackSessionInterface->ensureControlsManager();
435 WebVideoFullscreenInterfaceMacObjC *WebVideoFullscreenInterfaceMac::videoFullscreenInterfaceObjC()
437 if (!m_webVideoFullscreenInterfaceObjC)
438 m_webVideoFullscreenInterfaceObjC = adoptNS([[WebVideoFullscreenInterfaceMacObjC alloc] initWithWebVideoFullscreenInterfaceMac:this]);
440 return m_webVideoFullscreenInterfaceObjC.get();
443 void WebVideoFullscreenInterfaceMac::setupFullscreen(NSView& layerHostedView, const IntRect& initialRect, NSWindow *parentWindow, HTMLMediaElementEnums::VideoFullscreenMode mode, bool allowsPictureInPicturePlayback)
445 LOG(Fullscreen, "WebVideoFullscreenInterfaceMac::setupFullscreen(%p), initialRect:{%d, %d, %d, %d}, parentWindow:%p, mode:%d", this, initialRect.x(), initialRect.y(), initialRect.width(), initialRect.height(), parentWindow, mode);
447 UNUSED_PARAM(allowsPictureInPicturePlayback);
448 ASSERT(mode == HTMLMediaElementEnums::VideoFullscreenModePictureInPicture);
452 [videoFullscreenInterfaceObjC() setUpPIPForVideoView:&layerHostedView withFrame:(NSRect)initialRect inWindow:parentWindow];
454 RefPtr<WebVideoFullscreenInterfaceMac> protectedThis(this);
455 dispatch_async(dispatch_get_main_queue(), [protectedThis, this] {
456 if (m_fullscreenChangeObserver)
457 m_fullscreenChangeObserver->didSetupFullscreen();
461 void WebVideoFullscreenInterfaceMac::enterFullscreen()
463 LOG(Fullscreen, "WebVideoFullscreenInterfaceMac::enterFullscreen(%p)", this);
465 if (mode() == HTMLMediaElementEnums::VideoFullscreenModePictureInPicture) {
466 [m_webVideoFullscreenInterfaceObjC enterPIP];
468 if (m_fullscreenChangeObserver)
469 m_fullscreenChangeObserver->didEnterFullscreen();
473 void WebVideoFullscreenInterfaceMac::exitFullscreen(const IntRect& finalRect, NSWindow *parentWindow)
475 LOG(Fullscreen, "WebVideoFullscreenInterfaceMac::exitFullscreen(%p), finalRect:{%d, %d, %d, %d}, parentWindow:%p", this, finalRect.x(), finalRect.y(), finalRect.width(), finalRect.height(), parentWindow);
477 if ([m_webVideoFullscreenInterfaceObjC didRequestExitingPIP])
480 if (finalRect.isEmpty())
481 [m_webVideoFullscreenInterfaceObjC exitPIP];
483 [m_webVideoFullscreenInterfaceObjC exitPIPAnimatingToRect:finalRect inWindow:parentWindow];
486 void WebVideoFullscreenInterfaceMac::exitFullscreenWithoutAnimationToMode(HTMLMediaElementEnums::VideoFullscreenMode mode)
488 LOG(Fullscreen, "WebVideoFullscreenInterfaceMac::exitFullscreenWithoutAnimationToMode(%p), mode:%d", this, mode);
490 if ([m_webVideoFullscreenInterfaceObjC didRequestExitingPIP])
493 bool isExitingToStandardFullscreen = mode == HTMLMediaElementEnums::VideoFullscreenModeStandard;
494 // On Mac, standard fullscreen is handled by the Fullscreen API and not by WebVideoFullscreenManager.
495 // Just update m_mode directly to HTMLMediaElementEnums::VideoFullscreenModeStandard in that case to keep
496 // m_mode in sync with the fullscreen mode in HTMLMediaElement.
497 if (isExitingToStandardFullscreen)
498 m_mode = HTMLMediaElementEnums::VideoFullscreenModeStandard;
500 [m_webVideoFullscreenInterfaceObjC setExitingToStandardFullscreen:isExitingToStandardFullscreen];
501 [m_webVideoFullscreenInterfaceObjC exitPIP];
504 void WebVideoFullscreenInterfaceMac::cleanupFullscreen()
506 LOG(Fullscreen, "WebVideoFullscreenInterfaceMac::cleanupFullscreen(%p)", this);
508 [m_webVideoFullscreenInterfaceObjC exitPIP];
509 [m_webVideoFullscreenInterfaceObjC invalidateFullscreenState];
511 if (m_fullscreenChangeObserver)
512 m_fullscreenChangeObserver->didCleanupFullscreen();
515 void WebVideoFullscreenInterfaceMac::invalidate()
517 LOG(Fullscreen, "WebVideoFullscreenInterfaceMac::invalidate(%p)", this);
519 m_videoFullscreenModel = nil;
520 m_fullscreenChangeObserver = nil;
524 [m_webVideoFullscreenInterfaceObjC invalidate];
525 m_webVideoFullscreenInterfaceObjC = nil;
529 static const char* boolString(bool val)
531 return val ? "true" : "false";
535 void WebVideoFullscreenInterfaceMac::preparedToReturnToInline(bool visible, const IntRect& inlineRect, NSWindow *parentWindow)
537 LOG(Fullscreen, "WebVideoFullscreenInterfaceMac::preparedToReturnToInline(%p), visible:%s, inlineRect:{%d, %d, %d, %d}, parentWindow:%p", this, boolString(visible), inlineRect.x(), inlineRect.y(), inlineRect.width(), inlineRect.height(), parentWindow);
540 [m_webVideoFullscreenInterfaceObjC exitPIP];
544 ASSERT(parentWindow);
545 [m_webVideoFullscreenInterfaceObjC exitPIPAnimatingToRect:(NSRect)inlineRect inWindow:parentWindow];
548 void WebVideoFullscreenInterfaceMac::setExternalPlayback(bool enabled, ExternalPlaybackTargetType, String)
550 LOG(Fullscreen, "WebVideoFullscreenInterfaceMac::setExternalPlayback(%p), enabled:%s", this, boolString(enabled));
552 if (enabled && m_mode == HTMLMediaElementEnums::VideoFullscreenModePictureInPicture)
553 exitFullscreen(IntRect(), nil);
556 void WebVideoFullscreenInterfaceMac::setVideoDimensions(bool hasVideo, float width, float height)
558 LOG(Fullscreen, "WebVideoFullscreenInterfaceMac::setVideoDimensions(%p), hasVideo:%s, width:%.0f, height:%.0f", this, boolString(hasVideo), width, height);
561 exitFullscreenWithoutAnimationToMode(HTMLMediaElementEnums::VideoFullscreenModeNone);
565 // Width and height can be zero when we are transitioning from one video to another. Ignore zero values.
567 [m_webVideoFullscreenInterfaceObjC setVideoDimensions:NSMakeSize(width, height)];
570 bool WebVideoFullscreenInterfaceMac::isPlayingVideoInEnhancedFullscreen() const
572 return hasMode(WebCore::HTMLMediaElementEnums::VideoFullscreenModePictureInPicture) && [m_webVideoFullscreenInterfaceObjC isPlaying];
575 bool supportsPictureInPicture()
577 return PIPLibrary() && getPIPViewControllerClass();