MediaPlayerPrivateQTKit.mm   [plain text]


/*
 * Copyright (C) 2007-2017 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"

#if ENABLE(VIDEO) && USE(QTKIT)

#import "MediaPlayerPrivateQTKit.h"

#import "DocumentLoader.h"
#import "GraphicsContext.h"
#import "Logging.h"
#import "MIMETypeRegistry.h"
#import "MediaTimeQTKit.h"
#import "PlatformLayer.h"
#import "PlatformTimeRanges.h"
#import "SecurityOrigin.h"
#import "URL.h"
#import "UTIUtilities.h"
#import <objc/runtime.h>
#import <pal/spi/mac/QTKitSPI.h>
#import <wtf/BlockObjCExceptions.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/SoftLinking.h>

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"

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, QTMovieLayer)
SOFT_LINK_CLASS(QTKit, QTUtilities)

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, QTMovieDataAttribute, 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, QTMovieLoadStateErrorAttribute, NSString *)
SOFT_LINK_POINTER(QTKit, QTMovieLoadStateDidChangeNotification, NSString *)
SOFT_LINK_POINTER(QTKit, QTMovieLoadedRangesDidChangeNotification, 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 *)
SOFT_LINK_POINTER(QTKit, QTMovieApertureModeClean, NSString *)
SOFT_LINK_POINTER(QTKit, QTMovieApertureModeAttribute, NSString *)

SOFT_LINK_POINTER_OPTIONAL(QTKit, QTSecurityPolicyNoLocalToRemoteSiteAttribute, NSString *)
SOFT_LINK_POINTER_OPTIONAL(QTKit, QTSecurityPolicyNoRemoteToLocalSiteAttribute, NSString *)

@interface QTMovie(WebKitExtras)
- (QTTime)maxTimeLoaded;
- (NSArray *)availableRanges;
- (NSArray *)loadedRanges;
@end

#define QTMovieLayer getQTMovieLayerClass()
#define QTUtilities getQTUtilitiesClass()

#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 QTMovieDataAttribute getQTMovieDataAttribute()
#define QTMovieDataSizeAttribute getQTMovieDataSizeAttribute()
#define QTMovieDidEndNotification getQTMovieDidEndNotification()
#define QTMovieHasVideoAttribute getQTMovieHasVideoAttribute()
#define QTMovieHasAudioAttribute getQTMovieHasAudioAttribute()
#define QTMovieIsActiveAttribute getQTMovieIsActiveAttribute()
#define QTMovieLoadStateAttribute getQTMovieLoadStateAttribute()
#define QTMovieLoadStateErrorAttribute getQTMovieLoadStateErrorAttribute()
#define QTMovieLoadStateDidChangeNotification getQTMovieLoadStateDidChangeNotification()
#define QTMovieLoadedRangesDidChangeNotification getQTMovieLoadedRangesDidChangeNotification()
#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 QTSecurityPolicyNoLocalToRemoteSiteAttribute getQTSecurityPolicyNoLocalToRemoteSiteAttribute()
#define QTSecurityPolicyNoRemoteToLocalSiteAttribute getQTSecurityPolicyNoRemoteToLocalSiteAttribute()
#define QTVideoRendererWebKitOnlyNewImageAvailableNotification getQTVideoRendererWebKitOnlyNewImageAvailableNotification()
#define QTMovieApertureModeClean getQTMovieApertureModeClean()
#define QTMovieApertureModeAttribute getQTMovieApertureModeAttribute()

// 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

using namespace WebCore;

@interface WebCoreMovieObserver : NSObject
{
    MediaPlayerPrivateQTKit* m_callback;
    BOOL m_delayCallbacks;
}
-(id)initWithCallback:(MediaPlayerPrivateQTKit*)callback;
-(void)disconnect;
-(void)repaint;
-(void)setDelayCallbacks:(BOOL)shouldDelay;
-(void)loadStateChanged:(NSNotification *)notification;
- (void)loadedRangesChanged:(NSNotification *)notification;
-(void)rateChanged:(NSNotification *)notification;
-(void)sizeChanged:(NSNotification *)notification;
-(void)timeChanged:(NSNotification *)notification;
-(void)didEnd:(NSNotification *)notification;
-(void)layerHostChanged:(NSNotification *)notification;
- (void)newImageAvailable:(NSNotification *)notification;
@end

@protocol WebKitVideoRenderingDetails
-(void)setMovie:(id)movie;
-(void)drawInRect:(NSRect)rect;
@end

namespace WebCore {

void MediaPlayerPrivateQTKit::registerMediaEngine(MediaEngineRegistrar registrar)
{
    if (isAvailable())
        registrar([](MediaPlayer* player) { return std::make_unique<MediaPlayerPrivateQTKit>(player); }, getSupportedTypes,
            supportsType, originsInMediaCache, clearMediaCache, clearMediaCacheForOrigins, 0);
}

MediaPlayerPrivateQTKit::MediaPlayerPrivateQTKit(MediaPlayer* player)
    : m_player(player)
    , m_objcObserver(adoptNS([[WebCoreMovieObserver alloc] initWithCallback:this]))
    , m_seekTo(MediaTime::invalidTime())
    , 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(MediaTime::invalidTime())
    , m_cachedDuration(MediaTime::invalidTime())
    , m_timeToRestore(MediaTime::invalidTime())
    , m_preload(MediaPlayer::Auto)
    , m_startedPlaying(false)
    , m_isStreaming(false)
    , m_visible(false)
    , m_hasUnsupportedTracks(false)
    , m_videoFrameHasDrawn(false)
    , m_isAllowedToRender(false)
    , m_privateBrowsing(false)
{
}

MediaPlayerPrivateQTKit::~MediaPlayerPrivateQTKit()
{
    LOG(Media, "MediaPlayerPrivateQTKit::~MediaPlayerPrivateQTKit(%p)", this);
    tearDownVideoRendering();

    [[NSNotificationCenter defaultCenter] removeObserver:m_objcObserver.get()];
    [m_objcObserver.get() disconnect];
}

NSMutableDictionary *MediaPlayerPrivateQTKit::commonMovieAttributes() 
{
    NSMutableDictionary *movieAttributes = [NSMutableDictionary dictionaryWithObjectsAndKeys:
            [NSNumber numberWithBool:m_player->preservesPitch()], QTMovieRateChangesPreservePitchAttribute,
            [NSNumber numberWithBool:YES], QTMoviePreventExternalURLLinksAttribute,
            [NSNumber numberWithBool:NO], QTMovieAskUnresolvedDataRefsAttribute,
            [NSNumber numberWithBool:NO], QTMovieLoopsAttribute,
            [NSNumber numberWithBool:!m_privateBrowsing], @"QTMovieAllowPersistentCacheAttribute",
            QTMovieApertureModeClean, QTMovieApertureModeAttribute,
            nil];

    // Check to see if QTSecurityPolicyNoRemoteToLocalSiteAttribute is defined, which was added in QTKit 7.6.3.
    // If not, just set NoCrossSite = YES, which does the same thing as NoRemoteToLocal = YES and 
    // NoLocalToRemote = YES in QTKit < 7.6.3.
    if (QTSecurityPolicyNoRemoteToLocalSiteAttribute) {
        [movieAttributes setValue:[NSNumber numberWithBool:NO] forKey:QTSecurityPolicyNoCrossSiteAttribute];
        [movieAttributes setValue:[NSNumber numberWithBool:YES] forKey:QTSecurityPolicyNoRemoteToLocalSiteAttribute];
        [movieAttributes setValue:[NSNumber numberWithBool:YES] forKey:QTSecurityPolicyNoLocalToRemoteSiteAttribute];
    } else
        [movieAttributes setValue:[NSNumber numberWithBool:YES] forKey:QTSecurityPolicyNoCrossSiteAttribute];

    if (m_preload < MediaPlayer::Auto)
        [movieAttributes setValue:[NSNumber numberWithBool:YES] forKey:@"QTMovieLimitReadAheadAttribute"];

    return movieAttributes;
}

void MediaPlayerPrivateQTKit::createQTMovie(const String& url)
{
    URL kURL(ParsedURLString, url);
    NSURL *cocoaURL = kURL;
    NSMutableDictionary *movieAttributes = commonMovieAttributes();    
    [movieAttributes setValue:cocoaURL forKey:QTMovieURLAttribute];

    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 && !kURL.protocolIsData()) {
        // Only pass the QTMovieOpenForPlaybackAttribute flag if there are no proxy servers, due
        // to rdar://problem/7531776, or if not loading a data:// url due to rdar://problem/8103801.
        [movieAttributes setObject:[NSNumber numberWithBool:YES] forKey:@"QTMovieOpenForPlaybackAttribute"];
    }
    
    if (proxiesForURL)
        CFRelease(proxiesForURL);
    if (proxySettings)
        CFRelease(proxySettings);

    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:
    ComponentDescription componentsToDisable[11] = {
        {'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 (auto& component : componentsToDisable)
        [getQTMovieClass() disableComponent:component];
}

void MediaPlayerPrivateQTKit::createQTMovie(NSURL *url, NSDictionary *movieAttributes)
{
    LOG(Media, "MediaPlayerPrivateQTKit::createQTMovie(%p) ", this);
    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([allocQTMovieInstance() 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().
    [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get()
                                             selector:@selector(loadedRangesChanged:)
                                                 name:QTMovieLoadedRangesDidChangeNotification
                                               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 Class QTVideoRendererClass()
{
     static Class QTVideoRendererWebKitOnlyClass = NSClassFromString(@"QTVideoRendererWebKitOnly");
     return QTVideoRendererWebKitOnlyClass;
}

void MediaPlayerPrivateQTKit::createQTVideoRenderer(QTVideoRendererMode rendererMode)
{
    LOG(Media, "MediaPlayerPrivateQTKit::createQTVideoRenderer(%p)", this);
    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()
{
    LOG(Media, "MediaPlayerPrivateQTKit::destroyQTVideoRenderer(%p)", this);
    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()
{
    LOG(Media, "MediaPlayerPrivateQTKit::createQTMovieLayer(%p)", this);
    if (!m_qtMovie)
        return;

    ASSERT(supportsAcceleratedRendering());
    
    if (!m_qtVideoLayer) {
        m_qtVideoLayer = adoptNS([allocQTMovieLayerInstance() 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::updateConfiguration().
    }
}

void MediaPlayerPrivateQTKit::destroyQTMovieLayer()
{
    LOG(Media, "MediaPlayerPrivateQTKit::destroyQTMovieLayer(%p)", this);
    if (!m_qtVideoLayer)
        return;

    // disassociate our movie from our instance of QTMovieLayer
    [m_qtVideoLayer.get() setMovie:nil];    
    m_qtVideoLayer = nil;
}

MediaPlayerPrivateQTKit::MediaRenderingMode MediaPlayerPrivateQTKit::currentRenderingMode() const
{
    if (m_qtVideoLayer)
        return MediaRenderingMovieLayer;

    if (m_qtVideoRenderer)
        return MediaRenderingSoftwareRenderer;
    
    return MediaRenderingNone;
}

MediaPlayerPrivateQTKit::MediaRenderingMode MediaPlayerPrivateQTKit::preferredRenderingMode() const
{
    if (!m_qtMovie)
        return MediaRenderingNone;

    if (supportsAcceleratedRendering() && m_player->client().mediaPlayerRenderingCanBeAccelerated(m_player))
        return MediaRenderingMovieLayer;

    if (!QTVideoRendererClass())
        return MediaRenderingNone;
    
    return MediaRenderingSoftwareRenderer;
}

void MediaPlayerPrivateQTKit::setUpVideoRendering()
{
    LOG(Media, "MediaPlayerPrivateQTKit::setUpVideoRendering(%p)", this);
    if (!isReadyForVideoSetup())
        return;

    MediaRenderingMode currentMode = currentRenderingMode();
    MediaRenderingMode preferredMode = preferredRenderingMode();
    if (currentMode == preferredMode && currentMode != MediaRenderingNone)
        return;

    if (currentMode != MediaRenderingNone)  
        tearDownVideoRendering();

    switch (preferredMode) {
    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->client().mediaPlayerRenderingModeChanged(m_player);
}

void MediaPlayerPrivateQTKit::tearDownVideoRendering()
{
    LOG(Media, "MediaPlayerPrivateQTKit::tearDownVideoRendering(%p)", this);
    if (m_qtVideoRenderer)
        destroyQTVideoRenderer();
    if (m_qtVideoLayer)
        destroyQTMovieLayer();
}

bool MediaPlayerPrivateQTKit::hasSetUpVideoRendering() const
{
    return m_qtVideoLayer
        || m_qtVideoRenderer;
}

void MediaPlayerPrivateQTKit::resumeLoad()
{
    if (!m_movieURL.isNull())
        loadInternal(m_movieURL);
}

void MediaPlayerPrivateQTKit::load(const String& url)
{
    LOG(Media, "MediaPlayerPrivateQTKit::load(%p)", this);
    m_movieURL = url;

    // If the element is not supposed to load any data return immediately.
    if (m_preload == MediaPlayer::None)
        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];
}

#if ENABLE(MEDIA_SOURCE)
void MediaPlayerPrivateQTKit::load(const String&, MediaSourcePrivateClient*)
{
    m_networkState = MediaPlayer::FormatError;
    m_player->networkStateChanged();
}
#endif


void MediaPlayerPrivateQTKit::prepareToPlay()
{
    LOG(Media, "MediaPlayerPrivateQTKit::prepareToPlay(%p)", this);
    setPreload(MediaPlayer::Auto);
}

PlatformMedia MediaPlayerPrivateQTKit::platformMedia() const
{
    PlatformMedia pm;
    pm.type = PlatformMedia::QTMovieType;
    pm.media.qtMovie = m_qtMovie.get();
    return pm;
}

PlatformLayer* MediaPlayerPrivateQTKit::platformLayer() const
{
    return m_qtVideoLayer.get();
}

void MediaPlayerPrivateQTKit::play()
{
    LOG(Media, "MediaPlayerPrivateQTKit::play(%p)", this);
    if (!metaDataAvailable())
        return;
    m_startedPlaying = true;
    [m_objcObserver.get() setDelayCallbacks:YES];
    [m_qtMovie.get() setRate:m_player->rate()];
    [m_objcObserver.get() setDelayCallbacks:NO];
}

void MediaPlayerPrivateQTKit::pause()
{
    LOG(Media, "MediaPlayerPrivateQTKit::pause(%p)", this);
    if (!metaDataAvailable())
        return;
    m_startedPlaying = false;
    [m_objcObserver.get() setDelayCallbacks:YES];
    [m_qtMovie.get() stop];
    [m_objcObserver.get() setDelayCallbacks:NO];
}

MediaTime MediaPlayerPrivateQTKit::durationMediaTime() const
{
    if (!metaDataAvailable())
        return MediaTime::zeroTime();

    if (m_cachedDuration.isValid())
        return m_cachedDuration;

    QTTime time = [m_qtMovie.get() duration];
    if (time.flags == kQTTimeIsIndefinite)
        return MediaTime::positiveInfiniteTime();
    return toMediaTime(time);
}

MediaTime MediaPlayerPrivateQTKit::currentMediaTime() const
{
    if (!metaDataAvailable())
        return MediaTime::zeroTime();
    QTTime time = [m_qtMovie.get() currentTime];
    return toMediaTime(time);
}

void MediaPlayerPrivateQTKit::seek(const MediaTime& inTime)
{
    MediaTime time = inTime;
    LOG(Media, "MediaPlayerPrivateQTKit::seek(%p) - time %s", this, toString(time).utf8().data());
    // 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 > durationMediaTime())
        time = durationMediaTime();

    m_seekTo = time;
    if (maxMediaTimeSeekable() >= m_seekTo)
        doSeek();
    else 
        m_seekTimer.start(0_s, 500_ms);
}

void MediaPlayerPrivateQTKit::doSeek() 
{
    QTTime qttime = toQTTime(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
    MediaTime timeAfterSeek = currentMediaTime();
    if (oldRate && timeAfterSeek < durationMediaTime())
        [m_qtMovie.get() setRate:oldRate];

    cancelSeek();
    [m_objcObserver.get() setDelayCallbacks:NO];
}

void MediaPlayerPrivateQTKit::cancelSeek()
{
    LOG(Media, "MediaPlayerPrivateQTKit::cancelSeek(%p)", this);
    m_seekTo = MediaTime::invalidTime();
    m_seekTimer.stop();
}

void MediaPlayerPrivateQTKit::seekTimerFired()
{        
    if (!metaDataAvailable() || !seeking() || currentMediaTime() == m_seekTo) {
        cancelSeek();
        updateStates();
        m_player->timeChanged(); 
        return;
    } 

    if (maxMediaTimeSeekable() >= 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.isValid() && m_seekTo >= MediaTime::zeroTime();
}

FloatSize MediaPlayerPrivateQTKit::naturalSize() const
{
    if (!metaDataAvailable())
        return FloatSize();

    // 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
    
    FloatSize naturalSize([[m_qtMovie.get() attributeForKey:QTMovieNaturalSizeAttribute] sizeValue]);
    if (naturalSize.isEmpty() && m_isStreaming) {
        // HTTP Live Streams will occasionally return {0,0} natural sizes while scrubbing.
        // Work around this problem (<rdar://problem/9078563>) by returning the last valid 
        // cached natural size:
        naturalSize = m_cachedNaturalSize;
    } else {
        // Unfortunately, due to another QTKit bug (<rdar://problem/9082071>) we won't get a sizeChanged
        // event when this happens, so we must cache the last valid naturalSize here:
        m_cachedNaturalSize = naturalSize;
    }
    
    naturalSize.scale(m_scaleFactor.width(), m_scaleFactor.height());
    return naturalSize;
}

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
{
    return true;
}

void MediaPlayerPrivateQTKit::setVolume(float volume)
{
    LOG(Media, "MediaPlayerPrivateQTKit::setVolume(%p) - volume %f", this, volume);
    if (m_qtMovie)
        [m_qtMovie.get() setVolume:volume];  
}

bool MediaPlayerPrivateQTKit::hasClosedCaptions() const
{
    if (!metaDataAvailable())
        return false;
    return [[m_qtMovie alternateGroupTypes] containsObject:@"clcp"];
}

void MediaPlayerPrivateQTKit::setClosedCaptionsVisible(bool closedCaptionsVisible)
{
    if (!metaDataAvailable())
        return;

    if (![[m_qtMovie alternateGroupTypes] containsObject:@"clcp"])
        return;

    NSArray *trackAlternatesArray = [m_qtMovie alternatesForMediaType:@"clcp"];
    QTTrack *track = trackAlternatesArray[0][@"QTAlternates_QTTrack"];
    if (!track)
        return;

    if (!closedCaptionsVisible)
        [m_qtMovie deselectAlternateGroupTrack:track];
    else
        [m_qtMovie selectAlternateGroupTrack:track];

    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];
    }
}

void MediaPlayerPrivateQTKit::setRate(float rate)
{
    LOG(Media, "MediaPlayerPrivateQTKit::setRate(%p) - rate %f", this, rate);
    if (m_qtMovie)
        [m_qtMovie.get() setRate:rate];
}

double MediaPlayerPrivateQTKit::rate() const
{
    return m_qtMovie ? [m_qtMovie rate] : 0;
}

void MediaPlayerPrivateQTKit::setPreservesPitch(bool preservesPitch)
{
    LOG(Media, "MediaPlayerPrivateQTKit::setPreservesPitch(%p) - preservesPitch %d", this, (int)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 = currentMediaTime();

    createQTMovie([movieAttributes.get() valueForKey:QTMovieURLAttribute], movieAttributes.get());
}

std::unique_ptr<PlatformTimeRanges> MediaPlayerPrivateQTKit::buffered() const
{
    auto timeRanges = std::make_unique<PlatformTimeRanges>();
    MediaTime loaded = maxMediaTimeLoaded();
    if (loaded > MediaTime::zeroTime())
        timeRanges->add(MediaTime::zeroTime(), loaded);
    return timeRanges;
}

static MediaTime maxValueForTimeRanges(NSArray *ranges)
{
    if (!ranges)
        return MediaTime::zeroTime();

    MediaTime max;
    for (NSValue *value in ranges) {
        QTTimeRange range = [value QTTimeRangeValue];
        if (!range.time.timeScale || !range.duration.timeScale)
            continue;

        MediaTime time = toMediaTime(range.time);
        MediaTime duration = toMediaTime(range.duration);
        if (time.isValid() && duration.isValid())
            max = std::max(max, time + duration);
    }

    return max;
}

MediaTime MediaPlayerPrivateQTKit::maxMediaTimeSeekable() const
{
    if (!metaDataAvailable())
        return MediaTime::zeroTime();

    // infinite duration means live stream
    if (durationMediaTime().isPositiveInfinite())
        return MediaTime::zeroTime();

    NSArray* seekableRanges = [m_qtMovie availableRanges];

    return maxValueForTimeRanges(seekableRanges);
}

MediaTime MediaPlayerPrivateQTKit::maxMediaTimeLoaded() const
{
    if (!metaDataAvailable())
        return MediaTime::zeroTime();
    if ([m_qtMovie respondsToSelector:@selector(loadedRanges)])
        return maxValueForTimeRanges([m_qtMovie loadedRanges]);
    return toMediaTime([m_qtMovie maxTimeLoaded]);
}

bool MediaPlayerPrivateQTKit::didLoadingProgress() const
{
    if (!duration() || !totalBytes())
        return false;
    MediaTime currentMaxTimeLoaded = maxMediaTimeLoaded();
    bool didLoadingProgress = currentMaxTimeLoaded != m_maxTimeLoadedAtLastDidLoadingProgress;
    m_maxTimeLoadedAtLastDidLoadingProgress = currentMaxTimeLoaded;
    return didLoadingProgress;
}

unsigned long long MediaPlayerPrivateQTKit::totalBytes() const
{
    if (!metaDataAvailable())
        return 0;
    return [[m_qtMovie.get() attributeForKey:QTMovieDataSizeAttribute] longLongValue];
}

void MediaPlayerPrivateQTKit::cancelLoad()
{
    LOG(Media, "MediaPlayerPrivateQTKit::cancelLoad(%p)", this);
    // 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];

    // 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;
    }

    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()
{
    LOG(Media, "MediaPlayerPrivateQTKit::prepareForRendering(%p)", this);
    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->client().mediaPlayerRenderingModeChanged(m_player);
}

static void selectPreferredAlternateTrackForMediaType(QTMovie *movie, NSString *mediaType)
{
    NSArray *alternates = [movie alternatesForMediaType:mediaType];
    if (!alternates.count)
        return;

    auto languageToQTTrackMap = adoptNS([[NSMutableDictionary alloc] initWithCapacity:alternates.count]);

    for (NSUInteger index = 0; index < alternates.count; ++index) {
        NSDictionary *alternateDict = alternates[index];
        NSString *languageString = alternateDict[@"QTAlternates_LanguageCodeEncoding_ISO_639_2T"];
        if (![languageString cStringUsingEncoding:kCFStringEncodingASCII])
            continue;
        if (!languageString)
            languageString = alternateDict[@"QTAlternates_LanguageCodeEncoding_RFC_4646"];
        if (!languageString) {
            LangCode langCode = [alternateDict[@"QTAlternates_LanguageCodeEncoding_MacType_LangCode"] intValue];
            auto identifier = adoptCF(CFLocaleCreateCanonicalLocaleIdentifierFromScriptManagerCodes(kCFAllocatorDefault, langCode, 0));
            languageString = (NSString *)identifier.autorelease();
        }
        if (!languageString)
            continue;

        id alternateTrack = alternateDict[@"QTAlternates_QTTrack"];
        if (!alternateTrack)
            continue;

        if (![[alternateTrack attributeForKey:@"QTTrackEnabledAttribute"] boolValue])
            continue;

        [languageToQTTrackMap setObject:alternateTrack forKey:languageString];
    }

    NSArray *preferredLanguages = [NSBundle preferredLocalizationsFromArray:[languageToQTTrackMap allKeys] forPreferences:nil];
    if (preferredLanguages.count) {
        id preferredTrack = [languageToQTTrackMap objectForKey:preferredLanguages[0]];
        if (preferredTrack) {
            // +[NSBundle preferredLocalizationsFromArray:forPreferences] may return a language which was
            // not present in preferredLanguages, and will therefore not have an associated track.
            [movie selectAlternateGroupTrack:preferredTrack];
        }
    }
}

static void selectPreferredAlternates(QTMovie *movie)
{
    selectPreferredAlternateTrackForMediaType(movie, @"vide");
    selectPreferredAlternateTrackForMediaType(movie, @"soun");
    selectPreferredAlternateTrackForMediaType(movie, @"cplp");
    selectPreferredAlternateTrackForMediaType(movie, @"sbtl");
}

void MediaPlayerPrivateQTKit::updateStates()
{
    MediaPlayer::NetworkState oldNetworkState = m_networkState;
    MediaPlayer::ReadyState oldReadyState = m_readyState;

    LOG(Media, "MediaPlayerPrivateQTKit::updateStates(%p) - entering with networkState = %i, readyState = %i", this, static_cast<int>(m_networkState), static_cast<int>(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) {
            selectPreferredAlternates(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.isValid()) {
        QTTime qttime = toQTTime(m_timeToRestore);
        m_timeToRestore = MediaTime::invalidTime();
            
        // 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 = maxMediaTimeLoaded() == durationMediaTime();

    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 = currentMediaTime() < maxMediaTimeLoaded() ? 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;
        }

        MediaTime loaded = maxMediaTimeLoaded();
        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 > MediaTime::zeroTime())
                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) {
        MediaTime dur = durationMediaTime();
        if (dur != m_reportedDuration) {
            if (m_reportedDuration.isValid())
                m_player->durationChanged();
            m_reportedDuration = dur;
        }
    }

    LOG(Media, "MediaPlayerPrivateQTKit::updateStates(%p) - exiting with networkState = %i, readyState = %i", this, static_cast<int>(m_networkState), static_cast<int>(m_readyState));
}

long MediaPlayerPrivateQTKit::platformErrorCode() const
{
    if (!m_qtMovie)
        return 0;

    NSError* error = (NSError*)[m_qtMovie attributeForKey:QTMovieLoadStateErrorAttribute];
    if (!error || ![error isKindOfClass:[NSError class]])
        return 0;

    return [error code];
}

void MediaPlayerPrivateQTKit::loadStateChanged()
{
    LOG(Media, "MediaPlayerPrivateQTKit::loadStateChanged(%p) - loadState = %li", this, [[m_qtMovie.get() attributeForKey:QTMovieLoadStateAttribute] longValue]);

    if (!m_hasUnsupportedTracks)
        updateStates();
}

void MediaPlayerPrivateQTKit::loadedRangesChanged()
{
    LOG(Media, "MediaPlayerPrivateQTKit::loadedRangesChanged(%p) - loadState = %li", this, [[m_qtMovie.get() attributeForKey:QTMovieLoadStateAttribute] longValue]);

    if (!m_hasUnsupportedTracks)
        updateStates();
}

void MediaPlayerPrivateQTKit::rateChanged()
{
    LOG(Media, "MediaPlayerPrivateQTKit::rateChanged(%p) - rate = %li", this, [m_qtMovie.get() rate]);
    if (m_hasUnsupportedTracks)
        return;

    updateStates();
    m_player->rateChanged();
}

void MediaPlayerPrivateQTKit::sizeChanged()
{
    LOG(Media, "MediaPlayerPrivateQTKit::sizeChanged(%p)", this);
    if (!m_hasUnsupportedTracks)
        m_player->sizeChanged();
}

void MediaPlayerPrivateQTKit::timeChanged()
{
    LOG(Media, "MediaPlayerPrivateQTKit::timeChanged(%p)", this);
    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.isValid())
        m_seekTo = currentMediaTime();

    m_timeToRestore = MediaTime::invalidTime();
    updateStates();
    m_player->timeChanged();
}

void MediaPlayerPrivateQTKit::didEnd()
{
    LOG(Media, "MediaPlayerPrivateQTKit::didEnd(%p)", this);
    if (m_hasUnsupportedTracks)
        return;

    m_startedPlaying = false;

    // 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!
    MediaTime now = currentMediaTime();
    if (now > MediaTime::zeroTime())
        m_cachedDuration = now;

    updateStates();
    m_player->timeChanged();
}

void MediaPlayerPrivateQTKit::layerHostChanged(PlatformLayer* rootLayer)
{
    UNUSED_PARAM(rootLayer);
}

void MediaPlayerPrivateQTKit::setSize(const IntSize&) 
{ 
}

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;

    m_videoFrameHasDrawn = true;
    m_player->repaint();
}

void MediaPlayerPrivateQTKit::paintCurrentFrameInContext(GraphicsContext& context, const FloatRect& 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);
    }
    paint(context, r);
}

void MediaPlayerPrivateQTKit::paint(GraphicsContext& context, const FloatRect& r)
{
    if (context.paintingDisabled() || m_hasUnsupportedTracks)
        return;
    id qtVideoRenderer = m_qtVideoRenderer.get();
    if (!qtVideoRenderer)
        return;

    [m_objcObserver.get() setDelayCallbacks:YES];
    BEGIN_BLOCK_OBJC_EXCEPTIONS;
    NSGraphicsContext* newContext;
    FloatSize scaleFactor(1.0f, -1.0f);
    FloatRect paintRect(FloatPoint(), r.size());

    GraphicsContextStateSaver stateSaver(context);
    context.translate(r.x(), r.y() + r.height());
    context.scale(scaleFactor);
    context.setImageInterpolationQuality(InterpolationLow);

    newContext = [NSGraphicsContext graphicsContextWithGraphicsPort:context.platformContext() flipped:NO];

    [NSGraphicsContext saveGraphicsState];
    [NSGraphicsContext setCurrentContext:newContext];
    [(id<WebKitVideoRenderingDetails>)qtVideoRenderer drawInRect:paintRect];
    [NSGraphicsContext restoreGraphicsState];

    END_BLOCK_OBJC_EXCEPTIONS;
    [m_objcObserver.get() setDelayCallbacks:NO];
}

static bool shouldRejectMIMEType(const String& type)
{
    // QTKit will return non-video MIME types which it claims to support, but which we
    // do not support in the <video> element. Disclaim all non video/ or audio/ types.
    return !startsWithLettersIgnoringASCIICase(type, "video/") && !startsWithLettersIgnoringASCIICase(type, "audio/");
}

static HashSet<String, ASCIICaseInsensitiveHash> createFileTypesSet(NSArray *fileTypes)
{
    HashSet<String, ASCIICaseInsensitiveHash> set;
    for (NSString *fileType in fileTypes) {
        CFStringRef ext = reinterpret_cast<CFStringRef>(fileType);
        auto uti = adoptCF(UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext, NULL));
        if (!uti)
            continue;
        auto mime = MIMETypeFromUTI(uti.get());
        if (shouldRejectMIMEType(mime))
            continue;
        if (!mime.isEmpty())
            set.add(mime);

        // -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 registry.
            for (auto& type : MIMETypeRegistry::getMediaMIMETypesForExtension(ext)) {
                if (!shouldRejectMIMEType(type))
                    set.add(type);
            }
        }
    }
    return set;
}

static const HashSet<String, ASCIICaseInsensitiveHash>& mimeCommonTypesCache()
{
    static const auto cache = makeNeverDestroyed(createFileTypesSet([getQTMovieClass() movieFileTypes:QTIncludeCommonTypes]));
    return cache;
} 

static const HashSet<String, ASCIICaseInsensitiveHash>& mimeModernTypesCache()
{
    static const auto cache = makeNeverDestroyed(createFileTypesSet([getQTMovieClass() movieFileTypes:(QTMovieFileTypeOptions)QTIncludeOnlyFigMediaFileTypes]));
    return cache;
}

static void concatenateHashSets(HashSet<String, ASCIICaseInsensitiveHash>& destination, const HashSet<String, ASCIICaseInsensitiveHash>& source)
{
    for (auto& type : source)
        destination.add(type);
}

void MediaPlayerPrivateQTKit::getSupportedTypes(HashSet<String, ASCIICaseInsensitiveHash>& supportedTypes)
{
    concatenateHashSets(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.
    concatenateHashSets(supportedTypes, mimeCommonTypesCache());
}

MediaPlayer::SupportsType MediaPlayerPrivateQTKit::supportsType(const MediaEngineSupportParameters& parameters)
{
#if ENABLE(MEDIA_SOURCE)
    if (parameters.isMediaSource)
        return MediaPlayer::IsNotSupported;
#endif

    // 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.

    // Due to <rdar://problem/10777059>, avoid calling the mime types cache functions if at
    // all possible:
    auto containerType = parameters.type.containerType();
    if (shouldRejectMIMEType(containerType))
        return MediaPlayer::IsNotSupported;

    // We check the "modern" type cache first, as it doesn't require QTKitServer to start.
    if (mimeModernTypesCache().contains(containerType) || mimeCommonTypesCache().contains(containerType))
        return parameters.type.codecs().isEmpty() ? MediaPlayer::MayBeSupported : MediaPlayer::IsSupported;

    return MediaPlayer::IsNotSupported;
}

bool MediaPlayerPrivateQTKit::isAvailable()
{
    // 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();
}

HashSet<RefPtr<SecurityOrigin>> MediaPlayerPrivateQTKit::originsInMediaCache(const String&)
{
    HashSet<RefPtr<SecurityOrigin>> origins;
    NSArray *mediaSites = [[QTUtilities qtUtilities] sitesInDownloadCache];
    
    for (NSString *site in mediaSites) {
        URL siteAsURL = URL(URL(), site);
        if (siteAsURL.isValid())
            origins.add(SecurityOrigin::create(siteAsURL));
    }
    return origins;
}

void MediaPlayerPrivateQTKit::clearMediaCache(const String&, WallTime)
{
    LOG(Media, "MediaPlayerPrivateQTKit::clearMediaCache()");
    [[QTUtilities qtUtilities] clearDownloadCache];
}

void MediaPlayerPrivateQTKit::clearMediaCacheForOrigins(const String&, const HashSet<RefPtr<SecurityOrigin>>& origins)
{
    LOG(Media, "MediaPlayerPrivateQTKit::clearMediaCacheForOrigins()");
    for (auto& origin : origins)
        [[QTUtilities qtUtilities] clearDownloadCacheForSite:origin->toRawString()];
}

void MediaPlayerPrivateQTKit::disableUnsupportedTracks()
{
    LOG(Media, "MediaPlayerPrivateQTKit::disableUnsupportedTracks(%p)", this);

    if (!m_qtMovie) {
        m_enabledTrackCount = 0;
        m_totalTrackCount = 0;
        return;
    }
    
    static NeverDestroyed<HashSet<String>> allowedTrackTypes = [] {
        static NSString * const types[] = {
            QTMediaTypeVideo,
            QTMediaTypeSound,
            QTMediaTypeText,
            QTMediaTypeBase,
            QTMediaTypeMPEG,
            @"clcp", // Closed caption
            @"sbtl", // Subtitle
            @"odsm", // MPEG-4 object descriptor stream
            @"sdsm", // MPEG-4 scene description stream
            @"tmcd", // timecode
            @"tc64", // timcode-64
            @"tmet", // timed metadata
        };
        HashSet<String> set;
        for (auto& type : types)
            set.add(type);
        return set;
    }();
    
    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.get().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.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
        QTTrack *chapterTrack = [track performSelector:@selector(chapterlist)];
#pragma clang diagnostic pop
        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->client().mediaPlayerSawUnsupportedTracks(m_player);
}

bool MediaPlayerPrivateQTKit::supportsAcceleratedRendering() const
{
    return isReadyForVideoSetup() && getQTMovieLayerClass() != Nil;
}

void MediaPlayerPrivateQTKit::acceleratedRenderingStateChanged()
{
    // Set up or change the rendering path if necessary.
    setUpVideoRendering();
}

bool MediaPlayerPrivateQTKit::hasSingleSecurityOrigin() const
{
    if (!m_qtMovie)
        return false;

    Ref<SecurityOrigin> resolvedOrigin = SecurityOrigin::create(URL([m_qtMovie URL]));
    Ref<SecurityOrigin> requestedOrigin = SecurityOrigin::createFromString(m_movieURL);
    return resolvedOrigin->isSameSchemeHostPort(requestedOrigin.get());
}

MediaPlayer::MovieLoadType MediaPlayerPrivateQTKit::movieLoadType() const
{
    if (!m_qtMovie)
        return MediaPlayer::MovieLoadType::Unknown;

    UInt32 movieType = [m_qtMovie movieType];
    switch (movieType) {
    case QTMovieTypeLocal:
    case QTMovieTypeFastStart:
        return MediaPlayer::MovieLoadType::Download;
    case QTMovieTypeLiveStream:
        return MediaPlayer::MovieLoadType::LiveStream;
    case QTMovieTypeStoredStream:
        return MediaPlayer::MovieLoadType::StoredStream;
    case QTMovieTypeUnknown:
    default:
        return MediaPlayer::MovieLoadType::Unknown;
    }
}

void MediaPlayerPrivateQTKit::setPreload(MediaPlayer::Preload preload)
{
    m_preload = preload;
    if (m_preload == MediaPlayer::None)
        return;

    if (!m_qtMovie)
        resumeLoad();
    else if (m_preload == MediaPlayer::Auto)
        [m_qtMovie.get() setAttribute:[NSNumber numberWithBool:NO] forKey:@"QTMovieLimitReadAheadAttribute"];
}

void MediaPlayerPrivateQTKit::setPrivateBrowsingMode(bool privateBrowsing)
{
    m_privateBrowsing = privateBrowsing;
    if (!m_qtMovie)
        return;
    [m_qtMovie.get() setAttribute:[NSNumber numberWithBool:!privateBrowsing] forKey:@"QTMovieAllowPersistentCacheAttribute"];
}

bool MediaPlayerPrivateQTKit::canSaveMediaData() const
{
    URL url;

    if (durationMediaTime().isPositiveInfinite())
        return false;

    if (m_qtMovie)
        url = URL([m_qtMovie URL]);
    else
        url = URL(ParsedURLString, m_movieURL);

    if (url.isLocalFile())
        return true;

    if (url.protocolIsInHTTPFamily())
        return true;
    
    return false;
}

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
void MediaPlayerPrivateQTKit::setWirelessPlaybackTarget(Ref<MediaPlaybackTarget>&& target)
{
    m_playbackTarget = WTFMove(target);
}

void MediaPlayerPrivateQTKit::setShouldPlayToPlaybackTarget(bool shouldPlayToTarget)
{
    if (shouldPlayToTarget == m_shouldPlayToTarget)
        return;

    m_shouldPlayToTarget = shouldPlayToTarget;

    if (m_player)
        m_player->currentPlaybackTargetIsWirelessChanged();
}

bool MediaPlayerPrivateQTKit::isCurrentPlaybackTargetWireless() const
{
    if (!m_playbackTarget)
        return false;
    
    return m_shouldPlayToTarget && m_playbackTarget->hasActiveRoute();
}
#endif

} // namespace WebCore

@implementation WebCoreMovieObserver

- (id)initWithCallback:(MediaPlayerPrivateQTKit*)callback
{
    m_callback = callback;
    return [super init];
}

- (void)disconnect
{
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    m_callback = 0;
}

-(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)loadedRangesChanged:(NSNotification *)unusedNotification
{
    UNUSED_PARAM(unusedNotification);
    if (m_delayCallbacks)
        [self performSelector:_cmd withObject:nil afterDelay:0];
    else
        m_callback->loadedRangesChanged();
}

- (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)layerHostChanged:(NSNotification *)notification
{
    CALayer* rootLayer = static_cast<CALayer*>([notification object]);
    m_callback->layerHostChanged(rootLayer);
}

- (void)setDelayCallbacks:(BOOL)shouldDelay
{
    m_delayCallbacks = shouldDelay;
}

@end

#pragma clang diagnostic pop // deprecated-declarations

#endif