diff options
author | Steve Block <steveblock@google.com> | 2011-05-06 11:45:16 +0100 |
---|---|---|
committer | Steve Block <steveblock@google.com> | 2011-05-12 13:44:10 +0100 |
commit | cad810f21b803229eb11403f9209855525a25d57 (patch) | |
tree | 29a6fd0279be608e0fe9ffe9841f722f0f4e4269 /Source/WebCore/platform/graphics/mac/MediaPlayerPrivateQTKit.mm | |
parent | 121b0cf4517156d0ac5111caf9830c51b69bae8f (diff) | |
download | external_webkit-cad810f21b803229eb11403f9209855525a25d57.zip external_webkit-cad810f21b803229eb11403f9209855525a25d57.tar.gz external_webkit-cad810f21b803229eb11403f9209855525a25d57.tar.bz2 |
Merge WebKit at r75315: Initial merge by git.
Change-Id: I570314b346ce101c935ed22a626b48c2af266b84
Diffstat (limited to 'Source/WebCore/platform/graphics/mac/MediaPlayerPrivateQTKit.mm')
-rw-r--r-- | Source/WebCore/platform/graphics/mac/MediaPlayerPrivateQTKit.mm | 1673 |
1 files changed, 1673 insertions, 0 deletions
diff --git a/Source/WebCore/platform/graphics/mac/MediaPlayerPrivateQTKit.mm b/Source/WebCore/platform/graphics/mac/MediaPlayerPrivateQTKit.mm new file mode 100644 index 0000000..2361f6a --- /dev/null +++ b/Source/WebCore/platform/graphics/mac/MediaPlayerPrivateQTKit.mm @@ -0,0 +1,1673 @@ +/* + * Copyright (C) 2007, 2008, 2009, 2010 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) + +#import "MediaPlayerPrivateQTKit.h" + +#ifdef BUILDING_ON_TIGER +#import "AutodrainedPool.h" +#endif + +#import "BlockExceptions.h" +#import "FrameView.h" +#import "GraphicsContext.h" +#import "KURL.h" +#import "MIMETypeRegistry.h" +#import "SoftLinking.h" +#import "TimeRanges.h" +#import "WebCoreSystemInterface.h" +#import <QTKit/QTKit.h> +#import <objc/objc-runtime.h> +#import <wtf/UnusedParam.h> + +#if USE(ACCELERATED_COMPOSITING) +#include "GraphicsLayer.h" +#endif + +#if DRAW_FRAME_RATE +#import "Font.h" +#import "Frame.h" +#import "Document.h" +#import "RenderObject.h" +#import "RenderStyle.h" +#endif + +#ifdef BUILDING_ON_TIGER +static IMP method_setImplementation(Method m, IMP imp) +{ + IMP result = m->method_imp; + m->method_imp = imp; + return result; +} +#endif + +SOFT_LINK_FRAMEWORK(QTKit) + +SOFT_LINK(QTKit, QTMakeTime, QTTime, (long long timeValue, long timeScale), (timeValue, timeScale)) + +SOFT_LINK_CLASS(QTKit, QTMovie) +SOFT_LINK_CLASS(QTKit, QTMovieView) +SOFT_LINK_CLASS(QTKit, QTMovieLayer) + +SOFT_LINK_POINTER(QTKit, QTTrackMediaTypeAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMediaTypeAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMediaTypeBase, NSString *) +SOFT_LINK_POINTER(QTKit, QTMediaTypeMPEG, NSString *) +SOFT_LINK_POINTER(QTKit, QTMediaTypeSound, NSString *) +SOFT_LINK_POINTER(QTKit, QTMediaTypeText, NSString *) +SOFT_LINK_POINTER(QTKit, QTMediaTypeVideo, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieAskUnresolvedDataRefsAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieLoopsAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieDataSizeAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieDidEndNotification, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieHasVideoAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieHasAudioAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieIsActiveAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieLoadStateAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieLoadStateDidChangeNotification, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieNaturalSizeAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieCurrentSizeAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMoviePreventExternalURLLinksAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieRateChangesPreservePitchAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieRateDidChangeNotification, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieSizeDidChangeNotification, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieTimeDidChangeNotification, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieTimeScaleAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieURLAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieVolumeDidChangeNotification, NSString *) +SOFT_LINK_POINTER(QTKit, QTSecurityPolicyNoCrossSiteAttribute, NSString *) +SOFT_LINK_POINTER(QTKit, QTVideoRendererWebKitOnlyNewImageAvailableNotification, NSString *) +#ifndef BUILDING_ON_TIGER +SOFT_LINK_POINTER(QTKit, QTMovieApertureModeClean, NSString *) +SOFT_LINK_POINTER(QTKit, QTMovieApertureModeAttribute, NSString *) +#endif + +#define QTMovie getQTMovieClass() +#define QTMovieView getQTMovieViewClass() +#define QTMovieLayer getQTMovieLayerClass() + +#define QTTrackMediaTypeAttribute getQTTrackMediaTypeAttribute() +#define QTMediaTypeAttribute getQTMediaTypeAttribute() +#define QTMediaTypeBase getQTMediaTypeBase() +#define QTMediaTypeMPEG getQTMediaTypeMPEG() +#define QTMediaTypeSound getQTMediaTypeSound() +#define QTMediaTypeText getQTMediaTypeText() +#define QTMediaTypeVideo getQTMediaTypeVideo() +#define QTMovieAskUnresolvedDataRefsAttribute getQTMovieAskUnresolvedDataRefsAttribute() +#define QTMovieLoopsAttribute getQTMovieLoopsAttribute() +#define QTMovieDataSizeAttribute getQTMovieDataSizeAttribute() +#define QTMovieDidEndNotification getQTMovieDidEndNotification() +#define QTMovieHasVideoAttribute getQTMovieHasVideoAttribute() +#define QTMovieHasAudioAttribute getQTMovieHasAudioAttribute() +#define QTMovieIsActiveAttribute getQTMovieIsActiveAttribute() +#define QTMovieLoadStateAttribute getQTMovieLoadStateAttribute() +#define QTMovieLoadStateDidChangeNotification getQTMovieLoadStateDidChangeNotification() +#define QTMovieNaturalSizeAttribute getQTMovieNaturalSizeAttribute() +#define QTMovieCurrentSizeAttribute getQTMovieCurrentSizeAttribute() +#define QTMoviePreventExternalURLLinksAttribute getQTMoviePreventExternalURLLinksAttribute() +#define QTMovieRateChangesPreservePitchAttribute getQTMovieRateChangesPreservePitchAttribute() +#define QTMovieRateDidChangeNotification getQTMovieRateDidChangeNotification() +#define QTMovieSizeDidChangeNotification getQTMovieSizeDidChangeNotification() +#define QTMovieTimeDidChangeNotification getQTMovieTimeDidChangeNotification() +#define QTMovieTimeScaleAttribute getQTMovieTimeScaleAttribute() +#define QTMovieURLAttribute getQTMovieURLAttribute() +#define QTMovieVolumeDidChangeNotification getQTMovieVolumeDidChangeNotification() +#define QTSecurityPolicyNoCrossSiteAttribute getQTSecurityPolicyNoCrossSiteAttribute() +#define QTVideoRendererWebKitOnlyNewImageAvailableNotification getQTVideoRendererWebKitOnlyNewImageAvailableNotification() +#ifndef BUILDING_ON_TIGER +#define QTMovieApertureModeClean getQTMovieApertureModeClean() +#define QTMovieApertureModeAttribute getQTMovieApertureModeAttribute() +#endif + +// Older versions of the QTKit header don't have these constants. +#if !defined QTKIT_VERSION_MAX_ALLOWED || QTKIT_VERSION_MAX_ALLOWED <= QTKIT_VERSION_7_0 +enum { + QTMovieLoadStateError = -1L, + QTMovieLoadStateLoaded = 2000L, + QTMovieLoadStatePlayable = 10000L, + QTMovieLoadStatePlaythroughOK = 20000L, + QTMovieLoadStateComplete = 100000L +}; +#endif + +@interface FakeQTMovieView : NSObject +- (WebCoreMovieObserver *)delegate; +@end + +using namespace WebCore; +using namespace std; + +@interface WebCoreMovieObserver : NSObject +{ + MediaPlayerPrivateQTKit* m_callback; + NSView* m_view; + BOOL m_delayCallbacks; +} +-(id)initWithCallback:(MediaPlayerPrivateQTKit*)callback; +-(void)disconnect; +-(void)setView:(NSView*)view; +-(void)repaint; +-(void)setDelayCallbacks:(BOOL)shouldDelay; +-(void)loadStateChanged:(NSNotification *)notification; +-(void)rateChanged:(NSNotification *)notification; +-(void)sizeChanged:(NSNotification *)notification; +-(void)timeChanged:(NSNotification *)notification; +-(void)didEnd:(NSNotification *)notification; +@end + +@protocol WebKitVideoRenderingDetails +-(void)setMovie:(id)movie; +-(void)drawInRect:(NSRect)rect; +@end + +namespace WebCore { + +#ifdef BUILDING_ON_TIGER +static const long minimumQuickTimeVersion = 0x07300000; // 7.3 +#endif + + +MediaPlayerPrivateInterface* MediaPlayerPrivateQTKit::create(MediaPlayer* player) +{ + return new MediaPlayerPrivateQTKit(player); +} + +void MediaPlayerPrivateQTKit::registerMediaEngine(MediaEngineRegistrar registrar) +{ + if (isAvailable()) + registrar(create, getSupportedTypes, supportsType); +} + +MediaPlayerPrivateQTKit::MediaPlayerPrivateQTKit(MediaPlayer* player) + : m_player(player) + , m_objcObserver(AdoptNS, [[WebCoreMovieObserver alloc] initWithCallback:this]) + , m_seekTo(-1) + , m_seekTimer(this, &MediaPlayerPrivateQTKit::seekTimerFired) + , m_networkState(MediaPlayer::Empty) + , m_readyState(MediaPlayer::HaveNothing) + , m_rect() + , m_scaleFactor(1, 1) + , m_enabledTrackCount(0) + , m_totalTrackCount(0) + , m_reportedDuration(-1) + , m_cachedDuration(-1) + , m_timeToRestore(-1) + , m_preload(MediaPlayer::Auto) + , m_startedPlaying(false) + , m_isStreaming(false) + , m_visible(false) + , m_hasUnsupportedTracks(false) + , m_videoFrameHasDrawn(false) + , m_isAllowedToRender(false) +#if DRAW_FRAME_RATE + , m_frameCountWhilePlaying(0) + , m_timeStartedPlaying(0) + , m_timeStoppedPlaying(0) +#endif +{ +} + +MediaPlayerPrivateQTKit::~MediaPlayerPrivateQTKit() +{ + tearDownVideoRendering(); + + [[NSNotificationCenter defaultCenter] removeObserver:m_objcObserver.get()]; + [m_objcObserver.get() disconnect]; +} + +void MediaPlayerPrivateQTKit::createQTMovie(const String& url) +{ + NSURL *cocoaURL = KURL(ParsedURLString, url); + NSMutableDictionary *movieAttributes = [NSMutableDictionary dictionaryWithObjectsAndKeys: + cocoaURL, QTMovieURLAttribute, + [NSNumber numberWithBool:m_player->preservesPitch()], QTMovieRateChangesPreservePitchAttribute, + [NSNumber numberWithBool:YES], QTMoviePreventExternalURLLinksAttribute, + [NSNumber numberWithBool:YES], QTSecurityPolicyNoCrossSiteAttribute, + [NSNumber numberWithBool:NO], QTMovieAskUnresolvedDataRefsAttribute, + [NSNumber numberWithBool:NO], QTMovieLoopsAttribute, +#ifndef BUILDING_ON_TIGER + QTMovieApertureModeClean, QTMovieApertureModeAttribute, +#endif + nil]; + +#if !defined(BUILDING_ON_LEOPARD) + CFDictionaryRef proxySettings = CFNetworkCopySystemProxySettings(); + CFArrayRef proxiesForURL = CFNetworkCopyProxiesForURL((CFURLRef)cocoaURL, proxySettings); + BOOL willUseProxy = YES; + + if (!proxiesForURL || !CFArrayGetCount(proxiesForURL)) + willUseProxy = NO; + + if (CFArrayGetCount(proxiesForURL) == 1) { + CFDictionaryRef proxy = (CFDictionaryRef)CFArrayGetValueAtIndex(proxiesForURL, 0); + ASSERT(CFGetTypeID(proxy) == CFDictionaryGetTypeID()); + + CFStringRef proxyType = (CFStringRef)CFDictionaryGetValue(proxy, kCFProxyTypeKey); + ASSERT(CFGetTypeID(proxyType) == CFStringGetTypeID()); + + if (CFStringCompare(proxyType, kCFProxyTypeNone, 0) == kCFCompareEqualTo) + willUseProxy = NO; + } + + if (!willUseProxy) { + // Only pass the QTMovieOpenForPlaybackAttribute flag if there are no proxy servers, due + // to rdar://problem/7531776. + [movieAttributes setObject:[NSNumber numberWithBool:YES] forKey:@"QTMovieOpenForPlaybackAttribute"]; + } + + if (proxiesForURL) + CFRelease(proxiesForURL); + if (proxySettings) + CFRelease(proxySettings); +#endif + + createQTMovie(cocoaURL, movieAttributes); +} + +static void disableComponentsOnce() +{ + static bool sComponentsDisabled = false; + if (sComponentsDisabled) + return; + sComponentsDisabled = true; + + // eat/PDF and grip/PDF components must be disabled twice since they are registered twice + // with different flags. However, there is currently a bug in 64-bit QTKit (<rdar://problem/8378237>) + // which causes subsequent disable component requests of exactly the same type to be ignored if + // QTKitServer has not yet started. As a result, we must pass in exactly the flags we want to + // disable per component. As a failsafe, if in the future these flags change, we will disable the + // PDF components for a third time with a wildcard flags field: + uint32_t componentsToDisable[11][5] = { + {'eat ', 'TEXT', 'text', 0, 0}, + {'eat ', 'TXT ', 'text', 0, 0}, + {'eat ', 'utxt', 'text', 0, 0}, + {'eat ', 'TEXT', 'tx3g', 0, 0}, + {'eat ', 'PDF ', 'vide', 0x44802, 0}, + {'eat ', 'PDF ', 'vide', 0x45802, 0}, + {'eat ', 'PDF ', 'vide', 0, 0}, + {'grip', 'PDF ', 'appl', 0x844a00, 0}, + {'grip', 'PDF ', 'appl', 0x845a00, 0}, + {'grip', 'PDF ', 'appl', 0, 0}, + {'imdc', 'pdf ', 'appl', 0, 0}, + }; + + for (size_t i = 0; i < WTF_ARRAY_LENGTH(componentsToDisable); ++i) + wkQTMovieDisableComponent(componentsToDisable[i]); +} + +void MediaPlayerPrivateQTKit::createQTMovie(NSURL *url, NSDictionary *movieAttributes) +{ + disableComponentsOnce(); + + [[NSNotificationCenter defaultCenter] removeObserver:m_objcObserver.get()]; + + bool recreating = false; + if (m_qtMovie) { + recreating = true; + destroyQTVideoRenderer(); + m_qtMovie = 0; + } + + // Disable rtsp streams for now, <rdar://problem/5693967> + if (protocolIs([url scheme], "rtsp")) + return; + + NSError *error = nil; + m_qtMovie.adoptNS([[QTMovie alloc] initWithAttributes:movieAttributes error:&error]); + + if (!m_qtMovie) + return; + + [m_qtMovie.get() setVolume:m_player->volume()]; + + if (recreating && hasVideo()) + createQTVideoRenderer(QTVideoRendererModeListensForNewImages); + + [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get() + selector:@selector(loadStateChanged:) + name:QTMovieLoadStateDidChangeNotification + object:m_qtMovie.get()]; + + // In updateState(), we track when maxTimeLoaded() == duration(). + // In newer version of QuickTime, a notification is emitted when maxTimeLoaded changes. + // In older version of QuickTime, QTMovieLoadStateDidChangeNotification be fired. + if (NSString *maxTimeLoadedChangeNotification = wkQTMovieMaxTimeLoadedChangeNotification()) { + [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get() + selector:@selector(loadStateChanged:) + name:maxTimeLoadedChangeNotification + object:m_qtMovie.get()]; + } + + [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get() + selector:@selector(rateChanged:) + name:QTMovieRateDidChangeNotification + object:m_qtMovie.get()]; + [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get() + selector:@selector(sizeChanged:) + name:QTMovieSizeDidChangeNotification + object:m_qtMovie.get()]; + [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get() + selector:@selector(timeChanged:) + name:QTMovieTimeDidChangeNotification + object:m_qtMovie.get()]; + [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get() + selector:@selector(didEnd:) + name:QTMovieDidEndNotification + object:m_qtMovie.get()]; +} + +static void mainThreadSetNeedsDisplay(id self, SEL) +{ + id view = [self superview]; + ASSERT(!view || [view isKindOfClass:[QTMovieView class]]); + if (!view || ![view isKindOfClass:[QTMovieView class]]) + return; + + FakeQTMovieView *movieView = static_cast<FakeQTMovieView *>(view); + WebCoreMovieObserver* delegate = [movieView delegate]; + ASSERT(!delegate || [delegate isKindOfClass:[WebCoreMovieObserver class]]); + if (!delegate || ![delegate isKindOfClass:[WebCoreMovieObserver class]]) + return; + + [delegate repaint]; +} + +static Class QTVideoRendererClass() +{ + static Class QTVideoRendererWebKitOnlyClass = NSClassFromString(@"QTVideoRendererWebKitOnly"); + return QTVideoRendererWebKitOnlyClass; +} + +void MediaPlayerPrivateQTKit::createQTMovieView() +{ + detachQTMovieView(); + + static bool addedCustomMethods = false; + if (!m_player->inMediaDocument() && !addedCustomMethods) { + Class QTMovieContentViewClass = NSClassFromString(@"QTMovieContentView"); + ASSERT(QTMovieContentViewClass); + + Method mainThreadSetNeedsDisplayMethod = class_getInstanceMethod(QTMovieContentViewClass, @selector(_mainThreadSetNeedsDisplay)); + ASSERT(mainThreadSetNeedsDisplayMethod); + + method_setImplementation(mainThreadSetNeedsDisplayMethod, reinterpret_cast<IMP>(mainThreadSetNeedsDisplay)); + addedCustomMethods = true; + } + + // delay callbacks as we *will* get notifications during setup + [m_objcObserver.get() setDelayCallbacks:YES]; + + m_qtMovieView.adoptNS([[QTMovieView alloc] init]); + setSize(m_player->size()); + NSView* parentView = m_player->frameView()->documentView(); + [parentView addSubview:m_qtMovieView.get()]; +#ifdef BUILDING_ON_TIGER + // setDelegate: isn't a public call in Tiger, so use performSelector to keep the compiler happy + [m_qtMovieView.get() performSelector:@selector(setDelegate:) withObject:m_objcObserver.get()]; +#else + [m_qtMovieView.get() setDelegate:m_objcObserver.get()]; +#endif + [m_objcObserver.get() setView:m_qtMovieView.get()]; + [m_qtMovieView.get() setMovie:m_qtMovie.get()]; + [m_qtMovieView.get() setControllerVisible:NO]; + [m_qtMovieView.get() setPreservesAspectRatio:NO]; + // the area not covered by video should be transparent + [m_qtMovieView.get() setFillColor:[NSColor clearColor]]; + + // If we're in a media document, allow QTMovieView to render in its default mode; + // otherwise tell it to draw synchronously. + // Note that we expect mainThreadSetNeedsDisplay to be invoked only when synchronous drawing is requested. + if (!m_player->inMediaDocument()) + wkQTMovieViewSetDrawSynchronously(m_qtMovieView.get(), YES); + + [m_objcObserver.get() setDelayCallbacks:NO]; +} + +void MediaPlayerPrivateQTKit::detachQTMovieView() +{ + if (m_qtMovieView) { + [m_objcObserver.get() setView:nil]; +#ifdef BUILDING_ON_TIGER + // setDelegate: isn't a public call in Tiger, so use performSelector to keep the compiler happy + [m_qtMovieView.get() performSelector:@selector(setDelegate:) withObject:nil]; +#else + [m_qtMovieView.get() setDelegate:nil]; +#endif + [m_qtMovieView.get() removeFromSuperview]; + m_qtMovieView = nil; + } +} + +void MediaPlayerPrivateQTKit::createQTVideoRenderer(QTVideoRendererMode rendererMode) +{ + destroyQTVideoRenderer(); + + m_qtVideoRenderer.adoptNS([[QTVideoRendererClass() alloc] init]); + if (!m_qtVideoRenderer) + return; + + // associate our movie with our instance of QTVideoRendererWebKitOnly + [(id<WebKitVideoRenderingDetails>)m_qtVideoRenderer.get() setMovie:m_qtMovie.get()]; + + if (rendererMode == QTVideoRendererModeListensForNewImages) { + // listen to QTVideoRendererWebKitOnly's QTVideoRendererWebKitOnlyNewImageDidBecomeAvailableNotification + [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get() + selector:@selector(newImageAvailable:) + name:QTVideoRendererWebKitOnlyNewImageAvailableNotification + object:m_qtVideoRenderer.get()]; + } +} + +void MediaPlayerPrivateQTKit::destroyQTVideoRenderer() +{ + if (!m_qtVideoRenderer) + return; + + // stop observing the renderer's notifications before we toss it + [[NSNotificationCenter defaultCenter] removeObserver:m_objcObserver.get() + name:QTVideoRendererWebKitOnlyNewImageAvailableNotification + object:m_qtVideoRenderer.get()]; + + // disassociate our movie from our instance of QTVideoRendererWebKitOnly + [(id<WebKitVideoRenderingDetails>)m_qtVideoRenderer.get() setMovie:nil]; + + m_qtVideoRenderer = nil; +} + +void MediaPlayerPrivateQTKit::createQTMovieLayer() +{ +#if USE(ACCELERATED_COMPOSITING) + if (!m_qtMovie) + return; + + ASSERT(supportsAcceleratedRendering()); + + if (!m_qtVideoLayer) { + m_qtVideoLayer.adoptNS([[QTMovieLayer alloc] init]); + if (!m_qtVideoLayer) + return; + + [m_qtVideoLayer.get() setMovie:m_qtMovie.get()]; +#ifndef NDEBUG + [(CALayer *)m_qtVideoLayer.get() setName:@"Video layer"]; +#endif + // The layer will get hooked up via RenderLayerBacking::updateGraphicsLayerConfiguration(). + } +#endif +} + +void MediaPlayerPrivateQTKit::destroyQTMovieLayer() +{ +#if USE(ACCELERATED_COMPOSITING) + if (!m_qtVideoLayer) + return; + + // disassociate our movie from our instance of QTMovieLayer + [m_qtVideoLayer.get() setMovie:nil]; + m_qtVideoLayer = nil; +#endif +} + +MediaPlayerPrivateQTKit::MediaRenderingMode MediaPlayerPrivateQTKit::currentRenderingMode() const +{ + if (m_qtMovieView) + return MediaRenderingMovieView; + + if (m_qtVideoLayer) + return MediaRenderingMovieLayer; + + if (m_qtVideoRenderer) + return MediaRenderingSoftwareRenderer; + + return MediaRenderingNone; +} + +MediaPlayerPrivateQTKit::MediaRenderingMode MediaPlayerPrivateQTKit::preferredRenderingMode() const +{ + if (!m_player->frameView() || !m_qtMovie) + return MediaRenderingNone; + + if (m_player->inMediaDocument() || !QTVideoRendererClass()) + return MediaRenderingMovieView; + +#if USE(ACCELERATED_COMPOSITING) + if (supportsAcceleratedRendering() && m_player->mediaPlayerClient()->mediaPlayerRenderingCanBeAccelerated(m_player)) + return MediaRenderingMovieLayer; +#endif + + return MediaRenderingSoftwareRenderer; +} + +void MediaPlayerPrivateQTKit::setUpVideoRendering() +{ + if (!isReadyForVideoSetup()) + return; + + MediaRenderingMode currentMode = currentRenderingMode(); + MediaRenderingMode preferredMode = preferredRenderingMode(); + if (currentMode == preferredMode && currentMode != MediaRenderingNone) + return; + + if (currentMode != MediaRenderingNone) + tearDownVideoRendering(); + + switch (preferredMode) { + case MediaRenderingMovieView: + createQTMovieView(); + break; + case MediaRenderingNone: + case MediaRenderingSoftwareRenderer: + createQTVideoRenderer(QTVideoRendererModeListensForNewImages); + break; + case MediaRenderingMovieLayer: + createQTMovieLayer(); + break; + } + + // If using a movie layer, inform the client so the compositing tree is updated. + if (currentMode == MediaRenderingMovieLayer || preferredMode == MediaRenderingMovieLayer) + m_player->mediaPlayerClient()->mediaPlayerRenderingModeChanged(m_player); +} + +void MediaPlayerPrivateQTKit::tearDownVideoRendering() +{ + if (m_qtMovieView) + detachQTMovieView(); + if (m_qtVideoRenderer) + destroyQTVideoRenderer(); + if (m_qtVideoLayer) + destroyQTMovieLayer(); +} + +bool MediaPlayerPrivateQTKit::hasSetUpVideoRendering() const +{ + return m_qtMovieView + || m_qtVideoLayer + || m_qtVideoRenderer; +} + +QTTime MediaPlayerPrivateQTKit::createQTTime(float time) const +{ + if (!metaDataAvailable()) + return QTMakeTime(0, 600); + long timeScale = [[m_qtMovie.get() attributeForKey:QTMovieTimeScaleAttribute] longValue]; + return QTMakeTime(time * timeScale, timeScale); +} + +void MediaPlayerPrivateQTKit::resumeLoad() +{ + m_delayingLoad = false; + + if (!m_movieURL.isNull()) + loadInternal(m_movieURL); +} + +void MediaPlayerPrivateQTKit::load(const String& url) +{ + m_movieURL = url; + + // If the element is not supposed to load any data return immediately because QTKit + // doesn't have API to throttle loading. + if (m_preload == MediaPlayer::None) { + m_delayingLoad = true; + return; + } + + loadInternal(url); +} + +void MediaPlayerPrivateQTKit::loadInternal(const String& url) +{ + if (m_networkState != MediaPlayer::Loading) { + m_networkState = MediaPlayer::Loading; + m_player->networkStateChanged(); + } + if (m_readyState != MediaPlayer::HaveNothing) { + m_readyState = MediaPlayer::HaveNothing; + m_player->readyStateChanged(); + } + cancelSeek(); + m_videoFrameHasDrawn = false; + + [m_objcObserver.get() setDelayCallbacks:YES]; + + createQTMovie(url); + + [m_objcObserver.get() loadStateChanged:nil]; + [m_objcObserver.get() setDelayCallbacks:NO]; +} + +void MediaPlayerPrivateQTKit::prepareToPlay() +{ + if (!m_qtMovie || m_delayingLoad) + resumeLoad(); +} + +PlatformMedia MediaPlayerPrivateQTKit::platformMedia() const +{ + PlatformMedia pm; + pm.type = PlatformMedia::QTMovieType; + pm.media.qtMovie = m_qtMovie.get(); + return pm; +} + +#if USE(ACCELERATED_COMPOSITING) +PlatformLayer* MediaPlayerPrivateQTKit::platformLayer() const +{ + return m_qtVideoLayer.get(); +} +#endif + +void MediaPlayerPrivateQTKit::play() +{ + if (!metaDataAvailable()) + return; + m_startedPlaying = true; +#if DRAW_FRAME_RATE + m_frameCountWhilePlaying = 0; +#endif + [m_objcObserver.get() setDelayCallbacks:YES]; + [m_qtMovie.get() setRate:m_player->rate()]; + [m_objcObserver.get() setDelayCallbacks:NO]; +} + +void MediaPlayerPrivateQTKit::pause() +{ + if (!metaDataAvailable()) + return; + m_startedPlaying = false; +#if DRAW_FRAME_RATE + m_timeStoppedPlaying = [NSDate timeIntervalSinceReferenceDate]; +#endif + [m_objcObserver.get() setDelayCallbacks:YES]; + [m_qtMovie.get() stop]; + [m_objcObserver.get() setDelayCallbacks:NO]; +} + +float MediaPlayerPrivateQTKit::duration() const +{ + if (!metaDataAvailable()) + return 0; + + if (m_cachedDuration != -1.0f) + return m_cachedDuration; + + QTTime time = [m_qtMovie.get() duration]; + if (time.flags == kQTTimeIsIndefinite) + return numeric_limits<float>::infinity(); + return static_cast<float>(time.timeValue) / time.timeScale; +} + +float MediaPlayerPrivateQTKit::currentTime() const +{ + if (!metaDataAvailable()) + return 0; + QTTime time = [m_qtMovie.get() currentTime]; + return static_cast<float>(time.timeValue) / time.timeScale; +} + +void MediaPlayerPrivateQTKit::seek(float time) +{ + // Nothing to do if we are already in the middle of a seek to the same time. + if (time == m_seekTo) + return; + + cancelSeek(); + + if (!metaDataAvailable()) + return; + + if (time > duration()) + time = duration(); + + m_seekTo = time; + if (maxTimeSeekable() >= m_seekTo) + doSeek(); + else + m_seekTimer.start(0, 0.5f); +} + +void MediaPlayerPrivateQTKit::doSeek() +{ + QTTime qttime = createQTTime(m_seekTo); + // setCurrentTime generates several event callbacks, update afterwards + [m_objcObserver.get() setDelayCallbacks:YES]; + float oldRate = [m_qtMovie.get() rate]; + + if (oldRate) + [m_qtMovie.get() setRate:0]; + [m_qtMovie.get() setCurrentTime:qttime]; + + // restore playback only if not at end, otherwise QTMovie will loop + float timeAfterSeek = currentTime(); + if (oldRate && timeAfterSeek < duration()) + [m_qtMovie.get() setRate:oldRate]; + + cancelSeek(); + [m_objcObserver.get() setDelayCallbacks:NO]; +} + +void MediaPlayerPrivateQTKit::cancelSeek() +{ + m_seekTo = -1; + m_seekTimer.stop(); +} + +void MediaPlayerPrivateQTKit::seekTimerFired(Timer<MediaPlayerPrivateQTKit>*) +{ + if (!metaDataAvailable()|| !seeking() || currentTime() == m_seekTo) { + cancelSeek(); + updateStates(); + m_player->timeChanged(); + return; + } + + if (maxTimeSeekable() >= m_seekTo) + doSeek(); + else { + MediaPlayer::NetworkState state = networkState(); + if (state == MediaPlayer::Empty || state == MediaPlayer::Loaded) { + cancelSeek(); + updateStates(); + m_player->timeChanged(); + } + } +} + +bool MediaPlayerPrivateQTKit::paused() const +{ + if (!metaDataAvailable()) + return true; + return [m_qtMovie.get() rate] == 0; +} + +bool MediaPlayerPrivateQTKit::seeking() const +{ + if (!metaDataAvailable()) + return false; + return m_seekTo >= 0; +} + +IntSize MediaPlayerPrivateQTKit::naturalSize() const +{ + if (!metaDataAvailable()) + return IntSize(); + + // In spite of the name of this method, return QTMovieNaturalSizeAttribute transformed by the + // initial movie scale because the spec says intrinsic size is: + // + // ... the dimensions of the resource in CSS pixels after taking into account the resource's + // dimensions, aspect ratio, clean aperture, resolution, and so forth, as defined for the + // format used by the resource + + NSSize naturalSize = [[m_qtMovie.get() attributeForKey:QTMovieNaturalSizeAttribute] sizeValue]; + return IntSize(naturalSize.width * m_scaleFactor.width(), naturalSize.height * m_scaleFactor.height()); +} + +bool MediaPlayerPrivateQTKit::hasVideo() const +{ + if (!metaDataAvailable()) + return false; + return [[m_qtMovie.get() attributeForKey:QTMovieHasVideoAttribute] boolValue]; +} + +bool MediaPlayerPrivateQTKit::hasAudio() const +{ + if (!m_qtMovie) + return false; + return [[m_qtMovie.get() attributeForKey:QTMovieHasAudioAttribute] boolValue]; +} + +bool MediaPlayerPrivateQTKit::supportsFullscreen() const +{ +#if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + return true; +#else + // See <rdar://problem/7389945> + return false; +#endif +} + +void MediaPlayerPrivateQTKit::setVolume(float volume) +{ + if (m_qtMovie) + [m_qtMovie.get() setVolume:volume]; +} + +bool MediaPlayerPrivateQTKit::hasClosedCaptions() const +{ + if (!metaDataAvailable()) + return false; + return wkQTMovieHasClosedCaptions(m_qtMovie.get()); +} + +void MediaPlayerPrivateQTKit::setClosedCaptionsVisible(bool closedCaptionsVisible) +{ + if (metaDataAvailable()) { + wkQTMovieSetShowClosedCaptions(m_qtMovie.get(), closedCaptionsVisible); + +#if USE(ACCELERATED_COMPOSITING) && (!defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD)) + if (closedCaptionsVisible && m_qtVideoLayer) { + // Captions will be rendered upside down unless we flag the movie as flipped (again). See <rdar://7408440>. + [m_qtVideoLayer.get() setGeometryFlipped:YES]; + } +#endif + } +} + +void MediaPlayerPrivateQTKit::setRate(float rate) +{ + if (m_qtMovie) + [m_qtMovie.get() setRate:rate]; +} + +void MediaPlayerPrivateQTKit::setPreservesPitch(bool preservesPitch) +{ + if (!m_qtMovie) + return; + + // QTMovieRateChangesPreservePitchAttribute cannot be changed dynamically after QTMovie creation. + // If the passed in value is different than what already exists, we need to recreate the QTMovie for it to take effect. + if ([[m_qtMovie.get() attributeForKey:QTMovieRateChangesPreservePitchAttribute] boolValue] == preservesPitch) + return; + + RetainPtr<NSDictionary> movieAttributes(AdoptNS, [[m_qtMovie.get() movieAttributes] mutableCopy]); + ASSERT(movieAttributes); + [movieAttributes.get() setValue:[NSNumber numberWithBool:preservesPitch] forKey:QTMovieRateChangesPreservePitchAttribute]; + m_timeToRestore = currentTime(); + + createQTMovie([movieAttributes.get() valueForKey:QTMovieURLAttribute], movieAttributes.get()); +} + +PassRefPtr<TimeRanges> MediaPlayerPrivateQTKit::buffered() const +{ + RefPtr<TimeRanges> timeRanges = TimeRanges::create(); + float loaded = maxTimeLoaded(); + if (loaded > 0) + timeRanges->add(0, loaded); + return timeRanges.release(); +} + +float MediaPlayerPrivateQTKit::maxTimeSeekable() const +{ + if (!metaDataAvailable()) + return 0; + + // infinite duration means live stream + if (isinf(duration())) + return 0; + + return wkQTMovieMaxTimeSeekable(m_qtMovie.get()); +} + +float MediaPlayerPrivateQTKit::maxTimeLoaded() const +{ + if (!metaDataAvailable()) + return 0; + return wkQTMovieMaxTimeLoaded(m_qtMovie.get()); +} + +unsigned MediaPlayerPrivateQTKit::bytesLoaded() const +{ + float dur = duration(); + if (!dur) + return 0; + return totalBytes() * maxTimeLoaded() / dur; +} + +unsigned MediaPlayerPrivateQTKit::totalBytes() const +{ + if (!metaDataAvailable()) + return 0; + return [[m_qtMovie.get() attributeForKey:QTMovieDataSizeAttribute] intValue]; +} + +void MediaPlayerPrivateQTKit::cancelLoad() +{ + // FIXME: Is there a better way to check for this? + if (m_networkState < MediaPlayer::Loading || m_networkState == MediaPlayer::Loaded) + return; + + tearDownVideoRendering(); + m_qtMovie = nil; + + updateStates(); +} + +void MediaPlayerPrivateQTKit::cacheMovieScale() +{ + NSSize initialSize = NSZeroSize; + NSSize naturalSize = [[m_qtMovie.get() attributeForKey:QTMovieNaturalSizeAttribute] sizeValue]; + +#if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + // QTMovieCurrentSizeAttribute is not allowed with instances of QTMovie that have been + // opened with QTMovieOpenForPlaybackAttribute, so ask for the display transform attribute instead. + NSAffineTransform *displayTransform = [m_qtMovie.get() attributeForKey:@"QTMoviePreferredTransformAttribute"]; + if (displayTransform) + initialSize = [displayTransform transformSize:naturalSize]; + else { + initialSize.width = naturalSize.width; + initialSize.height = naturalSize.height; + } +#else + initialSize = [[m_qtMovie.get() attributeForKey:QTMovieCurrentSizeAttribute] sizeValue]; +#endif + + if (naturalSize.width) + m_scaleFactor.setWidth(initialSize.width / naturalSize.width); + if (naturalSize.height) + m_scaleFactor.setHeight(initialSize.height / naturalSize.height); +} + +bool MediaPlayerPrivateQTKit::isReadyForVideoSetup() const +{ + return m_readyState >= MediaPlayer::HaveMetadata && m_player->visible(); +} + +void MediaPlayerPrivateQTKit::prepareForRendering() +{ + if (m_isAllowedToRender) + return; + m_isAllowedToRender = true; + + if (!hasSetUpVideoRendering()) + setUpVideoRendering(); + + // If using a movie layer, inform the client so the compositing tree is updated. This is crucial if the movie + // has a poster, as it will most likely not have a layer and we will now be rendering frames to the movie layer. + if (currentRenderingMode() == MediaRenderingMovieLayer || preferredRenderingMode() == MediaRenderingMovieLayer) + m_player->mediaPlayerClient()->mediaPlayerRenderingModeChanged(m_player); +} + +void MediaPlayerPrivateQTKit::updateStates() +{ + MediaPlayer::NetworkState oldNetworkState = m_networkState; + MediaPlayer::ReadyState oldReadyState = m_readyState; + + long loadState = m_qtMovie ? [[m_qtMovie.get() attributeForKey:QTMovieLoadStateAttribute] longValue] : static_cast<long>(QTMovieLoadStateError); + + if (loadState >= QTMovieLoadStateLoaded && m_readyState < MediaPlayer::HaveMetadata) { + disableUnsupportedTracks(); + if (m_player->inMediaDocument()) { + if (!m_enabledTrackCount || m_hasUnsupportedTracks) { + // This has a type of media that we do not handle directly with a <video> + // element, eg. a rtsp track or QuickTime VR. Tell the MediaPlayerClient + // that we noticed. + sawUnsupportedTracks(); + return; + } + } else if (!m_enabledTrackCount) + loadState = QTMovieLoadStateError; + + if (loadState != QTMovieLoadStateError) { + wkQTMovieSelectPreferredAlternates(m_qtMovie.get()); + cacheMovieScale(); + MediaPlayer::MovieLoadType movieType = movieLoadType(); + m_isStreaming = movieType == MediaPlayer::StoredStream || movieType == MediaPlayer::LiveStream; + } + } + + // If this movie is reloading and we mean to restore the current time/rate, this might be the right time to do it. + if (loadState >= QTMovieLoadStateLoaded && oldNetworkState < MediaPlayer::Loaded && m_timeToRestore != -1.0f) { + QTTime qttime = createQTTime(m_timeToRestore); + m_timeToRestore = -1.0f; + + // Disable event callbacks from setCurrentTime for restoring time in a recreated video + [m_objcObserver.get() setDelayCallbacks:YES]; + [m_qtMovie.get() setCurrentTime:qttime]; + [m_qtMovie.get() setRate:m_player->rate()]; + [m_objcObserver.get() setDelayCallbacks:NO]; + } + + BOOL completelyLoaded = !m_isStreaming && (loadState >= QTMovieLoadStateComplete); + + // Note: QT indicates that we are fully loaded with QTMovieLoadStateComplete. + // However newer versions of QT do not, so we check maxTimeLoaded against duration. + if (!completelyLoaded && !m_isStreaming && metaDataAvailable()) + completelyLoaded = maxTimeLoaded() == duration(); + + if (completelyLoaded) { + // "Loaded" is reserved for fully buffered movies, never the case when streaming + m_networkState = MediaPlayer::Loaded; + m_readyState = MediaPlayer::HaveEnoughData; + } else if (loadState >= QTMovieLoadStatePlaythroughOK) { + m_readyState = MediaPlayer::HaveEnoughData; + m_networkState = MediaPlayer::Loading; + } else if (loadState >= QTMovieLoadStatePlayable) { + // FIXME: This might not work correctly in streaming case, <rdar://problem/5693967> + m_readyState = currentTime() < maxTimeLoaded() ? MediaPlayer::HaveFutureData : MediaPlayer::HaveCurrentData; + m_networkState = MediaPlayer::Loading; + } else if (loadState >= QTMovieLoadStateLoaded) { + m_readyState = MediaPlayer::HaveMetadata; + m_networkState = MediaPlayer::Loading; + } else if (loadState > QTMovieLoadStateError) { + m_readyState = MediaPlayer::HaveNothing; + m_networkState = MediaPlayer::Loading; + } else { + // Loading or decoding failed. + + if (m_player->inMediaDocument()) { + // Something went wrong in the loading of media within a standalone file. + // This can occur with chained refmovies pointing to streamed media. + sawUnsupportedTracks(); + return; + } + + float loaded = maxTimeLoaded(); + if (!loaded) + m_readyState = MediaPlayer::HaveNothing; + + if (!m_enabledTrackCount) + m_networkState = MediaPlayer::FormatError; + else { + // FIXME: We should differentiate between load/network errors and decode errors <rdar://problem/5605692> + if (loaded > 0) + m_networkState = MediaPlayer::DecodeError; + else + m_readyState = MediaPlayer::HaveNothing; + } + } + + if (isReadyForVideoSetup() && !hasSetUpVideoRendering()) + setUpVideoRendering(); + + if (seeking()) + m_readyState = m_readyState >= MediaPlayer::HaveMetadata ? MediaPlayer::HaveMetadata : MediaPlayer::HaveNothing; + + // Streaming movies don't use the network when paused. + if (m_isStreaming && m_readyState >= MediaPlayer::HaveMetadata && m_networkState >= MediaPlayer::Loading && [m_qtMovie.get() rate] == 0) + m_networkState = MediaPlayer::Idle; + + if (m_networkState != oldNetworkState) + m_player->networkStateChanged(); + + if (m_readyState != oldReadyState) + m_player->readyStateChanged(); + + if (loadState >= QTMovieLoadStateLoaded) { + float dur = duration(); + if (dur != m_reportedDuration) { + if (m_reportedDuration != -1.0f) + m_player->durationChanged(); + m_reportedDuration = dur; + } + } +} + +void MediaPlayerPrivateQTKit::loadStateChanged() +{ + if (!m_hasUnsupportedTracks) + updateStates(); +} + +void MediaPlayerPrivateQTKit::rateChanged() +{ + if (m_hasUnsupportedTracks) + return; + + updateStates(); + m_player->rateChanged(); +} + +void MediaPlayerPrivateQTKit::sizeChanged() +{ + if (!m_hasUnsupportedTracks) + m_player->sizeChanged(); +} + +void MediaPlayerPrivateQTKit::timeChanged() +{ + if (m_hasUnsupportedTracks) + return; + + // It may not be possible to seek to a specific time in a streamed movie. When seeking in a + // stream QuickTime sets the movie time to closest time possible and posts a timechanged + // notification. Update m_seekTo so we can detect when the seek completes. + if (m_seekTo != -1) + m_seekTo = currentTime(); + + m_timeToRestore = -1.0f; + updateStates(); + m_player->timeChanged(); +} + +void MediaPlayerPrivateQTKit::didEnd() +{ + if (m_hasUnsupportedTracks) + return; + + m_startedPlaying = false; +#if DRAW_FRAME_RATE + m_timeStoppedPlaying = [NSDate timeIntervalSinceReferenceDate]; +#endif + + // Hang onto the current time and use it as duration from now on since QuickTime is telling us we + // are at the end. Do this because QuickTime sometimes reports one time for duration and stops + // playback at another time, which causes problems in HTMLMediaElement. QTKit's 'ended' event + // fires when playing in reverse so don't update duration when at time zero! + float now = currentTime(); + if (now > 0) + m_cachedDuration = now; + + updateStates(); + m_player->timeChanged(); +} + +void MediaPlayerPrivateQTKit::setSize(const IntSize&) +{ + // Don't resize the view now because [view setFrame] also resizes the movie itself, and because + // the renderer calls this function immediately when we report a size change (QTMovieSizeDidChangeNotification) + // we can get into a feedback loop observing the size change and resetting the size, and this can cause + // QuickTime to miss resetting a movie's size when the media size changes (as happens with an rtsp movie + // once the rtsp server sends the track sizes). Instead we remember the size passed to paint() and resize + // the view when it changes. + // <rdar://problem/6336092> REGRESSION: rtsp movie does not resize correctly +} + +void MediaPlayerPrivateQTKit::setVisible(bool b) +{ + if (m_visible != b) { + m_visible = b; + if (b) + setUpVideoRendering(); + else + tearDownVideoRendering(); + } +} + +bool MediaPlayerPrivateQTKit::hasAvailableVideoFrame() const +{ + // When using a QTMovieLayer return true as soon as the movie reaches QTMovieLoadStatePlayable + // because although we don't *know* when the first frame has decoded, by the time we get and + // process the notification a frame should have propagated the VisualContext and been set on + // the layer. + if (currentRenderingMode() == MediaRenderingMovieLayer) + return m_readyState >= MediaPlayer::HaveCurrentData; + + // When using the software renderer QuickTime signals that a frame is available so we might as well + // wait until we know that a frame has been drawn. + return m_videoFrameHasDrawn; +} + +void MediaPlayerPrivateQTKit::repaint() +{ + if (m_hasUnsupportedTracks) + return; + +#if DRAW_FRAME_RATE + if (m_startedPlaying) { + m_frameCountWhilePlaying++; + // to eliminate preroll costs from our calculation, + // our frame rate calculation excludes the first frame drawn after playback starts + if (1==m_frameCountWhilePlaying) + m_timeStartedPlaying = [NSDate timeIntervalSinceReferenceDate]; + } +#endif + m_videoFrameHasDrawn = true; + m_player->repaint(); +} + +void MediaPlayerPrivateQTKit::paintCurrentFrameInContext(GraphicsContext* context, const IntRect& r) +{ + id qtVideoRenderer = m_qtVideoRenderer.get(); + if (!qtVideoRenderer && currentRenderingMode() == MediaRenderingMovieLayer) { + // We're being told to render into a context, but we already have the + // MovieLayer going. This probably means we've been called from <canvas>. + // Set up a QTVideoRenderer to use, but one that doesn't register for + // update callbacks. That way, it won't bother us asking to repaint. + createQTVideoRenderer(QTVideoRendererModeDefault); + qtVideoRenderer = m_qtVideoRenderer.get(); + } + paint(context, r); +} + +void MediaPlayerPrivateQTKit::paint(GraphicsContext* context, const IntRect& r) +{ + if (context->paintingDisabled() || m_hasUnsupportedTracks) + return; + NSView *view = m_qtMovieView.get(); + id qtVideoRenderer = m_qtVideoRenderer.get(); + if (!view && !qtVideoRenderer) + return; + + [m_objcObserver.get() setDelayCallbacks:YES]; + BEGIN_BLOCK_OBJC_EXCEPTIONS; + context->save(); + context->translate(r.x(), r.y() + r.height()); + context->scale(FloatSize(1.0f, -1.0f)); + context->setImageInterpolationQuality(InterpolationLow); + IntRect paintRect(IntPoint(0, 0), IntSize(r.width(), r.height())); + +#ifdef BUILDING_ON_TIGER + AutodrainedPool pool; +#endif + NSGraphicsContext* newContext = [NSGraphicsContext graphicsContextWithGraphicsPort:context->platformContext() flipped:NO]; + + // draw the current video frame + if (qtVideoRenderer) { + [NSGraphicsContext saveGraphicsState]; + [NSGraphicsContext setCurrentContext:newContext]; + [(id<WebKitVideoRenderingDetails>)qtVideoRenderer drawInRect:paintRect]; + [NSGraphicsContext restoreGraphicsState]; + } else { + if (m_rect != r) { + m_rect = r; + if (m_player->inMediaDocument()) { + // the QTMovieView needs to be placed in the proper location for document mode + [view setFrame:m_rect]; + } + else { + // We don't really need the QTMovieView in any specific location so let's just get it out of the way + // where it won't intercept events or try to bring up the context menu. + IntRect farAwayButCorrectSize(m_rect); + farAwayButCorrectSize.move(-1000000, -1000000); + [view setFrame:farAwayButCorrectSize]; + } + } + + if (m_player->inMediaDocument()) { + // If we're using a QTMovieView in a media document, the view may get layer-backed. AppKit won't update + // the layer hosting correctly if we call displayRectIgnoringOpacity:inContext:, so use displayRectIgnoringOpacity: + // in this case. See <rdar://problem/6702882>. + [view displayRectIgnoringOpacity:paintRect]; + } else + [view displayRectIgnoringOpacity:paintRect inContext:newContext]; + } + +#if DRAW_FRAME_RATE + // Draw the frame rate only after having played more than 10 frames. + if (m_frameCountWhilePlaying > 10) { + Frame* frame = m_player->frameView() ? m_player->frameView()->frame() : NULL; + Document* document = frame ? frame->document() : NULL; + RenderObject* renderer = document ? document->renderer() : NULL; + RenderStyle* styleToUse = renderer ? renderer->style() : NULL; + if (styleToUse) { + double frameRate = (m_frameCountWhilePlaying - 1) / ( m_startedPlaying ? ([NSDate timeIntervalSinceReferenceDate] - m_timeStartedPlaying) : + (m_timeStoppedPlaying - m_timeStartedPlaying) ); + String text = String::format("%1.2f", frameRate); + TextRun textRun(text.characters(), text.length()); + const Color color(255, 0, 0); + context->scale(FloatSize(1.0f, -1.0f)); + context->setStrokeColor(color, styleToUse->colorSpace()); + context->setStrokeStyle(SolidStroke); + context->setStrokeThickness(1.0f); + context->setFillColor(color, styleToUse->colorSpace()); + context->drawText(styleToUse->font(), textRun, IntPoint(2, -3)); + } + } +#endif + + context->restore(); + END_BLOCK_OBJC_EXCEPTIONS; + [m_objcObserver.get() setDelayCallbacks:NO]; +} + +static void addFileTypesToCache(NSArray * fileTypes, HashSet<String> &cache) +{ + int count = [fileTypes count]; + for (int n = 0; n < count; n++) { + CFStringRef ext = reinterpret_cast<CFStringRef>([fileTypes objectAtIndex:n]); + RetainPtr<CFStringRef> uti(AdoptCF, UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext, NULL)); + if (!uti) + continue; + RetainPtr<CFStringRef> mime(AdoptCF, UTTypeCopyPreferredTagWithClass(uti.get(), kUTTagClassMIMEType)); + if (mime) + cache.add(mime.get()); + + // -movieFileTypes: returns both file extensions and OSTypes. The later are surrounded by single + // quotes, eg. 'MooV', so don't bother looking at those. + if (CFStringGetCharacterAtIndex(ext, 0) != '\'') { + // UTI is missing many media related MIME types supported by QTKit (see rdar://6434168), and not all + // web servers use the MIME type UTI returns for an extension (see rdar://7875393), so even if UTI + // has a type for this extension add any types in hard coded table in the MIME type regsitry. + Vector<String> typesForExtension = MIMETypeRegistry::getMediaMIMETypesForExtension(ext); + unsigned count = typesForExtension.size(); + for (unsigned ndx = 0; ndx < count; ++ndx) { + if (!cache.contains(typesForExtension[ndx])) + cache.add(typesForExtension[ndx]); + } + } + } +} + +static HashSet<String> mimeCommonTypesCache() +{ + DEFINE_STATIC_LOCAL(HashSet<String>, cache, ()); + static bool typeListInitialized = false; + + if (!typeListInitialized) { + typeListInitialized = true; + NSArray* fileTypes = [QTMovie movieFileTypes:QTIncludeCommonTypes]; + addFileTypesToCache(fileTypes, cache); + } + + return cache; +} + +static HashSet<String> mimeModernTypesCache() +{ + DEFINE_STATIC_LOCAL(HashSet<String>, cache, ()); + static bool typeListInitialized = false; + + if (!typeListInitialized) { + typeListInitialized = true; + NSArray* fileTypes = [QTMovie movieFileTypes:(QTMovieFileTypeOptions)wkQTIncludeOnlyModernMediaFileTypes()]; + addFileTypesToCache(fileTypes, cache); + } + + return cache; +} + +void MediaPlayerPrivateQTKit::getSupportedTypes(HashSet<String>& supportedTypes) +{ + supportedTypes = mimeModernTypesCache(); + + // Note: this method starts QTKitServer if it isn't already running when in 64-bit because it has to return the list + // of every MIME type supported by QTKit. + HashSet<String> commonTypes = mimeCommonTypesCache(); + HashSet<String>::const_iterator it = commonTypes.begin(); + HashSet<String>::const_iterator end = commonTypes.end(); + for (; it != end; ++it) + supportedTypes.add(*it); +} + +MediaPlayer::SupportsType MediaPlayerPrivateQTKit::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 QT if it supports an + // extended MIME type yet. + + // We check the "modern" type cache first, as it doesn't require QTKitServer to start. + if (mimeModernTypesCache().contains(type) || mimeCommonTypesCache().contains(type)) + return codecs.isEmpty() ? MediaPlayer::MayBeSupported : MediaPlayer::IsSupported; + + return MediaPlayer::IsNotSupported; +} + +bool MediaPlayerPrivateQTKit::isAvailable() +{ +#ifdef BUILDING_ON_TIGER + SInt32 version; + OSErr result; + result = Gestalt(gestaltQuickTime, &version); + if (result != noErr) { + LOG_ERROR("No QuickTime available. Disabling <video> and <audio> support."); + return false; + } + if (version < minimumQuickTimeVersion) { + LOG_ERROR("QuickTime version %x detected, at least %x required. Disabling <video> and <audio> support.", version, minimumQuickTimeVersion); + return false; + } + return true; +#else + // On 10.5 and higher, QuickTime will always be new enough for <video> and <audio> support, so we just check that the framework can be loaded. + return QTKitLibrary(); +#endif +} + +void MediaPlayerPrivateQTKit::disableUnsupportedTracks() +{ + if (!m_qtMovie) { + m_enabledTrackCount = 0; + m_totalTrackCount = 0; + return; + } + + static HashSet<String>* allowedTrackTypes = 0; + if (!allowedTrackTypes) { + allowedTrackTypes = new HashSet<String>; + allowedTrackTypes->add(QTMediaTypeVideo); + allowedTrackTypes->add(QTMediaTypeSound); + allowedTrackTypes->add(QTMediaTypeText); + allowedTrackTypes->add(QTMediaTypeBase); + allowedTrackTypes->add(QTMediaTypeMPEG); + allowedTrackTypes->add("clcp"); // Closed caption + allowedTrackTypes->add("sbtl"); // Subtitle + allowedTrackTypes->add("odsm"); // MPEG-4 object descriptor stream + allowedTrackTypes->add("sdsm"); // MPEG-4 scene description stream + allowedTrackTypes->add("tmcd"); // timecode + allowedTrackTypes->add("tc64"); // timcode-64 + allowedTrackTypes->add("tmet"); // timed metadata + } + + NSArray *tracks = [m_qtMovie.get() tracks]; + + m_totalTrackCount = [tracks count]; + m_enabledTrackCount = m_totalTrackCount; + for (unsigned trackIndex = 0; trackIndex < m_totalTrackCount; trackIndex++) { + // Grab the track at the current index. If there isn't one there, then + // we can move onto the next one. + QTTrack *track = [tracks objectAtIndex:trackIndex]; + if (!track) + continue; + + // Check to see if the track is disabled already, we should move along. + // We don't need to re-disable it. + if (![track isEnabled]) { + --m_enabledTrackCount; + continue; + } + + // Get the track's media type. + NSString *mediaType = [track attributeForKey:QTTrackMediaTypeAttribute]; + if (!mediaType) + continue; + + // Test whether the media type is in our white list. + if (!allowedTrackTypes->contains(mediaType)) { + // If this track type is not allowed, then we need to disable it. + [track setEnabled:NO]; + --m_enabledTrackCount; + m_hasUnsupportedTracks = true; + } + + // Disable chapter tracks. These are most likely to lead to trouble, as + // they will be composited under the video tracks, forcing QT to do extra + // work. + QTTrack *chapterTrack = [track performSelector:@selector(chapterlist)]; + if (!chapterTrack) + continue; + + // Try to grab the media for the track. + QTMedia *chapterMedia = [chapterTrack media]; + if (!chapterMedia) + continue; + + // Grab the media type for this track. + id chapterMediaType = [chapterMedia attributeForKey:QTMediaTypeAttribute]; + if (!chapterMediaType) + continue; + + // Check to see if the track is a video track. We don't care about + // other non-video tracks. + if (![chapterMediaType isEqual:QTMediaTypeVideo]) + continue; + + // Check to see if the track is already disabled. If it is, we + // should move along. + if (![chapterTrack isEnabled]) + continue; + + // Disable the evil, evil track. + [chapterTrack setEnabled:NO]; + --m_enabledTrackCount; + m_hasUnsupportedTracks = true; + } +} + +void MediaPlayerPrivateQTKit::sawUnsupportedTracks() +{ + m_hasUnsupportedTracks = true; + m_player->mediaPlayerClient()->mediaPlayerSawUnsupportedTracks(m_player); +} + +#if USE(ACCELERATED_COMPOSITING) +bool MediaPlayerPrivateQTKit::supportsAcceleratedRendering() const +{ + // Also don't claim to support accelerated rendering when in the media document, as we will then render + // via QTMovieView which is already accelerated. + return isReadyForVideoSetup() && getQTMovieLayerClass() != Nil && !m_player->inMediaDocument(); +} + +void MediaPlayerPrivateQTKit::acceleratedRenderingStateChanged() +{ + // Set up or change the rendering path if necessary. + setUpVideoRendering(); +} +#endif + +bool MediaPlayerPrivateQTKit::hasSingleSecurityOrigin() const +{ + // We tell quicktime to disallow resources that come from different origins + // so we know all media is single origin. + return true; +} + +MediaPlayer::MovieLoadType MediaPlayerPrivateQTKit::movieLoadType() const +{ + if (!m_qtMovie) + return MediaPlayer::Unknown; + + MediaPlayer::MovieLoadType movieType = (MediaPlayer::MovieLoadType)wkQTMovieGetType(m_qtMovie.get()); + + // Can't include WebKitSystemInterface from WebCore so we can't get the enum returned + // by wkQTMovieGetType, but at least verify that the value is in the valid range. + ASSERT(movieType >= MediaPlayer::Unknown && movieType <= MediaPlayer::LiveStream); + + return movieType; +} + +void MediaPlayerPrivateQTKit::setPreload(MediaPlayer::Preload preload) +{ + m_preload = preload; + if (m_delayingLoad && m_preload != MediaPlayer::None) + resumeLoad(); +} + +float MediaPlayerPrivateQTKit::mediaTimeForTimeValue(float timeValue) const +{ + if (!metaDataAvailable()) + return timeValue; + + QTTime qttime = createQTTime(timeValue); + return static_cast<float>(qttime.timeValue) / qttime.timeScale; +} + +} // namespace WebCore + +@implementation WebCoreMovieObserver + +- (id)initWithCallback:(MediaPlayerPrivateQTKit*)callback +{ + m_callback = callback; + return [super init]; +} + +- (void)disconnect +{ + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + m_callback = 0; +} + +-(NSMenu*)menuForEventDelegate:(NSEvent*)theEvent +{ + // Get the contextual menu from the QTMovieView's superview, the frame view + return [[m_view superview] menuForEvent:theEvent]; +} + +-(void)setView:(NSView*)view +{ + m_view = view; +} + +-(void)repaint +{ + if (m_delayCallbacks) + [self performSelector:_cmd withObject:nil afterDelay:0.]; + else if (m_callback) + m_callback->repaint(); +} + +- (void)loadStateChanged:(NSNotification *)unusedNotification +{ + UNUSED_PARAM(unusedNotification); + if (m_delayCallbacks) + [self performSelector:_cmd withObject:nil afterDelay:0]; + else + m_callback->loadStateChanged(); +} + +- (void)rateChanged:(NSNotification *)unusedNotification +{ + UNUSED_PARAM(unusedNotification); + if (m_delayCallbacks) + [self performSelector:_cmd withObject:nil afterDelay:0]; + else + m_callback->rateChanged(); +} + +- (void)sizeChanged:(NSNotification *)unusedNotification +{ + UNUSED_PARAM(unusedNotification); + if (m_delayCallbacks) + [self performSelector:_cmd withObject:nil afterDelay:0]; + else + m_callback->sizeChanged(); +} + +- (void)timeChanged:(NSNotification *)unusedNotification +{ + UNUSED_PARAM(unusedNotification); + if (m_delayCallbacks) + [self performSelector:_cmd withObject:nil afterDelay:0]; + else + m_callback->timeChanged(); +} + +- (void)didEnd:(NSNotification *)unusedNotification +{ + UNUSED_PARAM(unusedNotification); + if (m_delayCallbacks) + [self performSelector:_cmd withObject:nil afterDelay:0]; + else + m_callback->didEnd(); +} + +- (void)newImageAvailable:(NSNotification *)unusedNotification +{ + UNUSED_PARAM(unusedNotification); + [self repaint]; +} + +- (void)setDelayCallbacks:(BOOL)shouldDelay +{ + m_delayCallbacks = shouldDelay; +} + +@end + +#endif |