QTMovie.cpp   [plain text]


/*
 * Copyright (C) 2007, 2008, 2009, 2010, 2011 Apple, Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
#include "config.h"

#include "QTMovie.h"

#include "QTMovieTask.h"
#include "QTMovieWinTimer.h"
#include <FixMath.h>
#include <GXMath.h>
#include <Movies.h>
#include <QTML.h>
#include <QuickTimeComponents.h>
#include <WebKitSystemInterface/WebKitSystemInterface.h>
#include <wtf/Assertions.h>
#include <wtf/MathExtras.h>
#include <wtf/Noncopyable.h>
#include <wtf/Vector.h>

using namespace std;

static const long minimumQuickTimeVersion = 0x07300000; // 7.3

static const long closedCaptionTrackType = 'clcp';
static const long subTitleTrackType = 'sbtl';
static const long mpeg4ObjectDescriptionTrackType = 'odsm';
static const long mpeg4SceneDescriptionTrackType = 'sdsm';
static const long closedCaptionDisplayPropertyID = 'disp';

// Resizing GWorlds is slow, give them a minimum size so size of small 
// videos can be animated smoothly
static const int cGWorldMinWidth = 640;
static const int cGWorldMinHeight = 360;

static const float cNonContinuousTimeChange = 0.2f;

union UppParam {
    long longValue;
    void* ptr;
};

static CFMutableArrayRef gSupportedTypes = 0;
static SInt32 quickTimeVersion = 0;

class QTMoviePrivate : public QTMovieTaskClient {
    WTF_MAKE_NONCOPYABLE(QTMoviePrivate);
public:
    QTMoviePrivate();
    ~QTMoviePrivate();
    void task();
    void startTask();
    void endTask();

    void createMovieController();
    void cacheMovieScale();

    QTMovie* m_movieWin;
    Movie m_movie;
    MovieController m_movieController;
    bool m_tasking;
    bool m_disabled;
    Vector<QTMovieClient*> m_clients;
    long m_loadState;
    bool m_ended;
    bool m_seeking;
    float m_lastMediaTime;
    double m_lastLoadStateCheckTime;
    int m_width;
    int m_height;
    bool m_visible;
    long m_loadError;
    float m_widthScaleFactor;
    float m_heightScaleFactor;
    CFURLRef m_currentURL;
    float m_timeToRestore;
    float m_rateToRestore;
    bool m_privateBrowsing;
#if !ASSERT_DISABLED
    bool m_scaleCached;
#endif
};

QTMoviePrivate::QTMoviePrivate()
    : m_movieWin(0)
    , m_movie(0)
    , m_movieController(0)
    , m_tasking(false)
    , m_loadState(0)
    , m_ended(false)
    , m_seeking(false)
    , m_lastMediaTime(0)
    , m_lastLoadStateCheckTime(0)
    , m_width(0)
    , m_height(0)
    , m_visible(false)
    , m_loadError(0)
    , m_widthScaleFactor(1)
    , m_heightScaleFactor(1)
    , m_currentURL(0)
    , m_timeToRestore(-1.0f)
    , m_rateToRestore(-1.0f)
    , m_disabled(false)
    , m_privateBrowsing(false)
#if !ASSERT_DISABLED
    , m_scaleCached(false)
#endif
{
}

QTMoviePrivate::~QTMoviePrivate()
{
    endTask();
    if (m_movieController)
        DisposeMovieController(m_movieController);
    if (m_movie)
        DisposeMovie(m_movie);
    if (m_currentURL)
        CFRelease(m_currentURL);
}

void QTMoviePrivate::startTask() 
{
    if (!m_tasking) {
        QTMovieTask::sharedTask()->addTaskClient(this);
        m_tasking = true;
    }
    QTMovieTask::sharedTask()->updateTaskTimer();
}

void QTMoviePrivate::endTask() 
{
    if (m_tasking) {
        QTMovieTask::sharedTask()->removeTaskClient(this);
        m_tasking = false;
    }
    QTMovieTask::sharedTask()->updateTaskTimer();
}

void QTMoviePrivate::task() 
{
    ASSERT(m_tasking);

    if (!m_loadError) {
        if (m_movieController)
            MCIdle(m_movieController);
        else
            MoviesTask(m_movie, 0);
    }

    // GetMovieLoadState documentation says that you should not call it more often than every quarter of a second.
    if (systemTime() >= m_lastLoadStateCheckTime + 0.25 || m_loadError) { 
        // If load fails QT's load state is QTMovieLoadStateComplete.
        // This is different from QTKit API and seems strange.
        long loadState = m_loadError ? QTMovieLoadStateError : GetMovieLoadState(m_movie);
        if (loadState != m_loadState) {
            // we only need to erase the movie gworld when the load state changes to loaded while it
            //  is visible as the gworld is destroyed/created when visibility changes
            bool shouldRestorePlaybackState = false;
            bool movieNewlyPlayable = loadState >= QTMovieLoadStateLoaded && m_loadState < QTMovieLoadStateLoaded;
            m_loadState = loadState;
            if (movieNewlyPlayable) {
                cacheMovieScale();
                shouldRestorePlaybackState = true;
            }

            if (!m_movieController && m_loadState >= QTMovieLoadStateLoaded)
                createMovieController();

            for (size_t i = 0; i < m_clients.size(); ++i)
                m_clients[i]->movieLoadStateChanged(m_movieWin);
            
            if (shouldRestorePlaybackState && m_timeToRestore != -1.0f) {
                m_movieWin->setCurrentTime(m_timeToRestore);
                m_timeToRestore = -1.0f;
                m_movieWin->setRate(m_rateToRestore);
                m_rateToRestore = -1.0f;
            }

            if (m_disabled) {
                endTask();
                return;
            }
        }
        m_lastLoadStateCheckTime = systemTime();
    }

    bool ended = !!IsMovieDone(m_movie);
    if (ended != m_ended) {
        m_ended = ended;
        if (ended) {
            for (size_t i = 0; i < m_clients.size(); ++i)
               m_clients[i]->movieEnded(m_movieWin);
        }
    }

    float time = m_movieWin->currentTime();
    if (time < m_lastMediaTime || time >= m_lastMediaTime + cNonContinuousTimeChange || m_seeking) {
        m_seeking = false;
        for (size_t i = 0; i < m_clients.size(); ++i)
            m_clients[i]->movieTimeChanged(m_movieWin);
    }
    m_lastMediaTime = time;

    if (m_loadError)
        endTask();
    else
        QTMovieTask::sharedTask()->updateTaskTimer();
}

void QTMoviePrivate::createMovieController()
{
    Rect bounds;
    long flags;

    if (!m_movie)
        return;

    if (m_movieController)
        DisposeMovieController(m_movieController);

    GetMovieBox(m_movie, &bounds);
    flags = mcTopLeftMovie | mcNotVisible;
    m_movieController = NewMovieController(m_movie, &bounds, flags);
    if (!m_movieController)
        return;

    // Disable automatic looping.
    MCDoAction(m_movieController, mcActionSetLooping, 0);
}

void QTMoviePrivate::cacheMovieScale()
{
    Rect naturalRect;
    Rect initialRect;

    GetMovieNaturalBoundsRect(m_movie, &naturalRect);
    GetMovieBox(m_movie, &initialRect);

    float naturalWidth = naturalRect.right - naturalRect.left;
    float naturalHeight = naturalRect.bottom - naturalRect.top;

    if (naturalWidth)
        m_widthScaleFactor = (initialRect.right - initialRect.left) / naturalWidth;
    if (naturalHeight)
        m_heightScaleFactor = (initialRect.bottom - initialRect.top) / naturalHeight;
#if !ASSERT_DISABLED
    m_scaleCached = true;
#endif
}

QTMovie::QTMovie(QTMovieClient* client)
    : m_private(new QTMoviePrivate())
{
    m_private->m_movieWin = this;
    if (client)
        m_private->m_clients.append(client);
    initializeQuickTime();
}

QTMovie::~QTMovie()
{
    delete m_private;
}

void QTMovie::disableComponent(uint32_t cd[5])
{
    ComponentDescription nullDesc = {'null', 'base', kAppleManufacturer, 0, 0};
    Component nullComp = FindNextComponent(0, &nullDesc);
    Component disabledComp = 0;

    while (disabledComp = FindNextComponent(disabledComp, (ComponentDescription*)&cd[0]))
        CaptureComponent(disabledComp, nullComp);
}

void QTMovie::addClient(QTMovieClient* client)
{
    if (client)
        m_private->m_clients.append(client);
}

void QTMovie::removeClient(QTMovieClient* client)
{
    size_t indexOfClient = m_private->m_clients.find(client);
    if (indexOfClient != notFound)
        m_private->m_clients.remove(indexOfClient);
}

void QTMovie::play()
{
    m_private->m_timeToRestore = -1.0f;

    if (m_private->m_movieController)
        MCDoAction(m_private->m_movieController, mcActionPrerollAndPlay, (void *)GetMoviePreferredRate(m_private->m_movie));
    else
        StartMovie(m_private->m_movie);
    m_private->startTask();
}

void QTMovie::pause()
{
    m_private->m_timeToRestore = -1.0f;

    if (m_private->m_movieController)
        MCDoAction(m_private->m_movieController, mcActionPlay, 0);
    else
        StopMovie(m_private->m_movie);
    QTMovieTask::sharedTask()->updateTaskTimer();
}

float QTMovie::rate() const
{
    if (!m_private->m_movie)
        return 0;
    return FixedToFloat(GetMovieRate(m_private->m_movie));
}

void QTMovie::setRate(float rate)
{
    if (!m_private->m_movie)
        return;    
    m_private->m_timeToRestore = -1.0f;
    
    if (m_private->m_movieController)
        MCDoAction(m_private->m_movieController, mcActionPrerollAndPlay, (void *)FloatToFixed(rate));
    else
        SetMovieRate(m_private->m_movie, FloatToFixed(rate));
    QTMovieTask::sharedTask()->updateTaskTimer();
}

float QTMovie::duration() const
{
    if (!m_private->m_movie)
        return 0;
    TimeValue val = GetMovieDuration(m_private->m_movie);
    TimeScale scale = GetMovieTimeScale(m_private->m_movie);
    return static_cast<float>(val) / scale;
}

float QTMovie::currentTime() const
{
    if (!m_private->m_movie)
        return 0;
    TimeValue val = GetMovieTime(m_private->m_movie, 0);
    TimeScale scale = GetMovieTimeScale(m_private->m_movie);
    return static_cast<float>(val) / scale;
}

void QTMovie::setCurrentTime(float time) const
{
    if (!m_private->m_movie)
        return;

    m_private->m_timeToRestore = -1.0f;
    
    m_private->m_seeking = true;
    TimeScale scale = GetMovieTimeScale(m_private->m_movie);
    if (m_private->m_movieController) {
        QTRestartAtTimeRecord restart = { lroundf(time * scale) , 0 };
        MCDoAction(m_private->m_movieController, mcActionRestartAtTime, (void *)&restart);
    } else
        SetMovieTimeValue(m_private->m_movie, TimeValue(lroundf(time * scale)));
    QTMovieTask::sharedTask()->updateTaskTimer();
}

void QTMovie::setVolume(float volume)
{
    if (!m_private->m_movie)
        return;
    SetMovieVolume(m_private->m_movie, static_cast<short>(volume * 256));
}

void QTMovie::setPreservesPitch(bool preservesPitch)
{
    if (!m_private->m_movie || !m_private->m_currentURL)
        return;

    OSErr error;
    bool prop = false;

    error = QTGetMovieProperty(m_private->m_movie, kQTPropertyClass_Audio, kQTAudioPropertyID_RateChangesPreservePitch,
                               sizeof(kQTAudioPropertyID_RateChangesPreservePitch), static_cast<QTPropertyValuePtr>(&prop), 0);

    if (error || prop == preservesPitch)
        return;

    m_private->m_timeToRestore = currentTime();
    m_private->m_rateToRestore = rate();
    load(m_private->m_currentURL, preservesPitch);
}

unsigned QTMovie::dataSize() const
{
    if (!m_private->m_movie)
        return 0;
    return GetMovieDataSize(m_private->m_movie, 0, GetMovieDuration(m_private->m_movie));
}

float QTMovie::maxTimeLoaded() const
{
    if (!m_private->m_movie)
        return 0;
    TimeValue val;
    GetMaxLoadedTimeInMovie(m_private->m_movie, &val);
    TimeScale scale = GetMovieTimeScale(m_private->m_movie);
    return static_cast<float>(val) / scale;
}

long QTMovie::loadState() const
{
    return m_private->m_loadState;
}

void QTMovie::getNaturalSize(int& width, int& height)
{
    Rect rect = { 0, };

    if (m_private->m_movie)
        GetMovieNaturalBoundsRect(m_private->m_movie, &rect);
    width = (rect.right - rect.left) * m_private->m_widthScaleFactor;
    height = (rect.bottom - rect.top) * m_private->m_heightScaleFactor;
}

void QTMovie::loadPath(const UChar* url, int len, bool preservesPitch)
{
    CFStringRef urlStringRef = CFStringCreateWithCharacters(kCFAllocatorDefault, reinterpret_cast<const UniChar*>(url), len);
    CFURLRef cfURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, urlStringRef, kCFURLWindowsPathStyle, false);

    load(cfURL, preservesPitch);

    if (cfURL)
        CFRelease(cfURL);
    if (urlStringRef)
        CFRelease(urlStringRef);
}

void QTMovie::load(const UChar* url, int len, bool preservesPitch)
{
    CFStringRef urlStringRef = CFStringCreateWithCharacters(kCFAllocatorDefault, reinterpret_cast<const UniChar*>(url), len);
    CFURLRef cfURL = CFURLCreateWithString(kCFAllocatorDefault, urlStringRef, 0);

    load(cfURL, preservesPitch);

    if (cfURL)
        CFRelease(cfURL);
    if (urlStringRef)
        CFRelease(urlStringRef);
}

void QTMovie::load(CFURLRef url, bool preservesPitch)
{
    if (!url)
        return;

    if (m_private->m_movie) {
        m_private->endTask();
        if (m_private->m_movieController)
            DisposeMovieController(m_private->m_movieController);
        m_private->m_movieController = 0;
        DisposeMovie(m_private->m_movie);
        m_private->m_movie = 0;
        m_private->m_loadState = 0;
    }  

    // Define a property array for NewMovieFromProperties.
    QTNewMoviePropertyElement movieProps[9]; 
    ItemCount moviePropCount = 0; 

    bool boolTrue = true;
    
    // Disable streaming support for now. 
    CFStringRef scheme = CFURLCopyScheme(url);
    bool isRTSP = CFStringHasPrefix(scheme, CFSTR("rtsp:"));
    CFRelease(scheme);

    if (isRTSP) {
        m_private->m_loadError = noMovieFound;
        goto end;
    }

    if (m_private->m_currentURL) {
        if (m_private->m_currentURL != url) {
            CFRelease(m_private->m_currentURL);
            m_private->m_currentURL = url;
            CFRetain(url);
        }
    } else {
        m_private->m_currentURL = url;
        CFRetain(url);
    }

    // Add the movie data location to the property array 
    movieProps[moviePropCount].propClass = kQTPropertyClass_DataLocation; 
    movieProps[moviePropCount].propID = kQTDataLocationPropertyID_CFURL; 
    movieProps[moviePropCount].propValueSize = sizeof(m_private->m_currentURL); 
    movieProps[moviePropCount].propValueAddress = &(m_private->m_currentURL); 
    movieProps[moviePropCount].propStatus = 0; 
    moviePropCount++; 

    movieProps[moviePropCount].propClass = kQTPropertyClass_MovieInstantiation; 
    movieProps[moviePropCount].propID = kQTMovieInstantiationPropertyID_DontAskUnresolvedDataRefs; 
    movieProps[moviePropCount].propValueSize = sizeof(boolTrue); 
    movieProps[moviePropCount].propValueAddress = &boolTrue; 
    movieProps[moviePropCount].propStatus = 0; 
    moviePropCount++; 

    movieProps[moviePropCount].propClass = kQTPropertyClass_MovieInstantiation; 
    movieProps[moviePropCount].propID = kQTMovieInstantiationPropertyID_AsyncOK; 
    movieProps[moviePropCount].propValueSize = sizeof(boolTrue); 
    movieProps[moviePropCount].propValueAddress = &boolTrue; 
    movieProps[moviePropCount].propStatus = 0; 
    moviePropCount++; 

    movieProps[moviePropCount].propClass = kQTPropertyClass_NewMovieProperty; 
    movieProps[moviePropCount].propID = kQTNewMoviePropertyID_Active; 
    movieProps[moviePropCount].propValueSize = sizeof(boolTrue); 
    movieProps[moviePropCount].propValueAddress = &boolTrue; 
    movieProps[moviePropCount].propStatus = 0; 
    moviePropCount++; 

    movieProps[moviePropCount].propClass = kQTPropertyClass_NewMovieProperty; 
    movieProps[moviePropCount].propID = kQTNewMoviePropertyID_DontInteractWithUser; 
    movieProps[moviePropCount].propValueSize = sizeof(boolTrue); 
    movieProps[moviePropCount].propValueAddress = &boolTrue; 
    movieProps[moviePropCount].propStatus = 0; 
    moviePropCount++; 

    movieProps[moviePropCount].propClass = kQTPropertyClass_MovieInstantiation;
    movieProps[moviePropCount].propID = '!url';
    movieProps[moviePropCount].propValueSize = sizeof(boolTrue); 
    movieProps[moviePropCount].propValueAddress = &boolTrue; 
    movieProps[moviePropCount].propStatus = 0; 
    moviePropCount++; 

    movieProps[moviePropCount].propClass = kQTPropertyClass_MovieInstantiation; 
    movieProps[moviePropCount].propID = 'site';
    movieProps[moviePropCount].propValueSize = sizeof(boolTrue); 
    movieProps[moviePropCount].propValueAddress = &boolTrue; 
    movieProps[moviePropCount].propStatus = 0; 
    moviePropCount++;

    movieProps[moviePropCount].propClass = kQTPropertyClass_Audio; 
    movieProps[moviePropCount].propID = kQTAudioPropertyID_RateChangesPreservePitch;
    movieProps[moviePropCount].propValueSize = sizeof(preservesPitch); 
    movieProps[moviePropCount].propValueAddress = &preservesPitch; 
    movieProps[moviePropCount].propStatus = 0; 
    moviePropCount++; 

    bool allowCaching = !m_private->m_privateBrowsing;
    movieProps[moviePropCount].propClass = kQTPropertyClass_MovieInstantiation; 
    movieProps[moviePropCount].propID = 'pers';
    movieProps[moviePropCount].propValueSize = sizeof(allowCaching); 
    movieProps[moviePropCount].propValueAddress = &allowCaching; 
    movieProps[moviePropCount].propStatus = 0; 
    moviePropCount++;

    ASSERT(moviePropCount <= WTF_ARRAY_LENGTH(movieProps));
    m_private->m_loadError = NewMovieFromProperties(moviePropCount, movieProps, 0, 0, &m_private->m_movie);

end:
    m_private->startTask();
    // get the load fail callback quickly 
    if (m_private->m_loadError)
        QTMovieTask::sharedTask()->updateTaskTimer(0);
    else {
        OSType mode = kQTApertureMode_CleanAperture;

        // Set the aperture mode property on a movie to signal that we want aspect ratio
        // and clean aperture dimensions. Don't worry about errors, we can't do anything if
        // the installed version of QT doesn't support it and it isn't serious enough to 
        // warrant failing.
        QTSetMovieProperty(m_private->m_movie, kQTPropertyClass_Visual, kQTVisualPropertyID_ApertureMode, sizeof(mode), &mode);
    }
}

void QTMovie::disableUnsupportedTracks(unsigned& enabledTrackCount, unsigned& totalTrackCount)
{
    if (!m_private->m_movie) {
        totalTrackCount = 0;
        enabledTrackCount = 0;
        return;
    }

    static HashSet<OSType>* allowedTrackTypes = 0;
    if (!allowedTrackTypes) {
        allowedTrackTypes = new HashSet<OSType>;
        allowedTrackTypes->add(VideoMediaType);
        allowedTrackTypes->add(SoundMediaType);
        allowedTrackTypes->add(TextMediaType);
        allowedTrackTypes->add(BaseMediaType);
        allowedTrackTypes->add(closedCaptionTrackType);
        allowedTrackTypes->add(subTitleTrackType);
        allowedTrackTypes->add(mpeg4ObjectDescriptionTrackType);
        allowedTrackTypes->add(mpeg4SceneDescriptionTrackType);
        allowedTrackTypes->add(TimeCodeMediaType);
        allowedTrackTypes->add(TimeCode64MediaType);
    }

    long trackCount = GetMovieTrackCount(m_private->m_movie);
    enabledTrackCount = trackCount;
    totalTrackCount = trackCount;

    // Track indexes are 1-based. yuck. These things must descend from old-
    // school mac resources or something.
    for (long trackIndex = 1; trackIndex <= trackCount; trackIndex++) {
        // Grab the track at the current index. If there isn't one there, then
        // we can move onto the next one.
        Track currentTrack = GetMovieIndTrack(m_private->m_movie, trackIndex);
        if (!currentTrack)
            continue;
        
        // Check to see if the track is disabled already, we should move along.
        // We don't need to re-disable it.
        if (!GetTrackEnabled(currentTrack))
            continue;

        // Grab the track's media. We're going to check to see if we need to
        // disable the tracks. They could be unsupported.
        Media trackMedia = GetTrackMedia(currentTrack);
        if (!trackMedia)
            continue;
        
        // Grab the media type for this track. Make sure that we don't
        // get an error in doing so. If we do, then something really funky is
        // wrong.
        OSType mediaType;
        GetMediaHandlerDescription(trackMedia, &mediaType, nil, nil);
        OSErr mediaErr = GetMoviesError();    
        if (mediaErr != noErr)
            continue;
        
        if (!allowedTrackTypes->contains(mediaType)) {

            // Different mpeg variants import as different track types so check for the "mpeg 
            // characteristic" instead of hard coding the (current) list of mpeg media types.
            if (GetMovieIndTrackType(m_private->m_movie, 1, 'mpeg', movieTrackCharacteristic | movieTrackEnabledOnly))
                continue;

            SetTrackEnabled(currentTrack, false);
            --enabledTrackCount;
        }
        
        // Grab the track reference count for chapters. This will tell us if it
        // has chapter tracks in it. If there aren't any references, then we
        // can move on the next track.
        long referenceCount = GetTrackReferenceCount(currentTrack, kTrackReferenceChapterList);
        if (referenceCount <= 0)
            continue;
        
        long referenceIndex = 0;        
        while (1) {
            // If we get nothing here, we've overstepped our bounds and can stop
            // looking. Chapter indices here are 1-based as well - hence, the
            // pre-increment.
            referenceIndex++;
            Track chapterTrack = GetTrackReference(currentTrack, kTrackReferenceChapterList, referenceIndex);
            if (!chapterTrack)
                break;
            
            // Try to grab the media for the track.
            Media chapterMedia = GetTrackMedia(chapterTrack);
            if (!chapterMedia)
                continue;
        
            // Grab the media type for this track. Make sure that we don't
            // get an error in doing so. If we do, then something really
            // funky is wrong.
            OSType mediaType;
            GetMediaHandlerDescription(chapterMedia, &mediaType, nil, nil);
            OSErr mediaErr = GetMoviesError();
            if (mediaErr != noErr)
                continue;
            
            // Check to see if the track is a video track. We don't care about
            // other non-video tracks.
            if (mediaType != VideoMediaType)
                continue;
            
            // Check to see if the track is already disabled. If it is, we
            // should move along.
            if (!GetTrackEnabled(chapterTrack))
                continue;
            
            // Disabled the evil, evil track.
            SetTrackEnabled(chapterTrack, false);
            --enabledTrackCount;
        }
    }
}

bool QTMovie::isDisabled() const
{
    return m_private->m_disabled;
}

void QTMovie::setDisabled(bool b)
{
    m_private->m_disabled = b;
}


bool QTMovie::hasVideo() const
{
    if (!m_private->m_movie)
        return false;
    return (GetMovieIndTrackType(m_private->m_movie, 1, VisualMediaCharacteristic, movieTrackCharacteristic | movieTrackEnabledOnly));
}

bool QTMovie::hasAudio() const
{
    if (!m_private->m_movie)
        return false;
    return (GetMovieIndTrackType(m_private->m_movie, 1, AudioMediaCharacteristic, movieTrackCharacteristic | movieTrackEnabledOnly));
}

QTTrackArray QTMovie::videoTracks() const
{
    QTTrackArray tracks;
    long trackIndex = 1;

    while (Track theTrack = GetMovieIndTrackType(m_private->m_movie, trackIndex++, VisualMediaCharacteristic, movieTrackCharacteristic | movieTrackEnabledOnly))
        tracks.append(QTTrack::create(theTrack));

    return tracks;
}

bool QTMovie::hasClosedCaptions() const 
{
    if (!m_private->m_movie)
        return false;
    return GetMovieIndTrackType(m_private->m_movie, 1, closedCaptionTrackType, movieTrackMediaType);
}

void QTMovie::setClosedCaptionsVisible(bool visible)
{
    if (!m_private->m_movie)
        return;

    Track ccTrack = GetMovieIndTrackType(m_private->m_movie, 1, closedCaptionTrackType, movieTrackMediaType);
    if (!ccTrack)
        return;

    Boolean doDisplay = visible;
    QTSetTrackProperty(ccTrack, closedCaptionTrackType, closedCaptionDisplayPropertyID, sizeof(doDisplay), &doDisplay);
}

long QTMovie::timeScale() const 
{
    if (!m_private->m_movie)
        return 0;

    return GetMovieTimeScale(m_private->m_movie);
}

static void getMIMETypeCallBack(const char* type);

static void initializeSupportedTypes() 
{
    if (gSupportedTypes)
        return;

    gSupportedTypes = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
    if (quickTimeVersion < minimumQuickTimeVersion) {
        LOG_ERROR("QuickTime version %x detected, at least %x required. Returning empty list of supported media MIME types.", quickTimeVersion, minimumQuickTimeVersion);
        return;
    }

    // QuickTime doesn't have an importer for video/quicktime. Add it manually.
    CFArrayAppendValue(gSupportedTypes, CFSTR("video/quicktime"));
    
    wkGetQuickTimeMIMETypeList(getMIMETypeCallBack);
}

static void getMIMETypeCallBack(const char* type)
{
    ASSERT(type);
    CFStringRef cfType = CFStringCreateWithCString(kCFAllocatorDefault, type, kCFStringEncodingMacRoman);
    if (!cfType)
        return;

    // Filter out all non-audio or -video MIME Types, and only add each type once:
    if (CFStringHasPrefix(cfType, CFSTR("audio/")) || CFStringHasPrefix(cfType, CFSTR("video/"))) {
        CFRange range = CFRangeMake(0, CFArrayGetCount(gSupportedTypes));
        if (!CFArrayContainsValue(gSupportedTypes, range, cfType))
            CFArrayAppendValue(gSupportedTypes, cfType);
    }

    CFRelease(cfType);
}

unsigned QTMovie::countSupportedTypes()
{
    initializeSupportedTypes();
    return static_cast<unsigned>(CFArrayGetCount(gSupportedTypes));
}

void QTMovie::getSupportedType(unsigned index, const UChar*& str, unsigned& len)
{
    initializeSupportedTypes();
    ASSERT(index < CFArrayGetCount(gSupportedTypes));

    // Allocate sufficient buffer to hold any MIME type
    static UniChar* staticBuffer = 0;
    if (!staticBuffer)
        staticBuffer = new UniChar[32];

    CFStringRef cfstr = (CFStringRef)CFArrayGetValueAtIndex(gSupportedTypes, index);
    len = CFStringGetLength(cfstr);
    CFRange range = { 0, len };
    CFStringGetCharacters(cfstr, range, staticBuffer);
    str = reinterpret_cast<const UChar*>(staticBuffer);
    
}

CGAffineTransform QTMovie::getTransform() const
{
    ASSERT(m_private->m_movie);
    MatrixRecord m = {0};
    GetMovieMatrix(m_private->m_movie, &m);

    ASSERT(!m.matrix[0][2]);
    ASSERT(!m.matrix[1][2]);
    CGAffineTransform transform = CGAffineTransformMake(
        Fix2X(m.matrix[0][0]),
        Fix2X(m.matrix[0][1]),
        Fix2X(m.matrix[1][0]),
        Fix2X(m.matrix[1][1]),
        Fix2X(m.matrix[2][0]),
        Fix2X(m.matrix[2][1]));
    return transform;
}

void QTMovie::setTransform(CGAffineTransform t)
{
    ASSERT(m_private->m_movie);
    MatrixRecord m = {{
        {X2Fix(t.a), X2Fix(t.b), 0},
        {X2Fix(t.c), X2Fix(t.d), 0},
        {X2Fix(t.tx), X2Fix(t.ty), fract1},
    }};

    SetMovieMatrix(m_private->m_movie, &m);
    m_private->cacheMovieScale();
}

void QTMovie::resetTransform()
{
    ASSERT(m_private->m_movie);
    SetMovieMatrix(m_private->m_movie, 0);
    m_private->cacheMovieScale();
}

void QTMovie::setPrivateBrowsingMode(bool privateBrowsing)
{
    m_private->m_privateBrowsing = privateBrowsing;
    if (m_private->m_movie) {
        bool allowCaching = !m_private->m_privateBrowsing;
        QTSetMovieProperty(m_private->m_movie, 'cach', 'pers', sizeof(allowCaching), &allowCaching);
    }
}

bool QTMovie::initializeQuickTime() 
{
    static bool initialized = false;
    static bool initializationSucceeded = false;
    if (!initialized) {
        initialized = true;
        // Initialize and check QuickTime version
        OSErr result = InitializeQTML(kInitializeQTMLEnableDoubleBufferedSurface);
        if (result == noErr)
            result = Gestalt(gestaltQuickTime, &quickTimeVersion);
        if (result != noErr) {
            LOG_ERROR("No QuickTime available. Disabling <video> and <audio> support.");
            return false;
        }
        if (quickTimeVersion < minimumQuickTimeVersion) {
            LOG_ERROR("QuickTime version %x detected, at least %x required. Disabling <video> and <audio> support.", quickTimeVersion, minimumQuickTimeVersion);
            return false;
        }
        EnterMovies();
        initializationSucceeded = true;
    }
    return initializationSucceeded;
}

Movie QTMovie::getMovieHandle() const 
{
    return m_private->m_movie;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
        return TRUE;
    case DLL_PROCESS_DETACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        return FALSE;
    }
    ASSERT_NOT_REACHED();
    return FALSE;
}