/* * Copyright (C) 2014-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. ``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 * 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 "WebAVPlayerController.h" #if PLATFORM(COCOA) && HAVE(AVKIT) #import "Logging.h" #import "PlaybackSessionInterfaceAVKit.h" #import "PlaybackSessionModel.h" #import "TimeRanges.h" #import #import #import #import #import SOFTLINK_AVKIT_FRAMEWORK() SOFT_LINK_CLASS_OPTIONAL(AVKit, AVPlayerController) SOFT_LINK_CLASS_OPTIONAL(AVKit, AVValueTiming) OBJC_CLASS AVAssetTrack; OBJC_CLASS AVMetadataItem; using namespace WebCore; static void * WebAVPlayerControllerSeekableTimeRangesObserverContext = &WebAVPlayerControllerSeekableTimeRangesObserverContext; static void * WebAVPlayerControllerHasLiveStreamingContentObserverContext = &WebAVPlayerControllerHasLiveStreamingContentObserverContext; static void * WebAVPlayerControllerIsPlayingOnSecondScreenObserverContext = &WebAVPlayerControllerIsPlayingOnSecondScreenObserverContext; static double WebAVPlayerControllerLiveStreamSeekableTimeRangeDurationHysteresisDelta = 3.0; // Minimum delta of time required to change the duration of the seekable time range. static double WebAVPlayerControllerLiveStreamMinimumTargetDuration = 1.0; // Minimum segment duration to be considered valid. static double WebAVPlayerControllerLiveStreamSeekableTimeRangeMinimumDuration = 30.0; @implementation WebAVPlayerController { WeakPtr _delegate; WeakPtr _playbackSessionInterface; double _defaultPlaybackRate; double _rate; BOOL _liveStreamEventModePossible; BOOL _isScrubbing; BOOL _allowsPictureInPicture; NSTimeInterval _seekToTime; } - (instancetype)init { if (!getAVPlayerControllerClass()) { [self release]; return nil; } if (!(self = [super init])) return self; initAVPlayerController(); self.playerControllerProxy = adoptNS([allocAVPlayerControllerInstance() init]).get(); _liveStreamEventModePossible = YES; [self addObserver:self forKeyPath:@"seekableTimeRanges" options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:WebAVPlayerControllerSeekableTimeRangesObserverContext]; [self addObserver:self forKeyPath:@"hasLiveStreamingContent" options:NSKeyValueObservingOptionInitial context:WebAVPlayerControllerHasLiveStreamingContentObserverContext]; [self addObserver:self forKeyPath:@"playingOnSecondScreen" options:NSKeyValueObservingOptionNew context:WebAVPlayerControllerIsPlayingOnSecondScreenObserverContext]; return self; } - (void)dealloc { [self removeObserver:self forKeyPath:@"seekableTimeRanges" context:WebAVPlayerControllerSeekableTimeRangesObserverContext]; [self removeObserver:self forKeyPath:@"hasLiveStreamingContent" context:WebAVPlayerControllerHasLiveStreamingContentObserverContext]; [self removeObserver:self forKeyPath:@"playingOnSecondScreen" context:WebAVPlayerControllerIsPlayingOnSecondScreenObserverContext]; [_playerControllerProxy release]; [_loadedTimeRanges release]; [_seekableTimeRanges release]; [_timing release]; [_audioMediaSelectionOptions release]; [_legibleMediaSelectionOptions release]; [_currentAudioMediaSelectionOption release]; [_currentLegibleMediaSelectionOption release]; [_externalPlaybackAirPlayDeviceLocalizedName release]; [_minTiming release]; [_maxTiming release]; [super dealloc]; } - (AVPlayer *)player { return nil; } - (id)forwardingTargetForSelector:(SEL)selector { UNUSED_PARAM(selector); return self.playerControllerProxy; } - (void)play:(id)sender { UNUSED_PARAM(sender); if (self.delegate) self.delegate->play(); } - (void)pause:(id)sender { UNUSED_PARAM(sender); if (self.delegate) self.delegate->pause(); } - (void)togglePlayback:(id)sender { UNUSED_PARAM(sender); if (!self.delegate) return; if (self.delegate->isPlaying()) self.delegate->pause(); else self.delegate->play(); } - (void)togglePlaybackEvenWhenInBackground:(id)sender { [self togglePlayback:sender]; } - (BOOL)isPlaying { return [self rate]; } - (void)setPlaying:(BOOL)playing { if (!self.delegate) return; if (playing) self.delegate->play(); else self.delegate->pause(); } - (WebCore::PlaybackSessionModel*)delegate { return _delegate.get(); } - (void)setDelegate:(WebCore::PlaybackSessionModel*)delegate { _delegate = WeakPtr { delegate }; } - (WebCore::PlaybackSessionInterfaceAVKit*)playbackSessionInterface { return _playbackSessionInterface.get(); } - (void)setPlaybackSessionInterface:(WebCore::PlaybackSessionInterfaceAVKit*)playbackSessionInterface { _playbackSessionInterface = WeakPtr { playbackSessionInterface }; } - (double)defaultPlaybackRate { return _defaultPlaybackRate; } - (void)setDefaultPlaybackRate:(double)defaultPlaybackRate { [self setDefaultPlaybackRate:defaultPlaybackRate fromJavaScript:NO]; } - (void)setDefaultPlaybackRate:(double)defaultPlaybackRate fromJavaScript:(BOOL)fromJavaScript { if (defaultPlaybackRate == _defaultPlaybackRate) return; [self willChangeValueForKey:@"defaultPlaybackRate"]; _defaultPlaybackRate = defaultPlaybackRate; [self didChangeValueForKey:@"defaultPlaybackRate"]; if (!fromJavaScript && self.delegate && self.delegate->defaultPlaybackRate() != _defaultPlaybackRate) self.delegate->setDefaultPlaybackRate(_defaultPlaybackRate); if ([self isPlaying]) [self setRate:_defaultPlaybackRate fromJavaScript:fromJavaScript]; } - (double)rate { return _rate; } - (void)setRate:(double)rate { [self setRate:rate fromJavaScript:NO]; } - (void)setRate:(double)rate fromJavaScript:(BOOL)fromJavaScript { if (rate == _rate) return; [self willChangeValueForKey:@"rate"]; _rate = rate; [self didChangeValueForKey:@"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 && self.delegate && self.delegate->playbackRate() != _rate) self.delegate->setPlaybackRate(_rate); } + (NSSet *)keyPathsForValuesAffectingPlaying { return [NSSet setWithObject:@"rate"]; } - (void)beginScrubbing:(id)sender { UNUSED_PARAM(sender); _isScrubbing = YES; if (self.delegate) self.delegate->beginScrubbing(); } - (void)endScrubbing:(id)sender { UNUSED_PARAM(sender); _isScrubbing = NO; if (self.delegate) self.delegate->endScrubbing(); } - (void)seekToTime:(NSTimeInterval)time { _seekToTime = time; if (self.delegate) self.delegate->seekToTime(time); } - (void)seekToTime:(NSTimeInterval)time toleranceBefore:(NSTimeInterval)before toleranceAfter:(NSTimeInterval)after { _seekToTime = time; if (self.delegate) self.delegate->seekToTime(time, before, after); } - (void)seekByTimeInterval:(NSTimeInterval)interval { [self seekByTimeInterval:interval toleranceBefore:0. toleranceAfter:0.]; } - (void)seekByTimeInterval:(NSTimeInterval)interval toleranceBefore:(NSTimeInterval)before toleranceAfter:(NSTimeInterval)after { NSTimeInterval targetTime = [[self timing] currentValue] + interval; [self seekToTime:targetTime toleranceBefore:before toleranceAfter:after]; } - (NSTimeInterval)currentTimeWithinEndTimes { return self.timing.currentValue; } - (void)setCurrentTimeWithinEndTimes:(NSTimeInterval)currentTimeWithinEndTimes { [self seekToTime:currentTimeWithinEndTimes]; } + (NSSet *)keyPathsForValuesAffectingCurrentTimeWithinEndTimes { return [NSSet setWithObject:@"timing"]; } - (BOOL)hasLiveStreamingContent { if ([self status] == AVPlayerControllerStatusReadyToPlay) return [self contentDuration] == std::numeric_limits::infinity(); return NO; } + (NSSet *)keyPathsForValuesAffectingHasLiveStreamingContent { return [NSSet setWithObjects:@"contentDuration", @"status", nil]; } - (NSTimeInterval)maxTime { NSTimeInterval maxTime = NAN; NSTimeInterval duration = [self contentDuration]; if (!isnan(duration) && !isinf(duration)) maxTime = duration; else if ([self hasSeekableLiveStreamingContent] && [self maxTiming]) maxTime = [[self maxTiming] currentValue]; return maxTime; } + (NSSet *)keyPathsForValuesAffectingMaxTime { return [NSSet setWithObjects:@"contentDuration", @"hasSeekableLiveStreamingContent", @"maxTiming", nil]; } - (NSTimeInterval)minTime { NSTimeInterval minTime = 0.0; if ([self hasSeekableLiveStreamingContent] && [self minTiming]) minTime = [[self minTiming] currentValue]; return minTime; } + (NSSet *)keyPathsForValuesAffectingMinTime { return [NSSet setWithObjects:@"hasSeekableLiveStreamingContent", @"minTiming", nil]; } - (void)skipBackwardThirtySeconds:(id)sender { using namespace PAL; UNUSED_PARAM(sender); BOOL isTimeWithinSeekableTimeRanges = NO; CMTime currentTime = CMTimeMakeWithSeconds([[self timing] currentValue], 1000); CMTime thirtySecondsBeforeCurrentTime = CMTimeSubtract(currentTime, PAL::CMTimeMake(30, 1)); for (NSValue *seekableTimeRangeValue in [self seekableTimeRanges]) { if (CMTimeRangeContainsTime([seekableTimeRangeValue CMTimeRangeValue], thirtySecondsBeforeCurrentTime)) { isTimeWithinSeekableTimeRanges = YES; break; } } if (isTimeWithinSeekableTimeRanges) [self seekToTime:CMTimeGetSeconds(thirtySecondsBeforeCurrentTime)]; } - (void)gotoEndOfSeekableRanges:(id)sender { using namespace PAL; UNUSED_PARAM(sender); NSTimeInterval timeAtEndOfSeekableTimeRanges = NAN; for (NSValue *seekableTimeRangeValue in [self seekableTimeRanges]) { CMTimeRange seekableTimeRange = [seekableTimeRangeValue CMTimeRangeValue]; NSTimeInterval endOfSeekableTimeRange = CMTimeGetSeconds(CMTimeRangeGetEnd(seekableTimeRange)); if (isnan(timeAtEndOfSeekableTimeRanges) || endOfSeekableTimeRange > timeAtEndOfSeekableTimeRanges) timeAtEndOfSeekableTimeRanges = endOfSeekableTimeRange; } if (!isnan(timeAtEndOfSeekableTimeRanges)) [self seekToTime:timeAtEndOfSeekableTimeRanges]; } - (BOOL)isScrubbing { return _isScrubbing; } - (BOOL)isSeeking { return _isScrubbing; } - (NSTimeInterval)seekToTime { return _seekToTime; } - (BOOL)canScanForward { return [self canPlay]; } + (NSSet *)keyPathsForValuesAffectingCanScanForward { return [NSSet setWithObject:@"canPlay"]; } - (void)beginScanningForward:(id)sender { UNUSED_PARAM(sender); if (self.delegate) self.delegate->beginScanningForward(); } - (void)endScanningForward:(id)sender { UNUSED_PARAM(sender); if (self.delegate) self.delegate->endScanning(); } - (void)beginScanningBackward:(id)sender { UNUSED_PARAM(sender); if (self.delegate) self.delegate->beginScanningBackward(); } - (void)endScanningBackward:(id)sender { UNUSED_PARAM(sender); if (self.delegate) self.delegate->endScanning(); } - (BOOL)canSeekToBeginning { using namespace PAL; CMTime minimumTime = kCMTimeIndefinite; for (NSValue *value in [self seekableTimeRanges]) minimumTime = CMTimeMinimum([value CMTimeRangeValue].start, minimumTime); return CMTIME_IS_NUMERIC(minimumTime); } + (NSSet *)keyPathsForValuesAffectingCanSeekToBeginning { return [NSSet setWithObject:@"seekableTimeRanges"]; } - (void)seekToBeginning:(id)sender { UNUSED_PARAM(sender); if (self.delegate) self.delegate->seekToTime(-INFINITY); } - (void)seekChapterBackward:(id)sender { [self seekToBeginning:sender]; } - (BOOL)canSeekToEnd { using namespace PAL; CMTime maximumTime = kCMTimeIndefinite; for (NSValue *value in [self seekableTimeRanges]) maximumTime = CMTimeMaximum(CMTimeRangeGetEnd([value CMTimeRangeValue]), maximumTime); return CMTIME_IS_NUMERIC(maximumTime); } + (NSSet *)keyPathsForValuesAffectingCanSeekToEnd { return [NSSet setWithObject:@"seekableTimeRanges"]; } - (void)seekToEnd:(id)sender { UNUSED_PARAM(sender); if (self.delegate) self.delegate->seekToTime(INFINITY); } - (void)seekChapterForward:(id)sender { [self seekToEnd:sender]; } - (BOOL)canSeekFrameBackward { return NO; } - (BOOL)canSeekFrameForward { return NO; } - (BOOL)hasMediaSelectionOptions { return [self hasAudioMediaSelectionOptions] || [self hasLegibleMediaSelectionOptions]; } + (NSSet *)keyPathsForValuesAffectingHasMediaSelectionOptions { return [NSSet setWithObjects:@"hasAudioMediaSelectionOptions", @"hasLegibleMediaSelectionOptions", nil]; } - (BOOL)hasAudioMediaSelectionOptions { return [[self audioMediaSelectionOptions] count] > 1; } + (NSSet *)keyPathsForValuesAffectingHasAudioMediaSelectionOptions { return [NSSet setWithObject:@"audioMediaSelectionOptions"]; } - (BOOL)hasLegibleMediaSelectionOptions { const NSUInteger numDefaultLegibleOptions = 2; return [[self legibleMediaSelectionOptions] count] > numDefaultLegibleOptions; } + (NSSet *)keyPathsForValuesAffectingHasLegibleMediaSelectionOptions { return [NSSet setWithObject:@"legibleMediaSelectionOptions"]; } - (WebAVMediaSelectionOption *)currentAudioMediaSelectionOption { return _currentAudioMediaSelectionOption; } - (void)setCurrentAudioMediaSelectionOption:(WebAVMediaSelectionOption *)option { if (option == _currentAudioMediaSelectionOption) return; [_currentAudioMediaSelectionOption release]; _currentAudioMediaSelectionOption = [option retain]; if (!self.delegate) return; NSInteger index = NSNotFound; if (option && self.audioMediaSelectionOptions) index = [self.audioMediaSelectionOptions indexOfObject:option]; if (index == NSNotFound) return; self.delegate->selectAudioMediaOption(index); } - (WebAVMediaSelectionOption *)currentLegibleMediaSelectionOption { return _currentLegibleMediaSelectionOption; } - (void)setCurrentLegibleMediaSelectionOption:(WebAVMediaSelectionOption *)option { if (option == _currentLegibleMediaSelectionOption) return; [_currentLegibleMediaSelectionOption release]; _currentLegibleMediaSelectionOption = [option retain]; if (!self.delegate) return; NSInteger index = NSNotFound; if (option && self.legibleMediaSelectionOptions) index = [self.legibleMediaSelectionOptions indexOfObject:option]; if (index == NSNotFound) return; self.delegate->selectLegibleMediaOption(index); } - (BOOL)isPlayingOnExternalScreen { return [self isExternalPlaybackActive] || [self isPlayingOnSecondScreen]; } + (NSSet *)keyPathsForValuesAffectingPlayingOnExternalScreen { return [NSSet setWithObjects:@"externalPlaybackActive", @"playingOnSecondScreen", nil]; } - (void)setAllowsPictureInPicture:(BOOL)allowsPictureInPicture { _allowsPictureInPicture = allowsPictureInPicture; } - (BOOL)isPictureInPicturePossible { return _allowsPictureInPicture && ![self isExternalPlaybackActive]; } - (BOOL)isPictureInPictureInterrupted { return _pictureInPictureInterrupted; } - (void)setPictureInPictureInterrupted:(BOOL)pictureInPictureInterrupted { if (_pictureInPictureInterrupted != pictureInPictureInterrupted) { _pictureInPictureInterrupted = pictureInPictureInterrupted; if (pictureInPictureInterrupted) [self setPlaying:NO]; } } - (BOOL)isMuted { return _muted; } - (void)setMuted:(BOOL)muted { if (_muted == muted) return; _muted = muted; if (self.delegate) self.delegate->setMuted(muted); } - (void)toggleMuted:(id)sender { UNUSED_PARAM(sender); if (self.delegate) self.delegate->toggleMuted(); } - (double)volume { return self.delegate ? self.delegate->volume() : 0; } - (void)setVolume:(double)volume { if (self.delegate) self.delegate->setVolume(volume); } - (void)volumeChanged:(double)volume { UNUSED_PARAM(volume); [self willChangeValueForKey:@"volume"]; [self didChangeValueForKey:@"volume"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { using namespace PAL; UNUSED_PARAM(object); UNUSED_PARAM(keyPath); if (WebAVPlayerControllerSeekableTimeRangesObserverContext == context) { NSArray *oldArray = change[NSKeyValueChangeOldKey]; NSArray *newArray = change[NSKeyValueChangeNewKey]; if ([oldArray isKindOfClass:[NSArray class]] && [newArray isKindOfClass:[NSArray class]]) { CMTimeRange oldSeekableTimeRange = [oldArray count] > 0 ? [[oldArray firstObject] CMTimeRangeValue] : kCMTimeRangeInvalid; CMTimeRange newSeekableTimeRange = [newArray count] > 0 ? [[newArray firstObject] CMTimeRangeValue] : kCMTimeRangeInvalid; if (!CMTimeRangeEqual(oldSeekableTimeRange, newSeekableTimeRange)) { if (CMTIMERANGE_IS_VALID(newSeekableTimeRange) && CMTIMERANGE_IS_VALID(oldSeekableTimeRange) && _liveStreamEventModePossible && !CMTimeRangeContainsTime(newSeekableTimeRange, oldSeekableTimeRange.start)) _liveStreamEventModePossible = NO; [self updateMinMaxTiming]; } } return; } if (WebAVPlayerControllerHasLiveStreamingContentObserverContext == context) { [self updateMinMaxTiming]; return; } if (WebAVPlayerControllerIsPlayingOnSecondScreenObserverContext == context) { if (auto* delegate = self.delegate) delegate->setPlayingOnSecondScreen(_playingOnSecondScreen); return; } ASSERT_NOT_REACHED(); } - (void)updateMinMaxTiming { using namespace PAL; AVValueTiming *newMinTiming = nil; AVValueTiming *newMaxTiming = nil; // For live streams value timings for the min and max times are needed. if ([self hasLiveStreamingContent] && ([[self seekableTimeRanges] count] > 0)) { CMTimeRange seekableTimeRange = [[[self seekableTimeRanges] firstObject] CMTimeRangeValue]; if (CMTIMERANGE_IS_VALID(seekableTimeRange)) { NSTimeInterval oldMinTime = [[self minTiming] currentValue]; NSTimeInterval oldMaxTime = [[self maxTiming] currentValue]; NSTimeInterval newMinTime = CMTimeGetSeconds(seekableTimeRange.start); NSTimeInterval newMaxTime = CMTimeGetSeconds(seekableTimeRange.start) + CMTimeGetSeconds(seekableTimeRange.duration) + (CACurrentMediaTime() - [self seekableTimeRangesLastModifiedTime]); double newMinTimingRate = _liveStreamEventModePossible ? 0.0 : 1.0; BOOL minTimingNeedsUpdate = YES; BOOL maxTimingNeedsUpdate = YES; if (isfinite([self liveUpdateInterval]) && [self liveUpdateInterval] > WebAVPlayerControllerLiveStreamMinimumTargetDuration) { // Only update the timing if the new time differs by one segment duration plus the hysteresis delta. minTimingNeedsUpdate = isnan(oldMinTime) || (fabs(oldMinTime - newMinTime) > [self liveUpdateInterval] + WebAVPlayerControllerLiveStreamSeekableTimeRangeDurationHysteresisDelta) || ([[self minTiming] rate] != newMinTimingRate); maxTimingNeedsUpdate = isnan(oldMaxTime) || (fabs(oldMaxTime - newMaxTime) > [self liveUpdateInterval] + WebAVPlayerControllerLiveStreamSeekableTimeRangeDurationHysteresisDelta); } if (minTimingNeedsUpdate || maxTimingNeedsUpdate) { newMinTiming = [getAVValueTimingClass() valueTimingWithAnchorValue:newMinTime anchorTimeStamp:[getAVValueTimingClass() currentTimeStamp] rate:newMinTimingRate]; newMaxTiming = [getAVValueTimingClass() valueTimingWithAnchorValue:newMaxTime anchorTimeStamp:[getAVValueTimingClass() currentTimeStamp] rate:1.0]; } else { newMinTiming = [self minTiming]; newMaxTiming = [self maxTiming]; } } } if (!newMinTiming) newMinTiming = [getAVValueTimingClass() valueTimingWithAnchorValue:[self minTime] anchorTimeStamp:NAN rate:0.0]; if (!newMaxTiming) newMaxTiming = [getAVValueTimingClass() valueTimingWithAnchorValue:[self maxTime] anchorTimeStamp:NAN rate:0.0]; [self setMinTiming:newMinTiming]; [self setMaxTiming:newMaxTiming]; } - (BOOL)hasSeekableLiveStreamingContent { BOOL hasSeekableLiveStreamingContent = NO; if ([self hasLiveStreamingContent] && [self minTiming] && [self maxTiming] && isfinite([self liveUpdateInterval]) && [self liveUpdateInterval] > WebAVPlayerControllerLiveStreamMinimumTargetDuration && ([self seekableTimeRangesLastModifiedTime] != 0.0)) { NSTimeInterval timeStamp = [getAVValueTimingClass() currentTimeStamp]; NSTimeInterval minTime = [[self minTiming] valueForTimeStamp:timeStamp]; NSTimeInterval maxTime = [[self maxTiming] valueForTimeStamp:timeStamp]; hasSeekableLiveStreamingContent = ((maxTime - minTime) > WebAVPlayerControllerLiveStreamSeekableTimeRangeMinimumDuration); } return hasSeekableLiveStreamingContent; } + (NSSet *)keyPathsForValuesAffectingHasSeekableLiveStreamingContent { return [NSSet setWithObjects:@"hasLiveStreamingContent", @"minTiming", @"maxTiming", @"seekableTimeRangesLastModifiedTime", nil]; } @end @implementation WebAVMediaSelectionOption { RetainPtr _localizedDisplayName; } - (instancetype)initWithMediaType:(AVMediaType)mediaType displayName:(NSString *)displayName { self = [super init]; if (!self) return nil; _mediaType = mediaType; _localizedDisplayName = displayName; return self; } - (NSString *)localizedDisplayName { return _localizedDisplayName.get(); } - (NSArray *)mediaSubTypes { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption mediaSubTypes] unimplemented"); return @[]; } - (BOOL)hasMediaCharacteristic:(AVMediaCharacteristic)mediaCharacteristic { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption hasMediaCharacteristic:] unimplemented"); return NO; } - (BOOL) isPlayable { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption isPlayable:] unimplemented"); return YES; } - (NSString *)extendedLanguageTag { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption extendedLanguageTag] unimplemented"); return nil; } - (NSLocale *)locale { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption locale] unimplemented"); return nil; } - (NSArray *)commonMetadata { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption commonMetadata] unimplemented"); return @[]; } - (NSArray *)availableMetadataFormats { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption availableMetadataFormats] unimplemented"); return @[]; } - (NSArray *)metadataForFormat:(NSString *)format { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption metadataForFormat:] unimplemented"); return @[]; } - (AVMediaSelectionOption *)associatedMediaSelectionOptionInMediaSelectionGroup:(AVMediaSelectionGroup *)mediaSelectionGroup { WTFLogAlways("ERROR: -[WebAVMediaSelectionOption associatedMediaSelectionOptionInMediaSelectionGroup] unimplemented"); ASSERT_NOT_REACHED(); return nil; } - (id)propertyList { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption propertyList] unimplemented"); return @[]; } - (NSString *)displayNameWithLocale:(NSLocale *)locale { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption displayNameWithLocale:] unimplemented"); return nil; } - (NSArray *)mediaCharacteristics { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption mediaCharacteristics] unimplemented"); return @[]; } - (NSString *)outOfBandSource { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption outOfBandSource] unimplemented"); return nil; } - (NSString *)outOfBandIdentifier { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption outOfBandIdentifier] unimplemented"); return nil; } - (BOOL)_isDesignatedDefault { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption _isDesignatedDefault] unimplemented"); return NO; } - (NSString *)languageCode { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption languageCode] unimplemented"); return nil; } - (AVAssetTrack *)track { ASSERT_NOT_REACHED(); WTFLogAlways("ERROR: -[WebAVMediaSelectionOption track:] unimplemented"); return nil; } @end #endif // PLATFORM(COCOA) && HAVE(AVKIT)