/* * Copyright (C) 2016-2021 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 "config.h" #import "WebPlaybackControlsManager.h" #if PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE) && ENABLE(WEB_PLAYBACK_CONTROLS_MANAGER) #import "MediaSelectionOption.h" #import "PlaybackSessionInterfaceMac.h" #import "PlaybackSessionModel.h" #import #import #import IGNORE_WARNINGS_BEGIN("nullability-completeness") SOFT_LINK_FRAMEWORK(AVKit) SOFT_LINK_CLASS_OPTIONAL(AVKit, AVTouchBarMediaSelectionOption) using WebCore::MediaSelectionOption; using WebCore::PlaybackSessionInterfaceMac; @implementation WebPlaybackControlsManager @synthesize seekToTime = _seekToTime; @synthesize hasEnabledAudio = _hasEnabledAudio; @synthesize hasEnabledVideo = _hasEnabledVideo; @synthesize canTogglePlayback = _canTogglePlayback; @synthesize allowsPictureInPicturePlayback; @synthesize pictureInPictureActive; @synthesize canTogglePictureInPicture; - (void)dealloc { if (_playbackSessionInterfaceMac) _playbackSessionInterfaceMac->setPlayBackControlsManager(nullptr); [super dealloc]; } - (BOOL)canSeek { return _canSeek; } - (void)setCanSeek:(BOOL)canSeek { _canSeek = canSeek; } + (NSSet *)keyPathsForValuesAffectingContentDuration { return [NSSet setWithObject:@"seekableTimeRanges"]; } - (NSTimeInterval)contentDuration { return [_seekableTimeRanges count] ? _contentDuration : std::numeric_limits::infinity(); } - (void)setContentDuration:(NSTimeInterval)duration { bool needCanSeekUpdate = std::isfinite(_contentDuration) != std::isfinite(duration); _contentDuration = duration; // Workaround rdar://82275552. We do so by toggling the canSeek property to force // a content refresh that will make the scrubber appear/disappear accordingly. if (needCanSeekUpdate) { bool canSeek = _canSeek; self.canSeek = !canSeek; self.canSeek = canSeek; } } - (AVValueTiming *)timing { return _timing.get(); } - (void)setTiming:(AVValueTiming *)timing { _timing = timing; } - (NSArray *)seekableTimeRanges { return _seekableTimeRanges.get(); } - (void)setSeekableTimeRanges:(NSArray *)timeRanges { _seekableTimeRanges = timeRanges; self.canSeek = timeRanges.count; } - (BOOL)isSeeking { return NO; } - (void)seekToTime:(NSTimeInterval)time toleranceBefore:(NSTimeInterval)toleranceBefore toleranceAfter:(NSTimeInterval)toleranceAfter { if (!_playbackSessionInterfaceMac) return; if (auto* model = _playbackSessionInterfaceMac->playbackSessionModel()) model->sendRemoteCommand(WebCore::PlatformMediaSession::RemoteControlCommandType::SeekToPlaybackPositionCommand, { time, toleranceBefore || toleranceAfter }); } - (void)cancelThumbnailAndAudioAmplitudeSampleGeneration { } - (void)generateTouchBarThumbnailsForTimes:(NSArray *)thumbnailTimes tolerance:(NSTimeInterval)tolerance size:(NSSize)size thumbnailHandler:(void (^)(NSArray *thumbnails, BOOL thumbnailGenerationFailed))thumbnailHandler { UNUSED_PARAM(thumbnailTimes); UNUSED_PARAM(tolerance); UNUSED_PARAM(size); thumbnailHandler(@[ ], YES); } - (void)generateTouchBarAudioAmplitudeSamples:(NSInteger)numberOfSamples completionHandler:(void (^)(NSArray *audioAmplitudeSamples))completionHandler { UNUSED_PARAM(numberOfSamples); completionHandler(@[ ]); } + (NSSet *)keyPathsForValuesAffectingCanBeginTouchBarScrubbing { return [NSSet setWithObjects:@"canSeek", @"contentDuration", nil]; } - (BOOL)canBeginTouchBarScrubbing { // At this time, we return YES for all media that is not a live stream and media that is not Netflix. (A Netflix // quirk means we pretend Netflix is a live stream for Touch Bar.) It's not ideal to return YES all the time for // other media. The intent of the API is that we return NO when the media is being scrubbed via the on-screen scrubber. // But we can only possibly get the right answer for media that uses the default controls. return _canSeek && std::isfinite(_contentDuration); } - (void)beginTouchBarScrubbing { if (!_playbackSessionInterfaceMac) return; auto* model = _playbackSessionInterfaceMac->playbackSessionModel(); if (!model) return; _playbackSessionInterfaceMac->willBeginScrubbing(); model->sendRemoteCommand(WebCore::PlatformMediaSession::RemoteControlCommandType::BeginScrubbingCommand, { }); } - (void)endTouchBarScrubbing { if (!_playbackSessionInterfaceMac) return; if (auto* model = _playbackSessionInterfaceMac->playbackSessionModel()) model->sendRemoteCommand(WebCore::PlatformMediaSession::RemoteControlCommandType::EndScrubbingCommand, { }); } - (NSArray *)audioTouchBarMediaSelectionOptions { return _audioTouchBarMediaSelectionOptions.get(); } - (void)setAudioTouchBarMediaSelectionOptions:(NSArray *)audioOptions { _audioTouchBarMediaSelectionOptions = audioOptions; } - (AVTouchBarMediaSelectionOption *)currentAudioTouchBarMediaSelectionOption { return _currentAudioTouchBarMediaSelectionOption.get(); } - (void)setCurrentAudioTouchBarMediaSelectionOption:(AVTouchBarMediaSelectionOption *)audioMediaSelectionOption { if (audioMediaSelectionOption == _currentAudioTouchBarMediaSelectionOption) return; _currentAudioTouchBarMediaSelectionOption = audioMediaSelectionOption; NSInteger index = NSNotFound; if (audioMediaSelectionOption && _audioTouchBarMediaSelectionOptions) index = [_audioTouchBarMediaSelectionOptions indexOfObject:audioMediaSelectionOption]; if (auto* model = _playbackSessionInterfaceMac->playbackSessionModel()) model->selectAudioMediaOption(index != NSNotFound ? index : UINT64_MAX); } - (NSArray *)legibleTouchBarMediaSelectionOptions { return _legibleTouchBarMediaSelectionOptions.get(); } - (void)setLegibleTouchBarMediaSelectionOptions:(NSArray *)legibleOptions { _legibleTouchBarMediaSelectionOptions = legibleOptions; } - (AVTouchBarMediaSelectionOption *)currentLegibleTouchBarMediaSelectionOption { return _currentLegibleTouchBarMediaSelectionOption.get(); } - (void)setCurrentLegibleTouchBarMediaSelectionOption:(AVTouchBarMediaSelectionOption *)legibleMediaSelectionOption { if (legibleMediaSelectionOption == _currentLegibleTouchBarMediaSelectionOption) return; _currentLegibleTouchBarMediaSelectionOption = legibleMediaSelectionOption; NSInteger index = NSNotFound; if (legibleMediaSelectionOption && _legibleTouchBarMediaSelectionOptions) index = [_legibleTouchBarMediaSelectionOptions indexOfObject:legibleMediaSelectionOption]; if (auto* model = _playbackSessionInterfaceMac->playbackSessionModel()) model->selectLegibleMediaOption(index != NSNotFound ? index : UINT64_MAX); } static AVTouchBarMediaSelectionOptionType toAVTouchBarMediaSelectionOptionType(MediaSelectionOption::LegibleType type) { switch (type) { case MediaSelectionOption::LegibleType::Regular: return AVTouchBarMediaSelectionOptionTypeRegular; case MediaSelectionOption::LegibleType::LegibleOff: return AVTouchBarMediaSelectionOptionTypeLegibleOff; case MediaSelectionOption::LegibleType::LegibleAuto: return AVTouchBarMediaSelectionOptionTypeLegibleAuto; } ASSERT_NOT_REACHED(); return AVTouchBarMediaSelectionOptionTypeRegular; } static RetainPtr mediaSelectionOptions(const Vector& options) { return createNSArray(options, [] (auto& option) { return adoptNS([allocAVTouchBarMediaSelectionOptionInstance() initWithTitle:option.displayName type:toAVTouchBarMediaSelectionOptionType(option.legibleType)]); }); } - (void)setAudioMediaSelectionOptions:(const Vector&)options withSelectedIndex:(NSUInteger)selectedIndex { auto webOptions = mediaSelectionOptions(options); [self setAudioTouchBarMediaSelectionOptions:webOptions.get()]; if (selectedIndex < [webOptions count]) [self setCurrentAudioTouchBarMediaSelectionOption:[webOptions objectAtIndex:selectedIndex]]; } - (void)setLegibleMediaSelectionOptions:(const Vector&)options withSelectedIndex:(NSUInteger)selectedIndex { auto webOptions = mediaSelectionOptions(options); [self setLegibleTouchBarMediaSelectionOptions:webOptions.get()]; if (selectedIndex < [webOptions count]) [self setCurrentLegibleTouchBarMediaSelectionOption:[webOptions objectAtIndex:selectedIndex]]; } - (void)setAudioMediaSelectionIndex:(NSUInteger)selectedIndex { if (selectedIndex >= [_audioTouchBarMediaSelectionOptions count]) return; [self willChangeValueForKey:@"currentAudioTouchBarMediaSelectionOption"]; _currentAudioTouchBarMediaSelectionOption = [_audioTouchBarMediaSelectionOptions objectAtIndex:selectedIndex]; [self didChangeValueForKey:@"currentAudioTouchBarMediaSelectionOption"]; } - (void)setLegibleMediaSelectionIndex:(NSUInteger)selectedIndex { if (selectedIndex >= [_legibleTouchBarMediaSelectionOptions count]) return; [self willChangeValueForKey:@"currentLegibleTouchBarMediaSelectionOption"]; _currentLegibleTouchBarMediaSelectionOption = [_legibleTouchBarMediaSelectionOptions objectAtIndex:selectedIndex]; [self didChangeValueForKey:@"currentLegibleTouchBarMediaSelectionOption"]; } - (PlaybackSessionInterfaceMac*)playbackSessionInterfaceMac { return _playbackSessionInterfaceMac.get(); } - (void)setPlaybackSessionInterfaceMac:(PlaybackSessionInterfaceMac*)playbackSessionInterfaceMac { if (_playbackSessionInterfaceMac == playbackSessionInterfaceMac) return; if (_playbackSessionInterfaceMac) _playbackSessionInterfaceMac->setPlayBackControlsManager(nullptr); _playbackSessionInterfaceMac = playbackSessionInterfaceMac; if (_playbackSessionInterfaceMac) _playbackSessionInterfaceMac->setPlayBackControlsManager(self); } - (void)togglePlayback { if (!_playbackSessionInterfaceMac) return; if (auto* model = _playbackSessionInterfaceMac->playbackSessionModel()) model->sendRemoteCommand(WebCore::PlatformMediaSession::RemoteControlCommandType::TogglePlayPauseCommand, { }); } - (void)setPlaying:(BOOL)playing { if (playing != _playing) { [self willChangeValueForKey:@"playing"]; _playing = playing; [self didChangeValueForKey:@"playing"]; } if (!_playbackSessionInterfaceMac) return; if (auto* model = _playbackSessionInterfaceMac->playbackSessionModel()) { BOOL isCurrentlyPlaying = model->isPlaying(); if (!isCurrentlyPlaying && _playing) model->sendRemoteCommand(WebCore::PlatformMediaSession::RemoteControlCommandType::PlayCommand, { }); else if (isCurrentlyPlaying && !_playing) model->sendRemoteCommand(WebCore::PlatformMediaSession::RemoteControlCommandType::PauseCommand, { }); } } - (BOOL)isPlaying { return _playing; } - (double)defaultPlaybackRate { return _defaultPlaybackRate; } - (void)setDefaultPlaybackRate:(double)defaultPlaybackRate { [self setDefaultPlaybackRate:defaultPlaybackRate fromJavaScript:NO]; } - (void)setDefaultPlaybackRate:(double)defaultPlaybackRate fromJavaScript:(BOOL)fromJavaScript { if (defaultPlaybackRate == _defaultPlaybackRate) return; _defaultPlaybackRate = defaultPlaybackRate; if (!fromJavaScript && _playbackSessionInterfaceMac) { if (auto* model = _playbackSessionInterfaceMac->playbackSessionModel(); model && model->defaultPlaybackRate() != _defaultPlaybackRate) model->setDefaultPlaybackRate(_defaultPlaybackRate); } if ([self isPlaying]) [self setRate:_defaultPlaybackRate fromJavaScript:fromJavaScript]; } - (float)rate { return _rate; } - (void)setRate:(float)rate { [self setRate:rate fromJavaScript:NO]; } - (void)setRate:(double)rate fromJavaScript:(BOOL)fromJavaScript { if (rate == _rate) return; _rate = rate; // AVKit doesn't have a separate variable for "paused", instead representing it by a `rate` of // `0`. Unfortunately, `HTMLMediaElement::play` doesn't call `HTMLMediaElement::setPlaybackRate` // so if we propagate a `rate` of `0` along to the `HTMLMediaElement` then any attempt to // `HTMLMediaElement::play` will effectively be a no-op since the `playbackRate` will be `0`. if (!_rate) return; // In AVKit, the `defaultPlaybackRate` is used when playback starts, such as resuming after // pausing. In WebKit, however, `defaultPlaybackRate` is only used when first loading and after // ending scanning, with the `playbackRate` being used in all other cases, including when // resuming after pausing. As such, WebKit should return the `playbackRate` instead of the // `defaultPlaybackRate` in these cases when communicating with AVKit. [self setDefaultPlaybackRate:_rate fromJavaScript:fromJavaScript]; if (!fromJavaScript && _playbackSessionInterfaceMac) { if (auto* model = _playbackSessionInterfaceMac->playbackSessionModel(); model && model->playbackRate() != _rate) model->setPlaybackRate(_rate); } } - (void)togglePictureInPicture { if (auto* model = _playbackSessionInterfaceMac->playbackSessionModel()) model->togglePictureInPicture(); } IGNORE_WARNINGS_END @end #endif // PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE) && ENABLE(WEB_PLAYBACK_CONTROLS_MANAGER)