diff options
Diffstat (limited to 'Source/WebCore/platform/graphics/avfoundation/MediaPlayerPrivateAVFoundationObjC.mm')
-rw-r--r-- | Source/WebCore/platform/graphics/avfoundation/MediaPlayerPrivateAVFoundationObjC.mm | 811 |
1 files changed, 811 insertions, 0 deletions
diff --git a/Source/WebCore/platform/graphics/avfoundation/MediaPlayerPrivateAVFoundationObjC.mm b/Source/WebCore/platform/graphics/avfoundation/MediaPlayerPrivateAVFoundationObjC.mm new file mode 100644 index 0000000..55eb433 --- /dev/null +++ b/Source/WebCore/platform/graphics/avfoundation/MediaPlayerPrivateAVFoundationObjC.mm @@ -0,0 +1,811 @@ +/* + * Copyright (C) 2011 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 COMPUTER, 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 COMPUTER, 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" + +#if ENABLE(VIDEO) && USE(AVFOUNDATION) + +#import "MediaPlayerPrivateAVFoundationObjC.h" + +#import "ApplicationCacheResource.h" +#import "BlockExceptions.h" +#import "FloatConversion.h" +#import "FrameView.h" +#import "FloatConversion.h" +#import "GraphicsContext.h" +#import "KURL.h" +#import "Logging.h" +#import "SoftLinking.h" +#import "TimeRanges.h" +#import "WebCoreSystemInterface.h" +#import <objc/objc-runtime.h> +#import <wtf/UnusedParam.h> + +#import <CoreMedia/CoreMedia.h> +#import <AVFoundation/AVFoundation.h> + +SOFT_LINK_FRAMEWORK(AVFoundation) +SOFT_LINK_FRAMEWORK(CoreMedia) + +SOFT_LINK(CoreMedia, CMTimeCompare, int32_t, (CMTime time1, CMTime time2), (time1, time2)) +SOFT_LINK(CoreMedia, CMTimeMakeWithSeconds, CMTime, (Float64 seconds, int32_t preferredTimeScale), (seconds, preferredTimeScale)) +SOFT_LINK(CoreMedia, CMTimeGetSeconds, Float64, (CMTime time), (time)) +SOFT_LINK(CoreMedia, CMTimeRangeGetEnd, CMTime, (CMTimeRange range), (range)) + +SOFT_LINK_CLASS(AVFoundation, AVPlayer) +SOFT_LINK_CLASS(AVFoundation, AVPlayerItem) +SOFT_LINK_CLASS(AVFoundation, AVPlayerLayer) +SOFT_LINK_CLASS(AVFoundation, AVURLAsset) +SOFT_LINK_CLASS(AVFoundation, AVAssetImageGenerator) + +SOFT_LINK_POINTER(AVFoundation, AVMediaCharacteristicVisual, NSString *) +SOFT_LINK_POINTER(AVFoundation, AVMediaCharacteristicAudible, NSString *) +SOFT_LINK_POINTER(AVFoundation, AVMediaTypeClosedCaption, NSString *) +SOFT_LINK_POINTER(AVFoundation, AVPlayerItemDidPlayToEndTimeNotification, NSString *) +SOFT_LINK_POINTER(AVFoundation, AVAssetImageGeneratorApertureModeCleanAperture, NSString *) + +SOFT_LINK_CONSTANT(CoreMedia, kCMTimeZero, CMTime) + +#define AVPlayer getAVPlayerClass() +#define AVPlayerItem getAVPlayerItemClass() +#define AVPlayerLayer getAVPlayerLayerClass() +#define AVURLAsset getAVURLAssetClass() +#define AVAssetImageGenerator getAVAssetImageGeneratorClass() + +#define AVMediaCharacteristicVisual getAVMediaCharacteristicVisual() +#define AVMediaCharacteristicAudible getAVMediaCharacteristicAudible() +#define AVMediaTypeClosedCaption getAVMediaTypeClosedCaption() +#define AVPlayerItemDidPlayToEndTimeNotification getAVPlayerItemDidPlayToEndTimeNotification() +#define AVAssetImageGeneratorApertureModeCleanAperture getAVAssetImageGeneratorApertureModeCleanAperture() + +#define kCMTimeZero getkCMTimeZero() + +using namespace WebCore; +using namespace std; + +enum MediaPlayerAVFoundationObservationContext { + MediaPlayerAVFoundationObservationContextPlayerItem, + MediaPlayerAVFoundationObservationContextPlayer +}; + +@interface WebCoreAVFMovieObserver : NSObject +{ + MediaPlayerPrivateAVFoundationObjC* m_callback; + int m_delayCallbacks; +} +-(id)initWithCallback:(MediaPlayerPrivateAVFoundationObjC*)callback; +-(void)disconnect; +-(void)playableKnown; +-(void)metadataLoaded; +-(void)timeChanged:(double)time; +-(void)didEnd:(NSNotification *)notification; +-(void)observeValueForKeyPath:keyPath ofObject:(id)object change:(NSDictionary *)change context:(MediaPlayerAVFoundationObservationContext)context; +@end + +namespace WebCore { + +static NSArray *assetMetadataKeyNames(); +static NSArray *itemKVOProperties(); + +#if !LOG_DISABLED +static const char *boolString(bool val) +{ + return val ? "true" : "false"; +} +#endif + +static const float invalidTime = -1.0f; + +MediaPlayerPrivateInterface* MediaPlayerPrivateAVFoundationObjC::create(MediaPlayer* player) +{ + return new MediaPlayerPrivateAVFoundationObjC(player); +} + +void MediaPlayerPrivateAVFoundationObjC::registerMediaEngine(MediaEngineRegistrar registrar) +{ + if (isAvailable()) + registrar(create, getSupportedTypes, supportsType, 0, 0, 0); +} + +MediaPlayerPrivateAVFoundationObjC::MediaPlayerPrivateAVFoundationObjC(MediaPlayer* player) + : MediaPlayerPrivateAVFoundation(player) + , m_objcObserver(AdoptNS, [[WebCoreAVFMovieObserver alloc] initWithCallback:this]) + , m_timeObserver(0) +{ +} + +MediaPlayerPrivateAVFoundationObjC::~MediaPlayerPrivateAVFoundationObjC() +{ + cancelLoad(); + [m_objcObserver.get() disconnect]; +} + +void MediaPlayerPrivateAVFoundationObjC::cancelLoad() +{ + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::cancelLoad(%p)", this); + tearDownVideoRendering(); + + [[NSNotificationCenter defaultCenter] removeObserver:m_objcObserver.get()]; + + // Tell our observer to do nothing when our cancellation of pending loading calls its completion handler. + setIgnoreLoadStateChanges(true); + if (m_avAsset) { + [m_avAsset.get() cancelLoading]; + m_avAsset = nil; + } + if (m_avPlayerItem) { + for (NSString *keyName in itemKVOProperties()) + [m_avPlayerItem.get() removeObserver:m_objcObserver.get() forKeyPath:keyName]; + + m_avPlayerItem = nil; + } + if (m_avPlayer) { + if (m_timeObserver) + [m_avPlayer.get() removeTimeObserver:m_timeObserver]; + [m_avPlayer.get() removeObserver:m_objcObserver.get() forKeyPath:@"rate"]; + m_avPlayer = nil; + } + setIgnoreLoadStateChanges(false); +} + +bool MediaPlayerPrivateAVFoundationObjC::hasLayerRenderer() const +{ + return m_videoLayer; +} + +bool MediaPlayerPrivateAVFoundationObjC::hasContextRenderer() const +{ + return m_imageGenerator; +} + +void MediaPlayerPrivateAVFoundationObjC::createContextVideoRenderer() +{ + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::createContextVideoRenderer(%p)", this); + + if (!m_avAsset || m_imageGenerator) + return; + + m_imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:m_avAsset.get()]; + + [m_imageGenerator.get() setApertureMode:AVAssetImageGeneratorApertureModeCleanAperture]; + [m_imageGenerator.get() setAppliesPreferredTrackTransform:YES]; + + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::createImageGenerator(%p) - returning %p", this, m_imageGenerator.get()); +} + +void MediaPlayerPrivateAVFoundationObjC::destroyContextVideoRenderer() +{ + if (!m_imageGenerator) + return; + + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::destroyContextVideoRenderer(%p) - destroying", this, m_imageGenerator.get()); + + m_imageGenerator = 0; +} + +void MediaPlayerPrivateAVFoundationObjC::createVideoLayer() +{ + if (!m_avPlayer) + return; + + if (!m_videoLayer) { + m_videoLayer.adoptNS([[AVPlayerLayer alloc] init]); + [m_videoLayer.get() setPlayer:m_avPlayer.get()]; + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::createVideoLayer(%p) - returning", this, m_videoLayer.get()); + } +} + +void MediaPlayerPrivateAVFoundationObjC::destroyVideoLayer() +{ + if (!m_videoLayer) + return; + + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::destroyVideoLayer(%p) - destroying", this, m_videoLayer.get()); + + [m_videoLayer.get() setPlayer:nil]; + + m_videoLayer = 0; +} + +bool MediaPlayerPrivateAVFoundationObjC::videoLayerIsReadyToDisplay() const +{ + return (m_videoLayer && [m_videoLayer.get() isReadyForDisplay]); +} + +void MediaPlayerPrivateAVFoundationObjC::createAVPlayerForURL(const String& url) +{ + setDelayCallbacks(true); + + if (!m_avAsset) { + NSURL *cocoaURL = KURL(ParsedURLString, url); + m_avAsset.adoptNS([[AVURLAsset alloc] initWithURL:cocoaURL options:nil]); + } + + createAVPlayer(); +} + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) +void MediaPlayerPrivateAVFoundationObjC::createAVPlayerForCacheResource(ApplicationCacheResource* resource) +{ + // AVFoundation can't open arbitrary data pointers, so if this ApplicationCacheResource doesn't + // have a valid local path, just open the resource's original URL. + if (resource->path().isEmpty()) { + createAVPlayerForURL(resource->url()); + return; + } + + setDelayCallbacks(true); + + if (!m_avAsset) { + NSURL* localURL = [NSURL fileURLWithPath:resource->path()]; + m_avAsset.adoptNS([[AVURLAsset alloc] initWithURL:localURL options:nil]); + } + + createAVPlayer(); +} +#endif + +void MediaPlayerPrivateAVFoundationObjC::createAVPlayer() +{ + if (!m_avPlayer) { + m_avPlayer.adoptNS([[AVPlayer alloc] init]); + + [m_avPlayer.get() addObserver:m_objcObserver.get() forKeyPath:@"rate" options:nil context:(void *)MediaPlayerAVFoundationObservationContextPlayer]; + + // Add a time observer, ask to be called infrequently because we don't really want periodic callbacks but + // our observer will also be called whenever a seek happens. + const double veryLongInterval = 60*60*60*24*30; + WebCoreAVFMovieObserver *observer = m_objcObserver.get(); + m_timeObserver = [m_avPlayer.get() addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(veryLongInterval, 10) queue:nil usingBlock:^(CMTime time){ + [observer timeChanged:CMTimeGetSeconds(time)]; + }]; + } + + if (!m_avPlayerItem) { + // Create the player item so we can media data. + m_avPlayerItem.adoptNS([[AVPlayerItem alloc] initWithAsset:m_avAsset.get()]); + + [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get()selector:@selector(didEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:m_avPlayerItem.get()]; + + for (NSString *keyName in itemKVOProperties()) + [m_avPlayerItem.get() addObserver:m_objcObserver.get() forKeyPath:keyName options:nil context:(void *)MediaPlayerAVFoundationObservationContextPlayerItem]; + + [m_avPlayer.get() replaceCurrentItemWithPlayerItem:m_avPlayerItem.get()]; + } + + setDelayCallbacks(false); +} + +void MediaPlayerPrivateAVFoundationObjC::checkPlayability() +{ + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::checkPlayability(%p)", this); + + [m_avAsset.get() loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:@"playable"] completionHandler:^{ + [m_objcObserver.get() playableKnown]; + }]; +} + +void MediaPlayerPrivateAVFoundationObjC::beginLoadingMetadata() +{ + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::playabilityKnown(%p) - requesting metadata loading", this); + [m_avAsset.get() loadValuesAsynchronouslyForKeys:[assetMetadataKeyNames() retain] completionHandler:^{ + [m_objcObserver.get() metadataLoaded]; + }]; +} + +MediaPlayerPrivateAVFoundation::ItemStatus MediaPlayerPrivateAVFoundationObjC::playerItemStatus() const +{ + if (!m_avPlayerItem) + return MediaPlayerPrivateAVFoundation::MediaPlayerAVPlayerItemStatusUnknown; + + AVPlayerItemStatus status = [m_avPlayerItem.get() status]; + if (status == AVPlayerItemStatusUnknown) + return MediaPlayerPrivateAVFoundation::MediaPlayerAVPlayerItemStatusUnknown; + if (status == AVPlayerItemStatusFailed) + return MediaPlayerPrivateAVFoundation::MediaPlayerAVPlayerItemStatusFailed; + if ([m_avPlayerItem.get() isPlaybackLikelyToKeepUp]) + return MediaPlayerPrivateAVFoundation::MediaPlayerAVPlayerItemStatusPlaybackLikelyToKeepUp; + if ([m_avPlayerItem.get() isPlaybackBufferFull]) + return MediaPlayerPrivateAVFoundation::MediaPlayerAVPlayerItemStatusPlaybackBufferFull; + if ([m_avPlayerItem.get() isPlaybackBufferEmpty]) + return MediaPlayerPrivateAVFoundation::MediaPlayerAVPlayerItemStatusPlaybackBufferEmpty; + + return MediaPlayerPrivateAVFoundation::MediaPlayerAVPlayerItemStatusReadyToPlay; +} + +PlatformMedia MediaPlayerPrivateAVFoundationObjC::platformMedia() const +{ + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::platformMedia(%p)", this); + PlatformMedia pm; + pm.type = PlatformMedia::AVFoundationMediaPlayerType; + pm.media.avfMediaPlayer = m_avPlayer.get(); + return pm; +} + +PlatformLayer* MediaPlayerPrivateAVFoundationObjC::platformLayer() const +{ + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::platformLayer(%p)", this); + return m_videoLayer.get(); +} + +void MediaPlayerPrivateAVFoundationObjC::play() +{ + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::play(%p)", this); + if (!metaDataAvailable()) + return; + + setDelayCallbacks(true); + [m_avPlayer.get() setRate:requestedRate()]; + setDelayCallbacks(false); +} + +void MediaPlayerPrivateAVFoundationObjC::pause() +{ + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::pause(%p)", this); + if (!metaDataAvailable()) + return; + + setDelayCallbacks(true); + [m_avPlayer.get() setRate:nil]; + setDelayCallbacks(false); +} + +float MediaPlayerPrivateAVFoundationObjC::platformDuration() const +{ + if (!metaDataAvailable() || !m_avPlayerItem) + return 0; + + float duration; + CMTime cmDuration = [m_avPlayerItem.get() duration]; + if (CMTIME_IS_NUMERIC(cmDuration)) + duration = narrowPrecisionToFloat(CMTimeGetSeconds(cmDuration)); + else if (CMTIME_IS_INDEFINITE(cmDuration)) + duration = numeric_limits<float>::infinity(); + else { + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::duration(%p) - invalid duration, returning 0", this); + return 0; + } + + return duration; +} + +float MediaPlayerPrivateAVFoundationObjC::currentTime() const +{ + if (!metaDataAvailable() || !m_avPlayerItem) + return 0; + + CMTime itemTime = [m_avPlayerItem.get() currentTime]; + if (CMTIME_IS_NUMERIC(itemTime)) + return narrowPrecisionToFloat(CMTimeGetSeconds(itemTime)); + + return 0; +} + +void MediaPlayerPrivateAVFoundationObjC::seekToTime(float time) +{ + // setCurrentTime generates several event callbacks, update afterwards. + setDelayCallbacks(true); + + float now = currentTime(); + if (time != now) + [m_avPlayerItem.get() seekToTime:CMTimeMakeWithSeconds(time, 600) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; + else { + // Force a call to the "time changed" notifier manually because a seek to the current time is a noop + // so the seek will never seem to complete. + [m_objcObserver.get() timeChanged:now]; + } + + setDelayCallbacks(false); +} + +void MediaPlayerPrivateAVFoundationObjC::setVolume(float volume) +{ + if (!metaDataAvailable()) + return; + + [m_avPlayer.get() setVolume:volume]; +} + +void MediaPlayerPrivateAVFoundationObjC::setClosedCaptionsVisible(bool closedCaptionsVisible) +{ + if (!metaDataAvailable()) + return; + + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::setClosedCaptionsVisible(%p) - setting to %s", this, boolString(closedCaptionsVisible)); + [m_avPlayer.get() setClosedCaptionDisplayEnabled:closedCaptionsVisible]; +} + +float MediaPlayerPrivateAVFoundationObjC::rate() const +{ + if (!metaDataAvailable()) + return 0; + + return [m_avPlayer.get() rate]; +} + +PassRefPtr<TimeRanges> MediaPlayerPrivateAVFoundationObjC::platformBufferedTimeRanges() const +{ + RefPtr<TimeRanges> timeRanges = TimeRanges::create(); + + if (!m_avPlayerItem) + return timeRanges.release(); + + NSArray *loadedRanges = [m_avPlayerItem.get() loadedTimeRanges]; + for (NSValue *thisRangeValue in loadedRanges) { + CMTimeRange timeRange = [thisRangeValue CMTimeRangeValue]; + if (CMTIMERANGE_IS_VALID(timeRange) && !CMTIMERANGE_IS_EMPTY(timeRange)) { + float rangeStart = narrowPrecisionToFloat(CMTimeGetSeconds(timeRange.start)); + float rangeEnd = narrowPrecisionToFloat(CMTimeGetSeconds(CMTimeRangeGetEnd(timeRange))); + timeRanges->add(rangeStart, rangeEnd); + } + } + return timeRanges.release(); +} + +float MediaPlayerPrivateAVFoundationObjC::platformMaxTimeSeekable() const +{ + NSArray *seekableRanges = [m_avPlayerItem.get() seekableTimeRanges]; + if (!seekableRanges) + return 0; + + float maxTimeSeekable = 0; + for (NSValue *thisRangeValue in seekableRanges) { + CMTimeRange timeRange = [thisRangeValue CMTimeRangeValue]; + if (!CMTIMERANGE_IS_VALID(timeRange) || CMTIMERANGE_IS_EMPTY(timeRange)) + continue; + + float endOfRange = narrowPrecisionToFloat(CMTimeGetSeconds(CMTimeRangeGetEnd(timeRange))); + if (maxTimeSeekable < endOfRange) + maxTimeSeekable = endOfRange; + } + return maxTimeSeekable; +} + +float MediaPlayerPrivateAVFoundationObjC::platformMaxTimeLoaded() const +{ + NSArray *loadedRanges = [m_avPlayerItem.get() loadedTimeRanges]; + if (!loadedRanges) + return 0; + + float maxTimeLoaded = 0; + for (NSValue *thisRangeValue in loadedRanges) { + CMTimeRange timeRange = [thisRangeValue CMTimeRangeValue]; + if (!CMTIMERANGE_IS_VALID(timeRange) || CMTIMERANGE_IS_EMPTY(timeRange)) + continue; + + float endOfRange = narrowPrecisionToFloat(CMTimeGetSeconds(CMTimeRangeGetEnd(timeRange))); + if (maxTimeLoaded < endOfRange) + maxTimeLoaded = endOfRange; + } + + return maxTimeLoaded; +} + +unsigned MediaPlayerPrivateAVFoundationObjC::totalBytes() const +{ + if (!metaDataAvailable()) + return 0; + + long long totalMediaSize = 0; + NSArray *tracks = [m_avAsset.get() tracks]; + for (AVAssetTrack *thisTrack in tracks) + totalMediaSize += [thisTrack totalSampleDataLength]; + + return static_cast<unsigned>(totalMediaSize); +} + +void MediaPlayerPrivateAVFoundationObjC::setAsset(id asset) +{ + m_avAsset = asset; +} + +MediaPlayerPrivateAVFoundation::AVAssetStatus MediaPlayerPrivateAVFoundationObjC::assetStatus() const +{ + if (!m_avAsset) + return MediaPlayerAVAssetStatusUnknown; + + for (NSString *keyName in assetMetadataKeyNames()) { + AVKeyValueStatus keyStatus = [m_avAsset.get() statusOfValueForKey:keyName error:nil]; + if (keyStatus < AVKeyValueStatusLoaded) + return MediaPlayerAVAssetStatusLoading;// At least one key is not loaded yet. + + if (keyStatus == AVKeyValueStatusFailed) + return MediaPlayerAVAssetStatusFailed; // At least one key could not be loaded. + if (keyStatus == AVKeyValueStatusCancelled) + return MediaPlayerAVAssetStatusCancelled; // Loading of at least one key was cancelled. + } + + if ([[m_avAsset.get() valueForKey:@"playable"] boolValue]) + return MediaPlayerAVAssetStatusPlayable; + + return MediaPlayerAVAssetStatusLoaded; +} + +void MediaPlayerPrivateAVFoundationObjC::paintCurrentFrameInContext(GraphicsContext* context, const IntRect& rect) +{ + if (!metaDataAvailable() || context->paintingDisabled()) + return; + + paint(context, rect); +} + +void MediaPlayerPrivateAVFoundationObjC::paint(GraphicsContext* context, const IntRect& rect) +{ + if (!metaDataAvailable() || context->paintingDisabled()) + return; + + setDelayCallbacks(true); + BEGIN_BLOCK_OBJC_EXCEPTIONS; + + RetainPtr<CGImageRef> image = createImageForTimeInRect(currentTime(), rect); + if (image) { + context->save(); + context->translate(rect.x(), rect.y() + rect.height()); + context->scale(FloatSize(1.0f, -1.0f)); + context->setImageInterpolationQuality(InterpolationLow); + IntRect paintRect(IntPoint(0, 0), IntSize(rect.width(), rect.height())); + CGContextDrawImage(context->platformContext(), CGRectMake(0, 0, paintRect.width(), paintRect.height()), image.get()); + context->restore(); + image = 0; + } + + END_BLOCK_OBJC_EXCEPTIONS; + setDelayCallbacks(false); + + MediaPlayerPrivateAVFoundation::paint(context, rect); +} + +static HashSet<String> mimeTypeCache() +{ + DEFINE_STATIC_LOCAL(HashSet<String>, cache, ()); + static bool typeListInitialized = false; + + if (typeListInitialized) + return cache; + typeListInitialized = true; + + NSArray *types = [AVURLAsset audiovisualMIMETypes]; + for (NSString *mimeType in types) + cache.add(mimeType); + + return cache; +} + +RetainPtr<CGImageRef> MediaPlayerPrivateAVFoundationObjC::createImageForTimeInRect(float time, const IntRect& rect) +{ + if (!m_imageGenerator) + createContextVideoRenderer(); + ASSERT(m_imageGenerator); + +#if !LOG_DISABLED + double start = WTF::currentTime(); +#endif + + [m_imageGenerator.get() setMaximumSize:CGSize(rect.size())]; + CGImageRef image = [m_imageGenerator.get() copyCGImageAtTime:CMTimeMakeWithSeconds(time, 600) actualTime:nil error:nil]; + +#if !LOG_DISABLED + double duration = WTF::currentTime() - start; + LOG(Media, "MediaPlayerPrivateAVFoundationObjC::createImageForTimeInRect(%p) - creating image took %.4f", this, narrowPrecisionToFloat(duration)); +#endif + + return image; +} + +void MediaPlayerPrivateAVFoundationObjC::getSupportedTypes(HashSet<String>& supportedTypes) +{ + supportedTypes = mimeTypeCache(); +} + +MediaPlayer::SupportsType MediaPlayerPrivateAVFoundationObjC::supportsType(const String& type, const String& codecs) +{ + // Only return "IsSupported" if there is no codecs parameter for now as there is no way to ask if it supports an + // extended MIME type until rdar://6220037 is fixed. + if (mimeTypeCache().contains(type)) + return codecs.isEmpty() ? MediaPlayer::MayBeSupported : MediaPlayer::IsSupported; + + return MediaPlayer::IsNotSupported; +} + +bool MediaPlayerPrivateAVFoundationObjC::isAvailable() +{ + return true; +} + +float MediaPlayerPrivateAVFoundationObjC::mediaTimeForTimeValue(float timeValue) const +{ + if (!metaDataAvailable()) + return timeValue; + + // FIXME - impossible to implement until rdar://8721510 is fixed. + return timeValue; +} + +void MediaPlayerPrivateAVFoundationObjC::tracksChanged() +{ + // This is called whenever the tracks collection changes so cache hasVideo and hasAudio since we get + // asked about those fairly fequently. + setHasVideo([[m_avAsset.get() tracksWithMediaCharacteristic:AVMediaCharacteristicVisual] count]); + setHasAudio([[m_avAsset.get() tracksWithMediaCharacteristic:AVMediaCharacteristicAudible] count]); + setHasClosedCaptions([[m_avAsset.get() tracksWithMediaType:AVMediaTypeClosedCaption] count]); + + sizeChanged(); +} + +void MediaPlayerPrivateAVFoundationObjC::sizeChanged() +{ + NSArray *tracks = [m_avAsset.get() tracks]; + + // Some assets don't report track properties until they are completely ready to play, but we + // want to report a size as early as possible so use presentationSize when an asset has no tracks. + if (![tracks count]) { + setNaturalSize(IntSize([m_avPlayerItem.get() presentationSize])); + return; + } + + // AVAsset's 'naturalSize' property only considers the movie's first video track, so we need to compute + // the union of all visual track rects. + CGRect trackUnionRect = CGRectZero; + for (AVAssetTrack *track in tracks) { + CGSize trackSize = [track naturalSize]; + CGRect trackRect = CGRectMake(0, 0, trackSize.width, trackSize.height); + trackUnionRect = CGRectUnion(trackUnionRect, CGRectApplyAffineTransform(trackRect, [track preferredTransform])); + } + + // The movie is always displayed at 0,0 so move the track rect to the origin before using width and height. + trackUnionRect = CGRectOffset(trackUnionRect, trackUnionRect.origin.x, trackUnionRect.origin.y); + + // Also look at the asset's preferred transform so we account for a movie matrix. + CGSize naturalSize = CGSizeApplyAffineTransform(trackUnionRect.size, [m_avAsset.get() preferredTransform]); + + // Cache the natural size (setNaturalSize will notify the player if it has changed). + setNaturalSize(IntSize(naturalSize)); +} + +NSArray* assetMetadataKeyNames() +{ + static NSArray* keys; + if (!keys) { + keys = [[NSArray alloc] initWithObjects:@"duration", + @"naturalSize", + @"preferredTransform", + @"preferredVolume", + @"preferredRate", + @"playable", + @"tracks", + nil]; + } + return keys; +} + +NSArray* itemKVOProperties() +{ + static NSArray* keys; + if (!keys) { + keys = [[NSArray alloc] initWithObjects:@"presentationSize", + @"status", + @"asset", + @"tracks", + @"seekableTimeRanges", + @"loadedTimeRanges", + @"playbackLikelyToKeepUp", + @"playbackBufferFull", + @"playbackBufferEmpty", + nil]; + } + return keys; +} + +} // namespace WebCore + +@implementation WebCoreAVFMovieObserver + +- (id)initWithCallback:(MediaPlayerPrivateAVFoundationObjC*)callback +{ + m_callback = callback; + return [super init]; +} + +- (void)disconnect +{ + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + m_callback = 0; +} + +- (void)metadataLoaded +{ + if (!m_callback) + return; + + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::AssetMetadataLoaded); +} + +- (void)playableKnown +{ + if (!m_callback) + return; + + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::AssetPlayabilityKnown); +} + +- (void)timeChanged:(double)time +{ + if (!m_callback) + return; + + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::PlayerTimeChanged, time); +} + +- (void)didEnd:(NSNotification *)unusedNotification +{ + UNUSED_PARAM(unusedNotification); + if (!m_callback) + return; + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::ItemDidPlayToEndTime); +} + +- (void)observeValueForKeyPath:keyPath ofObject:(id)object change:(NSDictionary *)change context:(MediaPlayerAVFoundationObservationContext)context +{ + UNUSED_PARAM(change); + + LOG(Media, "WebCoreAVFMovieObserver:observeValueForKeyPath(%p) - keyPath = %s", self, [keyPath UTF8String]); + + if (!m_callback) + return; + + if (context == MediaPlayerAVFoundationObservationContextPlayerItem) { + // A value changed for an AVPlayerItem + if ([keyPath isEqualToString:@"status"]) + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::ItemStatusChanged); + else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::ItemIsPlaybackLikelyToKeepUpChanged); + else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::ItemIsPlaybackBufferEmptyChanged); + else if ([keyPath isEqualToString:@"playbackBufferFull"]) + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::ItemIsPlaybackBufferFullChanged); + else if ([keyPath isEqualToString:@"asset"]) + m_callback->setAsset([object asset]); + else if ([keyPath isEqualToString:@"loadedTimeRanges"]) + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::ItemLoadedTimeRangesChanged); + else if ([keyPath isEqualToString:@"seekableTimeRanges"]) + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::ItemSeekableTimeRangesChanged); + else if ([keyPath isEqualToString:@"tracks"]) + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::ItemTracksChanged); + else if ([keyPath isEqualToString:@"presentationSize"]) + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::ItemPresentationSizeChanged); + + return; + } + + if (context == MediaPlayerAVFoundationObservationContextPlayer) { + // A value changed for an AVPlayer. + if ([keyPath isEqualToString:@"rate"]) + m_callback->scheduleMainThreadNotification(MediaPlayerPrivateAVFoundation::Notification::PlayerRateChanged); +} +} + +@end + +#endif |