1/*
2 * Copyright (C) 2007, 2008, 2009, 2010, 2011 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#import "config.h"
27
28#if ENABLE(VIDEO)
29
30#import "MediaPlayerPrivateQTKit.h"
31
32#if ENABLE(OFFLINE_WEB_APPLICATIONS)
33#include "ApplicationCacheHost.h"
34#include "ApplicationCacheResource.h"
35#include "DocumentLoader.h"
36#endif
37
38#ifdef BUILDING_ON_TIGER
39#import "AutodrainedPool.h"
40#endif
41
42#import "BlockExceptions.h"
43#import "DocumentLoader.h"
44#import "FrameView.h"
45#import "GraphicsContext.h"
46#import "KURL.h"
47#import "MIMETypeRegistry.h"
48#import "SoftLinking.h"
49#import "TimeRanges.h"
50#import "WebCoreSystemInterface.h"
51#import <QTKit/QTKit.h>
52#import <objc/objc-runtime.h>
53#import <wtf/UnusedParam.h>
54
55#if USE(ACCELERATED_COMPOSITING)
56#include "GraphicsLayer.h"
57#endif
58
59#if DRAW_FRAME_RATE
60#import "Font.h"
61#import "Frame.h"
62#import "Document.h"
63#import "RenderObject.h"
64#import "RenderStyle.h"
65#endif
66
67#ifdef BUILDING_ON_TIGER
68static IMP method_setImplementation(Method m, IMP imp)
69{
70    IMP result = m->method_imp;
71    m->method_imp = imp;
72    return result;
73}
74#endif
75
76SOFT_LINK_FRAMEWORK(QTKit)
77
78SOFT_LINK(QTKit, QTMakeTime, QTTime, (long long timeValue, long timeScale), (timeValue, timeScale))
79
80SOFT_LINK_CLASS(QTKit, QTMovie)
81SOFT_LINK_CLASS(QTKit, QTMovieView)
82SOFT_LINK_CLASS(QTKit, QTMovieLayer)
83
84SOFT_LINK_POINTER(QTKit, QTTrackMediaTypeAttribute, NSString *)
85SOFT_LINK_POINTER(QTKit, QTMediaTypeAttribute, NSString *)
86SOFT_LINK_POINTER(QTKit, QTMediaTypeBase, NSString *)
87SOFT_LINK_POINTER(QTKit, QTMediaTypeMPEG, NSString *)
88SOFT_LINK_POINTER(QTKit, QTMediaTypeSound, NSString *)
89SOFT_LINK_POINTER(QTKit, QTMediaTypeText, NSString *)
90SOFT_LINK_POINTER(QTKit, QTMediaTypeVideo, NSString *)
91SOFT_LINK_POINTER(QTKit, QTMovieAskUnresolvedDataRefsAttribute, NSString *)
92SOFT_LINK_POINTER(QTKit, QTMovieLoopsAttribute, NSString *)
93SOFT_LINK_POINTER(QTKit, QTMovieDataAttribute, NSString *)
94SOFT_LINK_POINTER(QTKit, QTMovieDataSizeAttribute, NSString *)
95SOFT_LINK_POINTER(QTKit, QTMovieDidEndNotification, NSString *)
96SOFT_LINK_POINTER(QTKit, QTMovieHasVideoAttribute, NSString *)
97SOFT_LINK_POINTER(QTKit, QTMovieHasAudioAttribute, NSString *)
98SOFT_LINK_POINTER(QTKit, QTMovieIsActiveAttribute, NSString *)
99SOFT_LINK_POINTER(QTKit, QTMovieLoadStateAttribute, NSString *)
100SOFT_LINK_POINTER(QTKit, QTMovieLoadStateDidChangeNotification, NSString *)
101SOFT_LINK_POINTER(QTKit, QTMovieNaturalSizeAttribute, NSString *)
102SOFT_LINK_POINTER(QTKit, QTMovieCurrentSizeAttribute, NSString *)
103SOFT_LINK_POINTER(QTKit, QTMoviePreventExternalURLLinksAttribute, NSString *)
104SOFT_LINK_POINTER(QTKit, QTMovieRateChangesPreservePitchAttribute, NSString *)
105SOFT_LINK_POINTER(QTKit, QTMovieRateDidChangeNotification, NSString *)
106SOFT_LINK_POINTER(QTKit, QTMovieSizeDidChangeNotification, NSString *)
107SOFT_LINK_POINTER(QTKit, QTMovieTimeDidChangeNotification, NSString *)
108SOFT_LINK_POINTER(QTKit, QTMovieTimeScaleAttribute, NSString *)
109SOFT_LINK_POINTER(QTKit, QTMovieURLAttribute, NSString *)
110SOFT_LINK_POINTER(QTKit, QTMovieVolumeDidChangeNotification, NSString *)
111SOFT_LINK_POINTER(QTKit, QTSecurityPolicyNoCrossSiteAttribute, NSString *)
112SOFT_LINK_POINTER(QTKit, QTVideoRendererWebKitOnlyNewImageAvailableNotification, NSString *)
113#ifndef BUILDING_ON_TIGER
114SOFT_LINK_POINTER(QTKit, QTMovieApertureModeClean, NSString *)
115SOFT_LINK_POINTER(QTKit, QTMovieApertureModeAttribute, NSString *)
116#endif
117
118#define QTMovie getQTMovieClass()
119#define QTMovieView getQTMovieViewClass()
120#define QTMovieLayer getQTMovieLayerClass()
121
122#define QTTrackMediaTypeAttribute getQTTrackMediaTypeAttribute()
123#define QTMediaTypeAttribute getQTMediaTypeAttribute()
124#define QTMediaTypeBase getQTMediaTypeBase()
125#define QTMediaTypeMPEG getQTMediaTypeMPEG()
126#define QTMediaTypeSound getQTMediaTypeSound()
127#define QTMediaTypeText getQTMediaTypeText()
128#define QTMediaTypeVideo getQTMediaTypeVideo()
129#define QTMovieAskUnresolvedDataRefsAttribute getQTMovieAskUnresolvedDataRefsAttribute()
130#define QTMovieLoopsAttribute getQTMovieLoopsAttribute()
131#define QTMovieDataAttribute getQTMovieDataAttribute()
132#define QTMovieDataSizeAttribute getQTMovieDataSizeAttribute()
133#define QTMovieDidEndNotification getQTMovieDidEndNotification()
134#define QTMovieHasVideoAttribute getQTMovieHasVideoAttribute()
135#define QTMovieHasAudioAttribute getQTMovieHasAudioAttribute()
136#define QTMovieIsActiveAttribute getQTMovieIsActiveAttribute()
137#define QTMovieLoadStateAttribute getQTMovieLoadStateAttribute()
138#define QTMovieLoadStateDidChangeNotification getQTMovieLoadStateDidChangeNotification()
139#define QTMovieNaturalSizeAttribute getQTMovieNaturalSizeAttribute()
140#define QTMovieCurrentSizeAttribute getQTMovieCurrentSizeAttribute()
141#define QTMoviePreventExternalURLLinksAttribute getQTMoviePreventExternalURLLinksAttribute()
142#define QTMovieRateChangesPreservePitchAttribute getQTMovieRateChangesPreservePitchAttribute()
143#define QTMovieRateDidChangeNotification getQTMovieRateDidChangeNotification()
144#define QTMovieSizeDidChangeNotification getQTMovieSizeDidChangeNotification()
145#define QTMovieTimeDidChangeNotification getQTMovieTimeDidChangeNotification()
146#define QTMovieTimeScaleAttribute getQTMovieTimeScaleAttribute()
147#define QTMovieURLAttribute getQTMovieURLAttribute()
148#define QTMovieVolumeDidChangeNotification getQTMovieVolumeDidChangeNotification()
149#define QTSecurityPolicyNoCrossSiteAttribute getQTSecurityPolicyNoCrossSiteAttribute()
150#define QTVideoRendererWebKitOnlyNewImageAvailableNotification getQTVideoRendererWebKitOnlyNewImageAvailableNotification()
151#ifndef BUILDING_ON_TIGER
152#define QTMovieApertureModeClean getQTMovieApertureModeClean()
153#define QTMovieApertureModeAttribute getQTMovieApertureModeAttribute()
154#endif
155
156// Older versions of the QTKit header don't have these constants.
157#if !defined QTKIT_VERSION_MAX_ALLOWED || QTKIT_VERSION_MAX_ALLOWED <= QTKIT_VERSION_7_0
158enum {
159    QTMovieLoadStateError = -1L,
160    QTMovieLoadStateLoaded  = 2000L,
161    QTMovieLoadStatePlayable = 10000L,
162    QTMovieLoadStatePlaythroughOK = 20000L,
163    QTMovieLoadStateComplete = 100000L
164};
165#endif
166
167@interface FakeQTMovieView : NSObject
168- (WebCoreMovieObserver *)delegate;
169@end
170
171using namespace WebCore;
172using namespace std;
173
174@interface WebCoreMovieObserver : NSObject
175{
176    MediaPlayerPrivateQTKit* m_callback;
177    NSView* m_view;
178    BOOL m_delayCallbacks;
179}
180-(id)initWithCallback:(MediaPlayerPrivateQTKit*)callback;
181-(void)disconnect;
182-(void)setView:(NSView*)view;
183-(void)repaint;
184-(void)setDelayCallbacks:(BOOL)shouldDelay;
185-(void)loadStateChanged:(NSNotification *)notification;
186-(void)rateChanged:(NSNotification *)notification;
187-(void)sizeChanged:(NSNotification *)notification;
188-(void)timeChanged:(NSNotification *)notification;
189-(void)didEnd:(NSNotification *)notification;
190@end
191
192@protocol WebKitVideoRenderingDetails
193-(void)setMovie:(id)movie;
194-(void)drawInRect:(NSRect)rect;
195@end
196
197namespace WebCore {
198
199#ifdef BUILDING_ON_TIGER
200static const long minimumQuickTimeVersion = 0x07300000; // 7.3
201#endif
202
203
204MediaPlayerPrivateInterface* MediaPlayerPrivateQTKit::create(MediaPlayer* player)
205{
206    return new MediaPlayerPrivateQTKit(player);
207}
208
209void MediaPlayerPrivateQTKit::registerMediaEngine(MediaEngineRegistrar registrar)
210{
211    if (isAvailable())
212        registrar(create, getSupportedTypes, supportsType, 0, 0, 0);
213}
214
215MediaPlayerPrivateQTKit::MediaPlayerPrivateQTKit(MediaPlayer* player)
216    : m_player(player)
217    , m_objcObserver(AdoptNS, [[WebCoreMovieObserver alloc] initWithCallback:this])
218    , m_seekTo(-1)
219    , m_seekTimer(this, &MediaPlayerPrivateQTKit::seekTimerFired)
220    , m_networkState(MediaPlayer::Empty)
221    , m_readyState(MediaPlayer::HaveNothing)
222    , m_rect()
223    , m_scaleFactor(1, 1)
224    , m_enabledTrackCount(0)
225    , m_totalTrackCount(0)
226    , m_reportedDuration(-1)
227    , m_cachedDuration(-1)
228    , m_timeToRestore(-1)
229    , m_preload(MediaPlayer::Auto)
230    , m_startedPlaying(false)
231    , m_isStreaming(false)
232    , m_visible(false)
233    , m_hasUnsupportedTracks(false)
234    , m_videoFrameHasDrawn(false)
235    , m_isAllowedToRender(false)
236    , m_privateBrowsing(false)
237#if DRAW_FRAME_RATE
238    , m_frameCountWhilePlaying(0)
239    , m_timeStartedPlaying(0)
240    , m_timeStoppedPlaying(0)
241#endif
242{
243}
244
245MediaPlayerPrivateQTKit::~MediaPlayerPrivateQTKit()
246{
247    tearDownVideoRendering();
248
249    [[NSNotificationCenter defaultCenter] removeObserver:m_objcObserver.get()];
250    [m_objcObserver.get() disconnect];
251}
252
253NSMutableDictionary* MediaPlayerPrivateQTKit::commonMovieAttributes()
254{
255    return [NSMutableDictionary dictionaryWithObjectsAndKeys:
256            [NSNumber numberWithBool:m_player->preservesPitch()], QTMovieRateChangesPreservePitchAttribute,
257            [NSNumber numberWithBool:YES], QTMoviePreventExternalURLLinksAttribute,
258            [NSNumber numberWithBool:YES], QTSecurityPolicyNoCrossSiteAttribute,
259            [NSNumber numberWithBool:NO], QTMovieAskUnresolvedDataRefsAttribute,
260            [NSNumber numberWithBool:NO], QTMovieLoopsAttribute,
261            [NSNumber numberWithBool:!m_privateBrowsing], @"QTMovieAllowPersistentCacheAttribute",
262#ifndef BUILDING_ON_TIGER
263            QTMovieApertureModeClean, QTMovieApertureModeAttribute,
264#endif
265            nil];
266}
267
268void MediaPlayerPrivateQTKit::createQTMovie(const String& url)
269{
270    NSURL *cocoaURL = KURL(ParsedURLString, url);
271    NSMutableDictionary *movieAttributes = commonMovieAttributes();
272    [movieAttributes setValue:cocoaURL forKey:QTMovieURLAttribute];
273
274#if !defined(BUILDING_ON_LEOPARD)
275    CFDictionaryRef proxySettings = CFNetworkCopySystemProxySettings();
276    CFArrayRef proxiesForURL = CFNetworkCopyProxiesForURL((CFURLRef)cocoaURL, proxySettings);
277    BOOL willUseProxy = YES;
278
279    if (!proxiesForURL || !CFArrayGetCount(proxiesForURL))
280        willUseProxy = NO;
281
282    if (CFArrayGetCount(proxiesForURL) == 1) {
283        CFDictionaryRef proxy = (CFDictionaryRef)CFArrayGetValueAtIndex(proxiesForURL, 0);
284        ASSERT(CFGetTypeID(proxy) == CFDictionaryGetTypeID());
285
286        CFStringRef proxyType = (CFStringRef)CFDictionaryGetValue(proxy, kCFProxyTypeKey);
287        ASSERT(CFGetTypeID(proxyType) == CFStringGetTypeID());
288
289        if (CFStringCompare(proxyType, kCFProxyTypeNone, 0) == kCFCompareEqualTo)
290            willUseProxy = NO;
291    }
292
293    if (!willUseProxy) {
294        // Only pass the QTMovieOpenForPlaybackAttribute flag if there are no proxy servers, due
295        // to rdar://problem/7531776.
296        [movieAttributes setObject:[NSNumber numberWithBool:YES] forKey:@"QTMovieOpenForPlaybackAttribute"];
297    }
298
299    if (proxiesForURL)
300        CFRelease(proxiesForURL);
301    if (proxySettings)
302        CFRelease(proxySettings);
303#endif
304
305    createQTMovie(cocoaURL, movieAttributes);
306}
307
308void MediaPlayerPrivateQTKit::createQTMovie(ApplicationCacheResource* resource)
309{
310#if ENABLE(OFFLINE_WEB_APPLICATIONS)
311    ASSERT(resource);
312
313    NSMutableDictionary *movieAttributes = commonMovieAttributes();
314    [movieAttributes setObject:[NSNumber numberWithBool:YES] forKey:@"QTMovieOpenForPlaybackAttribute"];
315
316    // ApplicationCacheResources can supply either a data pointer, or a path to a locally cached
317    // flat file.  We would prefer the path over the data, but QTKit can handle either:
318    String localPath = resource->path();
319    NSURL* cocoaURL = !localPath.isEmpty() ? [NSURL fileURLWithPath:localPath isDirectory:NO] : nil;
320    if (cocoaURL)
321        [movieAttributes setValue:cocoaURL forKey:QTMovieURLAttribute];
322    else {
323        NSData* movieData = resource->data()->createNSData();
324        [movieAttributes setValue:movieData forKey:QTMovieDataAttribute];
325        [movieData release];
326    }
327
328    createQTMovie(cocoaURL, movieAttributes);
329
330#else
331    ASSERT_NOT_REACHED();
332#endif
333}
334
335static void disableComponentsOnce()
336{
337    static bool sComponentsDisabled = false;
338    if (sComponentsDisabled)
339        return;
340    sComponentsDisabled = true;
341
342    // eat/PDF and grip/PDF components must be disabled twice since they are registered twice
343    // with different flags.  However, there is currently a bug in 64-bit QTKit (<rdar://problem/8378237>)
344    // which causes subsequent disable component requests of exactly the same type to be ignored if
345    // QTKitServer has not yet started.  As a result, we must pass in exactly the flags we want to
346    // disable per component.  As a failsafe, if in the future these flags change, we will disable the
347    // PDF components for a third time with a wildcard flags field:
348    uint32_t componentsToDisable[11][5] = {
349        {'eat ', 'TEXT', 'text', 0, 0},
350        {'eat ', 'TXT ', 'text', 0, 0},
351        {'eat ', 'utxt', 'text', 0, 0},
352        {'eat ', 'TEXT', 'tx3g', 0, 0},
353        {'eat ', 'PDF ', 'vide', 0x44802, 0},
354        {'eat ', 'PDF ', 'vide', 0x45802, 0},
355        {'eat ', 'PDF ', 'vide', 0, 0},
356        {'grip', 'PDF ', 'appl', 0x844a00, 0},
357        {'grip', 'PDF ', 'appl', 0x845a00, 0},
358        {'grip', 'PDF ', 'appl', 0, 0},
359        {'imdc', 'pdf ', 'appl', 0, 0},
360    };
361
362    for (size_t i = 0; i < WTF_ARRAY_LENGTH(componentsToDisable); ++i)
363        wkQTMovieDisableComponent(componentsToDisable[i]);
364}
365
366void MediaPlayerPrivateQTKit::createQTMovie(NSURL *url, NSDictionary *movieAttributes)
367{
368    disableComponentsOnce();
369
370    [[NSNotificationCenter defaultCenter] removeObserver:m_objcObserver.get()];
371
372    bool recreating = false;
373    if (m_qtMovie) {
374        recreating = true;
375        destroyQTVideoRenderer();
376        m_qtMovie = 0;
377    }
378
379    // Disable rtsp streams for now, <rdar://problem/5693967>
380    if (protocolIs([url scheme], "rtsp"))
381        return;
382
383    NSError *error = nil;
384    m_qtMovie.adoptNS([[QTMovie alloc] initWithAttributes:movieAttributes error:&error]);
385
386    if (!m_qtMovie)
387        return;
388
389    [m_qtMovie.get() setVolume:m_player->volume()];
390
391    if (recreating && hasVideo())
392        createQTVideoRenderer(QTVideoRendererModeListensForNewImages);
393
394    [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get()
395                                             selector:@selector(loadStateChanged:)
396                                                 name:QTMovieLoadStateDidChangeNotification
397                                               object:m_qtMovie.get()];
398
399    // In updateState(), we track when maxTimeLoaded() == duration().
400    // In newer version of QuickTime, a notification is emitted when maxTimeLoaded changes.
401    // In older version of QuickTime, QTMovieLoadStateDidChangeNotification be fired.
402    if (NSString *maxTimeLoadedChangeNotification = wkQTMovieMaxTimeLoadedChangeNotification()) {
403        [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get()
404                                                 selector:@selector(loadStateChanged:)
405                                                     name:maxTimeLoadedChangeNotification
406                                                   object:m_qtMovie.get()];
407    }
408
409    [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get()
410                                             selector:@selector(rateChanged:)
411                                                 name:QTMovieRateDidChangeNotification
412                                               object:m_qtMovie.get()];
413    [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get()
414                                             selector:@selector(sizeChanged:)
415                                                 name:QTMovieSizeDidChangeNotification
416                                               object:m_qtMovie.get()];
417    [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get()
418                                             selector:@selector(timeChanged:)
419                                                 name:QTMovieTimeDidChangeNotification
420                                               object:m_qtMovie.get()];
421    [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get()
422                                             selector:@selector(didEnd:)
423                                                 name:QTMovieDidEndNotification
424                                               object:m_qtMovie.get()];
425}
426
427static void mainThreadSetNeedsDisplay(id self, SEL)
428{
429    id view = [self superview];
430    ASSERT(!view || [view isKindOfClass:[QTMovieView class]]);
431    if (!view || ![view isKindOfClass:[QTMovieView class]])
432        return;
433
434    FakeQTMovieView *movieView = static_cast<FakeQTMovieView *>(view);
435    WebCoreMovieObserver* delegate = [movieView delegate];
436    ASSERT(!delegate || [delegate isKindOfClass:[WebCoreMovieObserver class]]);
437    if (!delegate || ![delegate isKindOfClass:[WebCoreMovieObserver class]])
438        return;
439
440    [delegate repaint];
441}
442
443static Class QTVideoRendererClass()
444{
445     static Class QTVideoRendererWebKitOnlyClass = NSClassFromString(@"QTVideoRendererWebKitOnly");
446     return QTVideoRendererWebKitOnlyClass;
447}
448
449void MediaPlayerPrivateQTKit::createQTMovieView()
450{
451    detachQTMovieView();
452
453    static bool addedCustomMethods = false;
454    if (!m_player->inMediaDocument() && !addedCustomMethods) {
455        Class QTMovieContentViewClass = NSClassFromString(@"QTMovieContentView");
456        ASSERT(QTMovieContentViewClass);
457
458        Method mainThreadSetNeedsDisplayMethod = class_getInstanceMethod(QTMovieContentViewClass, @selector(_mainThreadSetNeedsDisplay));
459        ASSERT(mainThreadSetNeedsDisplayMethod);
460
461        method_setImplementation(mainThreadSetNeedsDisplayMethod, reinterpret_cast<IMP>(mainThreadSetNeedsDisplay));
462        addedCustomMethods = true;
463    }
464
465    // delay callbacks as we *will* get notifications during setup
466    [m_objcObserver.get() setDelayCallbacks:YES];
467
468    m_qtMovieView.adoptNS([[QTMovieView alloc] init]);
469    setSize(m_player->size());
470    NSView* parentView = m_player->frameView()->documentView();
471    [parentView addSubview:m_qtMovieView.get()];
472#ifdef BUILDING_ON_TIGER
473    // setDelegate: isn't a public call in Tiger, so use performSelector to keep the compiler happy
474    [m_qtMovieView.get() performSelector:@selector(setDelegate:) withObject:m_objcObserver.get()];
475#else
476    [m_qtMovieView.get() setDelegate:m_objcObserver.get()];
477#endif
478    [m_objcObserver.get() setView:m_qtMovieView.get()];
479    [m_qtMovieView.get() setMovie:m_qtMovie.get()];
480    [m_qtMovieView.get() setControllerVisible:NO];
481    [m_qtMovieView.get() setPreservesAspectRatio:NO];
482    // the area not covered by video should be transparent
483    [m_qtMovieView.get() setFillColor:[NSColor clearColor]];
484
485    // If we're in a media document, allow QTMovieView to render in its default mode;
486    // otherwise tell it to draw synchronously.
487    // Note that we expect mainThreadSetNeedsDisplay to be invoked only when synchronous drawing is requested.
488    if (!m_player->inMediaDocument())
489        wkQTMovieViewSetDrawSynchronously(m_qtMovieView.get(), YES);
490
491    [m_objcObserver.get() setDelayCallbacks:NO];
492}
493
494void MediaPlayerPrivateQTKit::detachQTMovieView()
495{
496    if (m_qtMovieView) {
497        [m_objcObserver.get() setView:nil];
498#ifdef BUILDING_ON_TIGER
499        // setDelegate: isn't a public call in Tiger, so use performSelector to keep the compiler happy
500        [m_qtMovieView.get() performSelector:@selector(setDelegate:) withObject:nil];
501#else
502        [m_qtMovieView.get() setDelegate:nil];
503#endif
504        [m_qtMovieView.get() removeFromSuperview];
505        m_qtMovieView = nil;
506    }
507}
508
509void MediaPlayerPrivateQTKit::createQTVideoRenderer(QTVideoRendererMode rendererMode)
510{
511    destroyQTVideoRenderer();
512
513    m_qtVideoRenderer.adoptNS([[QTVideoRendererClass() alloc] init]);
514    if (!m_qtVideoRenderer)
515        return;
516
517    // associate our movie with our instance of QTVideoRendererWebKitOnly
518    [(id<WebKitVideoRenderingDetails>)m_qtVideoRenderer.get() setMovie:m_qtMovie.get()];
519
520    if (rendererMode == QTVideoRendererModeListensForNewImages) {
521        // listen to QTVideoRendererWebKitOnly's QTVideoRendererWebKitOnlyNewImageDidBecomeAvailableNotification
522        [[NSNotificationCenter defaultCenter] addObserver:m_objcObserver.get()
523                                                 selector:@selector(newImageAvailable:)
524                                                     name:QTVideoRendererWebKitOnlyNewImageAvailableNotification
525                                                   object:m_qtVideoRenderer.get()];
526    }
527}
528
529void MediaPlayerPrivateQTKit::destroyQTVideoRenderer()
530{
531    if (!m_qtVideoRenderer)
532        return;
533
534    // stop observing the renderer's notifications before we toss it
535    [[NSNotificationCenter defaultCenter] removeObserver:m_objcObserver.get()
536                                                    name:QTVideoRendererWebKitOnlyNewImageAvailableNotification
537                                                  object:m_qtVideoRenderer.get()];
538
539    // disassociate our movie from our instance of QTVideoRendererWebKitOnly
540    [(id<WebKitVideoRenderingDetails>)m_qtVideoRenderer.get() setMovie:nil];
541
542    m_qtVideoRenderer = nil;
543}
544
545void MediaPlayerPrivateQTKit::createQTMovieLayer()
546{
547#if USE(ACCELERATED_COMPOSITING)
548    if (!m_qtMovie)
549        return;
550
551    ASSERT(supportsAcceleratedRendering());
552
553    if (!m_qtVideoLayer) {
554        m_qtVideoLayer.adoptNS([[QTMovieLayer alloc] init]);
555        if (!m_qtVideoLayer)
556            return;
557
558        [m_qtVideoLayer.get() setMovie:m_qtMovie.get()];
559#ifndef NDEBUG
560        [(CALayer *)m_qtVideoLayer.get() setName:@"Video layer"];
561#endif
562        // The layer will get hooked up via RenderLayerBacking::updateGraphicsLayerConfiguration().
563    }
564#endif
565}
566
567void MediaPlayerPrivateQTKit::destroyQTMovieLayer()
568{
569#if USE(ACCELERATED_COMPOSITING)
570    if (!m_qtVideoLayer)
571        return;
572
573    // disassociate our movie from our instance of QTMovieLayer
574    [m_qtVideoLayer.get() setMovie:nil];
575    m_qtVideoLayer = nil;
576#endif
577}
578
579MediaPlayerPrivateQTKit::MediaRenderingMode MediaPlayerPrivateQTKit::currentRenderingMode() const
580{
581    if (m_qtMovieView)
582        return MediaRenderingMovieView;
583
584    if (m_qtVideoLayer)
585        return MediaRenderingMovieLayer;
586
587    if (m_qtVideoRenderer)
588        return MediaRenderingSoftwareRenderer;
589
590    return MediaRenderingNone;
591}
592
593MediaPlayerPrivateQTKit::MediaRenderingMode MediaPlayerPrivateQTKit::preferredRenderingMode() const
594{
595    if (!m_player->frameView() || !m_qtMovie)
596        return MediaRenderingNone;
597
598#if USE(ACCELERATED_COMPOSITING)
599    if (supportsAcceleratedRendering() && m_player->mediaPlayerClient()->mediaPlayerRenderingCanBeAccelerated(m_player))
600        return MediaRenderingMovieLayer;
601#endif
602
603    if (!QTVideoRendererClass())
604        return MediaRenderingMovieView;
605
606    return MediaRenderingSoftwareRenderer;
607}
608
609void MediaPlayerPrivateQTKit::setUpVideoRendering()
610{
611    if (!isReadyForVideoSetup())
612        return;
613
614    MediaRenderingMode currentMode = currentRenderingMode();
615    MediaRenderingMode preferredMode = preferredRenderingMode();
616    if (currentMode == preferredMode && currentMode != MediaRenderingNone)
617        return;
618
619    if (currentMode != MediaRenderingNone)
620        tearDownVideoRendering();
621
622    switch (preferredMode) {
623    case MediaRenderingMovieView:
624        createQTMovieView();
625        break;
626    case MediaRenderingNone:
627    case MediaRenderingSoftwareRenderer:
628        createQTVideoRenderer(QTVideoRendererModeListensForNewImages);
629        break;
630    case MediaRenderingMovieLayer:
631        createQTMovieLayer();
632        break;
633    }
634
635    // If using a movie layer, inform the client so the compositing tree is updated.
636    if (currentMode == MediaRenderingMovieLayer || preferredMode == MediaRenderingMovieLayer)
637        m_player->mediaPlayerClient()->mediaPlayerRenderingModeChanged(m_player);
638}
639
640void MediaPlayerPrivateQTKit::tearDownVideoRendering()
641{
642    if (m_qtMovieView)
643        detachQTMovieView();
644    if (m_qtVideoRenderer)
645        destroyQTVideoRenderer();
646    if (m_qtVideoLayer)
647        destroyQTMovieLayer();
648}
649
650bool MediaPlayerPrivateQTKit::hasSetUpVideoRendering() const
651{
652    return m_qtMovieView
653        || m_qtVideoLayer
654        || m_qtVideoRenderer;
655}
656
657QTTime MediaPlayerPrivateQTKit::createQTTime(float time) const
658{
659    if (!metaDataAvailable())
660        return QTMakeTime(0, 600);
661    long timeScale = [[m_qtMovie.get() attributeForKey:QTMovieTimeScaleAttribute] longValue];
662    return QTMakeTime(lroundf(time * timeScale), timeScale);
663}
664
665void MediaPlayerPrivateQTKit::resumeLoad()
666{
667    m_delayingLoad = false;
668
669    if (!m_movieURL.isNull())
670        loadInternal(m_movieURL);
671}
672
673void MediaPlayerPrivateQTKit::load(const String& url)
674{
675    m_movieURL = url;
676
677    // If the element is not supposed to load any data return immediately because QTKit
678    // doesn't have API to throttle loading.
679    if (m_preload == MediaPlayer::None) {
680        m_delayingLoad = true;
681        return;
682    }
683
684    loadInternal(url);
685}
686
687void MediaPlayerPrivateQTKit::loadInternal(const String& url)
688{
689    if (m_networkState != MediaPlayer::Loading) {
690        m_networkState = MediaPlayer::Loading;
691        m_player->networkStateChanged();
692    }
693    if (m_readyState != MediaPlayer::HaveNothing) {
694        m_readyState = MediaPlayer::HaveNothing;
695        m_player->readyStateChanged();
696    }
697    cancelSeek();
698    m_videoFrameHasDrawn = false;
699
700    [m_objcObserver.get() setDelayCallbacks:YES];
701
702#if ENABLE(OFFLINE_WEB_APPLICATIONS)
703    Frame* frame = m_player->frameView() ? m_player->frameView()->frame() : NULL;
704    ApplicationCacheHost* cacheHost = frame ? frame->loader()->documentLoader()->applicationCacheHost() : NULL;
705    ApplicationCacheResource* resource = NULL;
706    if (cacheHost && cacheHost->shouldLoadResourceFromApplicationCache(ResourceRequest(url), resource) && resource)
707        createQTMovie(resource);
708    else
709#endif
710    createQTMovie(url);
711
712    [m_objcObserver.get() loadStateChanged:nil];
713    [m_objcObserver.get() setDelayCallbacks:NO];
714}
715
716void MediaPlayerPrivateQTKit::prepareToPlay()
717{
718    if (!m_qtMovie || m_delayingLoad)
719        resumeLoad();
720}
721
722PlatformMedia MediaPlayerPrivateQTKit::platformMedia() const
723{
724    PlatformMedia pm;
725    pm.type = PlatformMedia::QTMovieType;
726    pm.media.qtMovie = m_qtMovie.get();
727    return pm;
728}
729
730#if USE(ACCELERATED_COMPOSITING)
731PlatformLayer* MediaPlayerPrivateQTKit::platformLayer() const
732{
733    return m_qtVideoLayer.get();
734}
735#endif
736
737void MediaPlayerPrivateQTKit::play()
738{
739    if (!metaDataAvailable())
740        return;
741    m_startedPlaying = true;
742#if DRAW_FRAME_RATE
743    m_frameCountWhilePlaying = 0;
744#endif
745    [m_objcObserver.get() setDelayCallbacks:YES];
746    [m_qtMovie.get() setRate:m_player->rate()];
747    [m_objcObserver.get() setDelayCallbacks:NO];
748}
749
750void MediaPlayerPrivateQTKit::pause()
751{
752    if (!metaDataAvailable())
753        return;
754    m_startedPlaying = false;
755#if DRAW_FRAME_RATE
756    m_timeStoppedPlaying = [NSDate timeIntervalSinceReferenceDate];
757#endif
758    [m_objcObserver.get() setDelayCallbacks:YES];
759    [m_qtMovie.get() stop];
760    [m_objcObserver.get() setDelayCallbacks:NO];
761}
762
763float MediaPlayerPrivateQTKit::duration() const
764{
765    if (!metaDataAvailable())
766        return 0;
767
768    if (m_cachedDuration != -1.0f)
769        return m_cachedDuration;
770
771    QTTime time = [m_qtMovie.get() duration];
772    if (time.flags == kQTTimeIsIndefinite)
773        return numeric_limits<float>::infinity();
774    return static_cast<float>(time.timeValue) / time.timeScale;
775}
776
777float MediaPlayerPrivateQTKit::currentTime() const
778{
779    if (!metaDataAvailable())
780        return 0;
781    QTTime time = [m_qtMovie.get() currentTime];
782    return static_cast<float>(time.timeValue) / time.timeScale;
783}
784
785void MediaPlayerPrivateQTKit::seek(float time)
786{
787    // Nothing to do if we are already in the middle of a seek to the same time.
788    if (time == m_seekTo)
789        return;
790
791    cancelSeek();
792
793    if (!metaDataAvailable())
794        return;
795
796    if (time > duration())
797        time = duration();
798
799    m_seekTo = time;
800    if (maxTimeSeekable() >= m_seekTo)
801        doSeek();
802    else
803        m_seekTimer.start(0, 0.5f);
804}
805
806void MediaPlayerPrivateQTKit::doSeek()
807{
808    QTTime qttime = createQTTime(m_seekTo);
809    // setCurrentTime generates several event callbacks, update afterwards
810    [m_objcObserver.get() setDelayCallbacks:YES];
811    float oldRate = [m_qtMovie.get() rate];
812
813    if (oldRate)
814        [m_qtMovie.get() setRate:0];
815    [m_qtMovie.get() setCurrentTime:qttime];
816
817    // restore playback only if not at end, otherwise QTMovie will loop
818    float timeAfterSeek = currentTime();
819    if (oldRate && timeAfterSeek < duration())
820        [m_qtMovie.get() setRate:oldRate];
821
822    cancelSeek();
823    [m_objcObserver.get() setDelayCallbacks:NO];
824}
825
826void MediaPlayerPrivateQTKit::cancelSeek()
827{
828    m_seekTo = -1;
829    m_seekTimer.stop();
830}
831
832void MediaPlayerPrivateQTKit::seekTimerFired(Timer<MediaPlayerPrivateQTKit>*)
833{
834    if (!metaDataAvailable()|| !seeking() || currentTime() == m_seekTo) {
835        cancelSeek();
836        updateStates();
837        m_player->timeChanged();
838        return;
839    }
840
841    if (maxTimeSeekable() >= m_seekTo)
842        doSeek();
843    else {
844        MediaPlayer::NetworkState state = networkState();
845        if (state == MediaPlayer::Empty || state == MediaPlayer::Loaded) {
846            cancelSeek();
847            updateStates();
848            m_player->timeChanged();
849        }
850    }
851}
852
853bool MediaPlayerPrivateQTKit::paused() const
854{
855    if (!metaDataAvailable())
856        return true;
857    return [m_qtMovie.get() rate] == 0;
858}
859
860bool MediaPlayerPrivateQTKit::seeking() const
861{
862    if (!metaDataAvailable())
863        return false;
864    return m_seekTo >= 0;
865}
866
867IntSize MediaPlayerPrivateQTKit::naturalSize() const
868{
869    if (!metaDataAvailable())
870        return IntSize();
871
872    // In spite of the name of this method, return QTMovieNaturalSizeAttribute transformed by the
873    // initial movie scale because the spec says intrinsic size is:
874    //
875    //    ... the dimensions of the resource in CSS pixels after taking into account the resource's
876    //    dimensions, aspect ratio, clean aperture, resolution, and so forth, as defined for the
877    //    format used by the resource
878
879    FloatSize naturalSize([[m_qtMovie.get() attributeForKey:QTMovieNaturalSizeAttribute] sizeValue]);
880    if (naturalSize.isEmpty() && m_isStreaming) {
881        // HTTP Live Streams will occasionally return {0,0} natural sizes while scrubbing.
882        // Work around this problem (<rdar://problem/9078563>) by returning the last valid
883        // cached natural size:
884        naturalSize = m_cachedNaturalSize;
885    } else {
886        // Unfortunately, due to another QTKit bug (<rdar://problem/9082071>) we won't get a sizeChanged
887        // event when this happens, so we must cache the last valid naturalSize here:
888        m_cachedNaturalSize = naturalSize;
889    }
890
891    return IntSize(naturalSize.width() * m_scaleFactor.width(), naturalSize.height() * m_scaleFactor.height());
892}
893
894bool MediaPlayerPrivateQTKit::hasVideo() const
895{
896    if (!metaDataAvailable())
897        return false;
898    return [[m_qtMovie.get() attributeForKey:QTMovieHasVideoAttribute] boolValue];
899}
900
901bool MediaPlayerPrivateQTKit::hasAudio() const
902{
903    if (!m_qtMovie)
904        return false;
905    return [[m_qtMovie.get() attributeForKey:QTMovieHasAudioAttribute] boolValue];
906}
907
908bool MediaPlayerPrivateQTKit::supportsFullscreen() const
909{
910#if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD)
911    return true;
912#else
913    // See <rdar://problem/7389945>
914    return false;
915#endif
916}
917
918void MediaPlayerPrivateQTKit::setVolume(float volume)
919{
920    if (m_qtMovie)
921        [m_qtMovie.get() setVolume:volume];
922}
923
924bool MediaPlayerPrivateQTKit::hasClosedCaptions() const
925{
926    if (!metaDataAvailable())
927        return false;
928    return wkQTMovieHasClosedCaptions(m_qtMovie.get());
929}
930
931void MediaPlayerPrivateQTKit::setClosedCaptionsVisible(bool closedCaptionsVisible)
932{
933    if (metaDataAvailable()) {
934        wkQTMovieSetShowClosedCaptions(m_qtMovie.get(), closedCaptionsVisible);
935
936#if USE(ACCELERATED_COMPOSITING) && (!defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD))
937    if (closedCaptionsVisible && m_qtVideoLayer) {
938        // Captions will be rendered upside down unless we flag the movie as flipped (again). See <rdar://7408440>.
939        [m_qtVideoLayer.get() setGeometryFlipped:YES];
940    }
941#endif
942    }
943}
944
945void MediaPlayerPrivateQTKit::setRate(float rate)
946{
947    if (m_qtMovie)
948        [m_qtMovie.get() setRate:rate];
949}
950
951void MediaPlayerPrivateQTKit::setPreservesPitch(bool preservesPitch)
952{
953    if (!m_qtMovie)
954        return;
955
956    // QTMovieRateChangesPreservePitchAttribute cannot be changed dynamically after QTMovie creation.
957    // If the passed in value is different than what already exists, we need to recreate the QTMovie for it to take effect.
958    if ([[m_qtMovie.get() attributeForKey:QTMovieRateChangesPreservePitchAttribute] boolValue] == preservesPitch)
959        return;
960
961    RetainPtr<NSDictionary> movieAttributes(AdoptNS, [[m_qtMovie.get() movieAttributes] mutableCopy]);
962    ASSERT(movieAttributes);
963    [movieAttributes.get() setValue:[NSNumber numberWithBool:preservesPitch] forKey:QTMovieRateChangesPreservePitchAttribute];
964    m_timeToRestore = currentTime();
965
966    createQTMovie([movieAttributes.get() valueForKey:QTMovieURLAttribute], movieAttributes.get());
967}
968
969PassRefPtr<TimeRanges> MediaPlayerPrivateQTKit::buffered() const
970{
971    RefPtr<TimeRanges> timeRanges = TimeRanges::create();
972    float loaded = maxTimeLoaded();
973    if (loaded > 0)
974        timeRanges->add(0, loaded);
975    return timeRanges.release();
976}
977
978float MediaPlayerPrivateQTKit::maxTimeSeekable() const
979{
980    if (!metaDataAvailable())
981        return 0;
982
983    // infinite duration means live stream
984    if (isinf(duration()))
985        return 0;
986
987    return wkQTMovieMaxTimeSeekable(m_qtMovie.get());
988}
989
990float MediaPlayerPrivateQTKit::maxTimeLoaded() const
991{
992    if (!metaDataAvailable())
993        return 0;
994    return wkQTMovieMaxTimeLoaded(m_qtMovie.get());
995}
996
997unsigned MediaPlayerPrivateQTKit::bytesLoaded() const
998{
999    float dur = duration();
1000    if (!dur)
1001        return 0;
1002    return totalBytes() * maxTimeLoaded() / dur;
1003}
1004
1005unsigned MediaPlayerPrivateQTKit::totalBytes() const
1006{
1007    if (!metaDataAvailable())
1008        return 0;
1009    return [[m_qtMovie.get() attributeForKey:QTMovieDataSizeAttribute] intValue];
1010}
1011
1012void MediaPlayerPrivateQTKit::cancelLoad()
1013{
1014    // FIXME: Is there a better way to check for this?
1015    if (m_networkState < MediaPlayer::Loading || m_networkState == MediaPlayer::Loaded)
1016        return;
1017
1018    tearDownVideoRendering();
1019    m_qtMovie = nil;
1020
1021    updateStates();
1022}
1023
1024void MediaPlayerPrivateQTKit::cacheMovieScale()
1025{
1026    NSSize initialSize = NSZeroSize;
1027    NSSize naturalSize = [[m_qtMovie.get() attributeForKey:QTMovieNaturalSizeAttribute] sizeValue];
1028
1029#if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD)
1030    // QTMovieCurrentSizeAttribute is not allowed with instances of QTMovie that have been
1031    // opened with QTMovieOpenForPlaybackAttribute, so ask for the display transform attribute instead.
1032    NSAffineTransform *displayTransform = [m_qtMovie.get() attributeForKey:@"QTMoviePreferredTransformAttribute"];
1033    if (displayTransform)
1034        initialSize = [displayTransform transformSize:naturalSize];
1035    else {
1036        initialSize.width = naturalSize.width;
1037        initialSize.height = naturalSize.height;
1038    }
1039#else
1040    initialSize = [[m_qtMovie.get() attributeForKey:QTMovieCurrentSizeAttribute] sizeValue];
1041#endif
1042
1043    if (naturalSize.width)
1044        m_scaleFactor.setWidth(initialSize.width / naturalSize.width);
1045    if (naturalSize.height)
1046        m_scaleFactor.setHeight(initialSize.height / naturalSize.height);
1047}
1048
1049bool MediaPlayerPrivateQTKit::isReadyForVideoSetup() const
1050{
1051    return m_readyState >= MediaPlayer::HaveMetadata && m_player->visible();
1052}
1053
1054void MediaPlayerPrivateQTKit::prepareForRendering()
1055{
1056    if (m_isAllowedToRender)
1057        return;
1058    m_isAllowedToRender = true;
1059
1060    if (!hasSetUpVideoRendering())
1061        setUpVideoRendering();
1062
1063    // If using a movie layer, inform the client so the compositing tree is updated. This is crucial if the movie
1064    // has a poster, as it will most likely not have a layer and we will now be rendering frames to the movie layer.
1065    if (currentRenderingMode() == MediaRenderingMovieLayer || preferredRenderingMode() == MediaRenderingMovieLayer)
1066        m_player->mediaPlayerClient()->mediaPlayerRenderingModeChanged(m_player);
1067}
1068
1069void MediaPlayerPrivateQTKit::updateStates()
1070{
1071    MediaPlayer::NetworkState oldNetworkState = m_networkState;
1072    MediaPlayer::ReadyState oldReadyState = m_readyState;
1073
1074    long loadState = m_qtMovie ? [[m_qtMovie.get() attributeForKey:QTMovieLoadStateAttribute] longValue] : static_cast<long>(QTMovieLoadStateError);
1075
1076    if (loadState >= QTMovieLoadStateLoaded && m_readyState < MediaPlayer::HaveMetadata) {
1077        disableUnsupportedTracks();
1078        if (m_player->inMediaDocument()) {
1079            if (!m_enabledTrackCount || m_hasUnsupportedTracks) {
1080                // This has a type of media that we do not handle directly with a <video>
1081                // element, eg. a rtsp track or QuickTime VR. Tell the MediaPlayerClient
1082                // that we noticed.
1083                sawUnsupportedTracks();
1084                return;
1085            }
1086        } else if (!m_enabledTrackCount)
1087            loadState = QTMovieLoadStateError;
1088
1089        if (loadState != QTMovieLoadStateError) {
1090            wkQTMovieSelectPreferredAlternates(m_qtMovie.get());
1091            cacheMovieScale();
1092            MediaPlayer::MovieLoadType movieType = movieLoadType();
1093            m_isStreaming = movieType == MediaPlayer::StoredStream || movieType == MediaPlayer::LiveStream;
1094        }
1095    }
1096
1097    // If this movie is reloading and we mean to restore the current time/rate, this might be the right time to do it.
1098    if (loadState >= QTMovieLoadStateLoaded && oldNetworkState < MediaPlayer::Loaded && m_timeToRestore != -1.0f) {
1099        QTTime qttime = createQTTime(m_timeToRestore);
1100        m_timeToRestore = -1.0f;
1101
1102        // Disable event callbacks from setCurrentTime for restoring time in a recreated video
1103        [m_objcObserver.get() setDelayCallbacks:YES];
1104        [m_qtMovie.get() setCurrentTime:qttime];
1105        [m_qtMovie.get() setRate:m_player->rate()];
1106        [m_objcObserver.get() setDelayCallbacks:NO];
1107    }
1108
1109    BOOL completelyLoaded = !m_isStreaming && (loadState >= QTMovieLoadStateComplete);
1110
1111    // Note: QT indicates that we are fully loaded with QTMovieLoadStateComplete.
1112    // However newer versions of QT do not, so we check maxTimeLoaded against duration.
1113    if (!completelyLoaded && !m_isStreaming && metaDataAvailable())
1114        completelyLoaded = maxTimeLoaded() == duration();
1115
1116    if (completelyLoaded) {
1117        // "Loaded" is reserved for fully buffered movies, never the case when streaming
1118        m_networkState = MediaPlayer::Loaded;
1119        m_readyState = MediaPlayer::HaveEnoughData;
1120    } else if (loadState >= QTMovieLoadStatePlaythroughOK) {
1121        m_readyState = MediaPlayer::HaveEnoughData;
1122        m_networkState = MediaPlayer::Loading;
1123    } else if (loadState >= QTMovieLoadStatePlayable) {
1124        // FIXME: This might not work correctly in streaming case, <rdar://problem/5693967>
1125        m_readyState = currentTime() < maxTimeLoaded() ? MediaPlayer::HaveFutureData : MediaPlayer::HaveCurrentData;
1126        m_networkState = MediaPlayer::Loading;
1127    } else if (loadState >= QTMovieLoadStateLoaded) {
1128        m_readyState = MediaPlayer::HaveMetadata;
1129        m_networkState = MediaPlayer::Loading;
1130    } else if (loadState > QTMovieLoadStateError) {
1131        m_readyState = MediaPlayer::HaveNothing;
1132        m_networkState = MediaPlayer::Loading;
1133    } else {
1134        // Loading or decoding failed.
1135
1136        if (m_player->inMediaDocument()) {
1137            // Something went wrong in the loading of media within a standalone file.
1138            // This can occur with chained refmovies pointing to streamed media.
1139            sawUnsupportedTracks();
1140            return;
1141        }
1142
1143        float loaded = maxTimeLoaded();
1144        if (!loaded)
1145            m_readyState = MediaPlayer::HaveNothing;
1146
1147        if (!m_enabledTrackCount)
1148            m_networkState = MediaPlayer::FormatError;
1149        else {
1150            // FIXME: We should differentiate between load/network errors and decode errors <rdar://problem/5605692>
1151            if (loaded > 0)
1152                m_networkState = MediaPlayer::DecodeError;
1153            else
1154                m_readyState = MediaPlayer::HaveNothing;
1155        }
1156    }
1157
1158    if (isReadyForVideoSetup() && !hasSetUpVideoRendering())
1159        setUpVideoRendering();
1160
1161    if (seeking())
1162        m_readyState = m_readyState >= MediaPlayer::HaveMetadata ? MediaPlayer::HaveMetadata : MediaPlayer::HaveNothing;
1163
1164    // Streaming movies don't use the network when paused.
1165    if (m_isStreaming && m_readyState >= MediaPlayer::HaveMetadata && m_networkState >= MediaPlayer::Loading && [m_qtMovie.get() rate] == 0)
1166        m_networkState = MediaPlayer::Idle;
1167
1168    if (m_networkState != oldNetworkState)
1169        m_player->networkStateChanged();
1170
1171    if (m_readyState != oldReadyState)
1172        m_player->readyStateChanged();
1173
1174    if (loadState >= QTMovieLoadStateLoaded) {
1175        float dur = duration();
1176        if (dur != m_reportedDuration) {
1177            if (m_reportedDuration != -1.0f)
1178                m_player->durationChanged();
1179            m_reportedDuration = dur;
1180        }
1181    }
1182}
1183
1184void MediaPlayerPrivateQTKit::loadStateChanged()
1185{
1186    if (!m_hasUnsupportedTracks)
1187        updateStates();
1188}
1189
1190void MediaPlayerPrivateQTKit::rateChanged()
1191{
1192    if (m_hasUnsupportedTracks)
1193        return;
1194
1195    updateStates();
1196    m_player->rateChanged();
1197}
1198
1199void MediaPlayerPrivateQTKit::sizeChanged()
1200{
1201    if (!m_hasUnsupportedTracks)
1202        m_player->sizeChanged();
1203}
1204
1205void MediaPlayerPrivateQTKit::timeChanged()
1206{
1207    if (m_hasUnsupportedTracks)
1208        return;
1209
1210    // It may not be possible to seek to a specific time in a streamed movie. When seeking in a
1211    // stream QuickTime sets the movie time to closest time possible and posts a timechanged
1212    // notification. Update m_seekTo so we can detect when the seek completes.
1213    if (m_seekTo != -1)
1214        m_seekTo = currentTime();
1215
1216    m_timeToRestore = -1.0f;
1217    updateStates();
1218    m_player->timeChanged();
1219}
1220
1221void MediaPlayerPrivateQTKit::didEnd()
1222{
1223    if (m_hasUnsupportedTracks)
1224        return;
1225
1226    m_startedPlaying = false;
1227#if DRAW_FRAME_RATE
1228    m_timeStoppedPlaying = [NSDate timeIntervalSinceReferenceDate];
1229#endif
1230
1231    // Hang onto the current time and use it as duration from now on since QuickTime is telling us we
1232    // are at the end. Do this because QuickTime sometimes reports one time for duration and stops
1233    // playback at another time, which causes problems in HTMLMediaElement. QTKit's 'ended' event
1234    // fires when playing in reverse so don't update duration when at time zero!
1235    float now = currentTime();
1236    if (now > 0)
1237        m_cachedDuration = now;
1238
1239    updateStates();
1240    m_player->timeChanged();
1241}
1242
1243void MediaPlayerPrivateQTKit::setSize(const IntSize&)
1244{
1245    // Don't resize the view now because [view setFrame] also resizes the movie itself, and because
1246    // the renderer calls this function immediately when we report a size change (QTMovieSizeDidChangeNotification)
1247    // we can get into a feedback loop observing the size change and resetting the size, and this can cause
1248    // QuickTime to miss resetting a movie's size when the media size changes (as happens with an rtsp movie
1249    // once the rtsp server sends the track sizes). Instead we remember the size passed to paint() and resize
1250    // the view when it changes.
1251    // <rdar://problem/6336092> REGRESSION: rtsp movie does not resize correctly
1252}
1253
1254void MediaPlayerPrivateQTKit::setVisible(bool b)
1255{
1256    if (m_visible != b) {
1257        m_visible = b;
1258        if (b)
1259            setUpVideoRendering();
1260        else
1261            tearDownVideoRendering();
1262    }
1263}
1264
1265bool MediaPlayerPrivateQTKit::hasAvailableVideoFrame() const
1266{
1267    // When using a QTMovieLayer return true as soon as the movie reaches QTMovieLoadStatePlayable
1268    // because although we don't *know* when the first frame has decoded, by the time we get and
1269    // process the notification a frame should have propagated the VisualContext and been set on
1270    // the layer.
1271    if (currentRenderingMode() == MediaRenderingMovieLayer)
1272        return m_readyState >= MediaPlayer::HaveCurrentData;
1273
1274    // When using the software renderer QuickTime signals that a frame is available so we might as well
1275    // wait until we know that a frame has been drawn.
1276    return m_videoFrameHasDrawn;
1277}
1278
1279void MediaPlayerPrivateQTKit::repaint()
1280{
1281    if (m_hasUnsupportedTracks)
1282        return;
1283
1284#if DRAW_FRAME_RATE
1285    if (m_startedPlaying) {
1286        m_frameCountWhilePlaying++;
1287        // to eliminate preroll costs from our calculation,
1288        // our frame rate calculation excludes the first frame drawn after playback starts
1289        if (1==m_frameCountWhilePlaying)
1290            m_timeStartedPlaying = [NSDate timeIntervalSinceReferenceDate];
1291    }
1292#endif
1293    m_videoFrameHasDrawn = true;
1294    m_player->repaint();
1295}
1296
1297void MediaPlayerPrivateQTKit::paintCurrentFrameInContext(GraphicsContext* context, const IntRect& r)
1298{
1299    id qtVideoRenderer = m_qtVideoRenderer.get();
1300    if (!qtVideoRenderer && currentRenderingMode() == MediaRenderingMovieLayer) {
1301        // We're being told to render into a context, but we already have the
1302        // MovieLayer going. This probably means we've been called from <canvas>.
1303        // Set up a QTVideoRenderer to use, but one that doesn't register for
1304        // update callbacks. That way, it won't bother us asking to repaint.
1305        createQTVideoRenderer(QTVideoRendererModeDefault);
1306        qtVideoRenderer = m_qtVideoRenderer.get();
1307    }
1308    paint(context, r);
1309}
1310
1311void MediaPlayerPrivateQTKit::paint(GraphicsContext* context, const IntRect& r)
1312{
1313    if (context->paintingDisabled() || m_hasUnsupportedTracks)
1314        return;
1315    NSView *view = m_qtMovieView.get();
1316    id qtVideoRenderer = m_qtVideoRenderer.get();
1317    if (!view && !qtVideoRenderer)
1318        return;
1319
1320    [m_objcObserver.get() setDelayCallbacks:YES];
1321    BEGIN_BLOCK_OBJC_EXCEPTIONS;
1322    context->save();
1323    context->translate(r.x(), r.y() + r.height());
1324    context->scale(FloatSize(1.0f, -1.0f));
1325    context->setImageInterpolationQuality(InterpolationLow);
1326    IntRect paintRect(IntPoint(0, 0), IntSize(r.width(), r.height()));
1327
1328#ifdef BUILDING_ON_TIGER
1329    AutodrainedPool pool;
1330#endif
1331    NSGraphicsContext* newContext = [NSGraphicsContext graphicsContextWithGraphicsPort:context->platformContext() flipped:NO];
1332
1333    // draw the current video frame
1334    if (qtVideoRenderer) {
1335        [NSGraphicsContext saveGraphicsState];
1336        [NSGraphicsContext setCurrentContext:newContext];
1337        [(id<WebKitVideoRenderingDetails>)qtVideoRenderer drawInRect:paintRect];
1338        [NSGraphicsContext restoreGraphicsState];
1339    } else {
1340        if (m_rect != r) {
1341             m_rect = r;
1342            if (m_player->inMediaDocument()) {
1343                // the QTMovieView needs to be placed in the proper location for document mode
1344                [view setFrame:m_rect];
1345            }
1346            else {
1347                // We don't really need the QTMovieView in any specific location so let's just get it out of the way
1348                // where it won't intercept events or try to bring up the context menu.
1349                IntRect farAwayButCorrectSize(m_rect);
1350                farAwayButCorrectSize.move(-1000000, -1000000);
1351                [view setFrame:farAwayButCorrectSize];
1352            }
1353        }
1354
1355        if (m_player->inMediaDocument()) {
1356            // If we're using a QTMovieView in a media document, the view may get layer-backed. AppKit won't update
1357            // the layer hosting correctly if we call displayRectIgnoringOpacity:inContext:, so use displayRectIgnoringOpacity:
1358            // in this case. See <rdar://problem/6702882>.
1359            [view displayRectIgnoringOpacity:paintRect];
1360        } else
1361            [view displayRectIgnoringOpacity:paintRect inContext:newContext];
1362    }
1363
1364#if DRAW_FRAME_RATE
1365    // Draw the frame rate only after having played more than 10 frames.
1366    if (m_frameCountWhilePlaying > 10) {
1367        Frame* frame = m_player->frameView() ? m_player->frameView()->frame() : NULL;
1368        Document* document = frame ? frame->document() : NULL;
1369        RenderObject* renderer = document ? document->renderer() : NULL;
1370        RenderStyle* styleToUse = renderer ? renderer->style() : NULL;
1371        if (styleToUse) {
1372            double frameRate = (m_frameCountWhilePlaying - 1) / ( m_startedPlaying ? ([NSDate timeIntervalSinceReferenceDate] - m_timeStartedPlaying) :
1373                (m_timeStoppedPlaying - m_timeStartedPlaying) );
1374            String text = String::format("%1.2f", frameRate);
1375            TextRun textRun(text.characters(), text.length());
1376            const Color color(255, 0, 0);
1377            context->scale(FloatSize(1.0f, -1.0f));
1378            context->setStrokeColor(color, styleToUse->colorSpace());
1379            context->setStrokeStyle(SolidStroke);
1380            context->setStrokeThickness(1.0f);
1381            context->setFillColor(color, styleToUse->colorSpace());
1382            context->drawText(styleToUse->font(), textRun, IntPoint(2, -3));
1383        }
1384    }
1385#endif
1386
1387    context->restore();
1388    END_BLOCK_OBJC_EXCEPTIONS;
1389    [m_objcObserver.get() setDelayCallbacks:NO];
1390}
1391
1392static void addFileTypesToCache(NSArray * fileTypes, HashSet<String> &cache)
1393{
1394    int count = [fileTypes count];
1395    for (int n = 0; n < count; n++) {
1396        CFStringRef ext = reinterpret_cast<CFStringRef>([fileTypes objectAtIndex:n]);
1397        RetainPtr<CFStringRef> uti(AdoptCF, UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext, NULL));
1398        if (!uti)
1399            continue;
1400        RetainPtr<CFStringRef> mime(AdoptCF, UTTypeCopyPreferredTagWithClass(uti.get(), kUTTagClassMIMEType));
1401        if (mime)
1402            cache.add(mime.get());
1403
1404        // -movieFileTypes: returns both file extensions and OSTypes. The later are surrounded by single
1405        // quotes, eg. 'MooV', so don't bother looking at those.
1406        if (CFStringGetCharacterAtIndex(ext, 0) != '\'') {
1407            // UTI is missing many media related MIME types supported by QTKit (see rdar://6434168), and not all
1408            // web servers use the MIME type UTI returns for an extension (see rdar://7875393), so even if UTI
1409            // has a type for this extension add any types in hard coded table in the MIME type regsitry.
1410            Vector<String> typesForExtension = MIMETypeRegistry::getMediaMIMETypesForExtension(ext);
1411            unsigned count = typesForExtension.size();
1412            for (unsigned ndx = 0; ndx < count; ++ndx) {
1413                if (!cache.contains(typesForExtension[ndx]))
1414                    cache.add(typesForExtension[ndx]);
1415            }
1416        }
1417    }
1418}
1419
1420static HashSet<String> mimeCommonTypesCache()
1421{
1422    DEFINE_STATIC_LOCAL(HashSet<String>, cache, ());
1423    static bool typeListInitialized = false;
1424
1425    if (!typeListInitialized) {
1426        typeListInitialized = true;
1427        NSArray* fileTypes = [QTMovie movieFileTypes:QTIncludeCommonTypes];
1428        addFileTypesToCache(fileTypes, cache);
1429    }
1430
1431    return cache;
1432}
1433
1434static HashSet<String> mimeModernTypesCache()
1435{
1436    DEFINE_STATIC_LOCAL(HashSet<String>, cache, ());
1437    static bool typeListInitialized = false;
1438
1439    if (!typeListInitialized) {
1440        typeListInitialized = true;
1441        NSArray* fileTypes = [QTMovie movieFileTypes:(QTMovieFileTypeOptions)wkQTIncludeOnlyModernMediaFileTypes()];
1442        addFileTypesToCache(fileTypes, cache);
1443    }
1444
1445    return cache;
1446}
1447
1448void MediaPlayerPrivateQTKit::getSupportedTypes(HashSet<String>& supportedTypes)
1449{
1450    supportedTypes = mimeModernTypesCache();
1451
1452    // Note: this method starts QTKitServer if it isn't already running when in 64-bit because it has to return the list
1453    // of every MIME type supported by QTKit.
1454    HashSet<String> commonTypes = mimeCommonTypesCache();
1455    HashSet<String>::const_iterator it = commonTypes.begin();
1456    HashSet<String>::const_iterator end = commonTypes.end();
1457    for (; it != end; ++it)
1458        supportedTypes.add(*it);
1459}
1460
1461MediaPlayer::SupportsType MediaPlayerPrivateQTKit::supportsType(const String& type, const String& codecs)
1462{
1463    // Only return "IsSupported" if there is no codecs parameter for now as there is no way to ask QT if it supports an
1464    // extended MIME type yet.
1465
1466    // We check the "modern" type cache first, as it doesn't require QTKitServer to start.
1467    if (mimeModernTypesCache().contains(type) || mimeCommonTypesCache().contains(type))
1468        return codecs.isEmpty() ? MediaPlayer::MayBeSupported : MediaPlayer::IsSupported;
1469
1470    return MediaPlayer::IsNotSupported;
1471}
1472
1473bool MediaPlayerPrivateQTKit::isAvailable()
1474{
1475#ifdef BUILDING_ON_TIGER
1476    SInt32 version;
1477    OSErr result;
1478    result = Gestalt(gestaltQuickTime, &version);
1479    if (result != noErr) {
1480        LOG_ERROR("No QuickTime available. Disabling <video> and <audio> support.");
1481        return false;
1482    }
1483    if (version < minimumQuickTimeVersion) {
1484        LOG_ERROR("QuickTime version %x detected, at least %x required. Disabling <video> and <audio> support.", version, minimumQuickTimeVersion);
1485        return false;
1486    }
1487    return true;
1488#else
1489    // 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.
1490    return QTKitLibrary();
1491#endif
1492}
1493
1494void MediaPlayerPrivateQTKit::disableUnsupportedTracks()
1495{
1496    if (!m_qtMovie) {
1497        m_enabledTrackCount = 0;
1498        m_totalTrackCount = 0;
1499        return;
1500    }
1501
1502    static HashSet<String>* allowedTrackTypes = 0;
1503    if (!allowedTrackTypes) {
1504        allowedTrackTypes = new HashSet<String>;
1505        allowedTrackTypes->add(QTMediaTypeVideo);
1506        allowedTrackTypes->add(QTMediaTypeSound);
1507        allowedTrackTypes->add(QTMediaTypeText);
1508        allowedTrackTypes->add(QTMediaTypeBase);
1509        allowedTrackTypes->add(QTMediaTypeMPEG);
1510        allowedTrackTypes->add("clcp"); // Closed caption
1511        allowedTrackTypes->add("sbtl"); // Subtitle
1512        allowedTrackTypes->add("odsm"); // MPEG-4 object descriptor stream
1513        allowedTrackTypes->add("sdsm"); // MPEG-4 scene description stream
1514        allowedTrackTypes->add("tmcd"); // timecode
1515        allowedTrackTypes->add("tc64"); // timcode-64
1516        allowedTrackTypes->add("tmet"); // timed metadata
1517    }
1518
1519    NSArray *tracks = [m_qtMovie.get() tracks];
1520
1521    m_totalTrackCount = [tracks count];
1522    m_enabledTrackCount = m_totalTrackCount;
1523    for (unsigned trackIndex = 0; trackIndex < m_totalTrackCount; trackIndex++) {
1524        // Grab the track at the current index. If there isn't one there, then
1525        // we can move onto the next one.
1526        QTTrack *track = [tracks objectAtIndex:trackIndex];
1527        if (!track)
1528            continue;
1529
1530        // Check to see if the track is disabled already, we should move along.
1531        // We don't need to re-disable it.
1532        if (![track isEnabled]) {
1533            --m_enabledTrackCount;
1534            continue;
1535        }
1536
1537        // Get the track's media type.
1538        NSString *mediaType = [track attributeForKey:QTTrackMediaTypeAttribute];
1539        if (!mediaType)
1540            continue;
1541
1542        // Test whether the media type is in our white list.
1543        if (!allowedTrackTypes->contains(mediaType)) {
1544            // If this track type is not allowed, then we need to disable it.
1545            [track setEnabled:NO];
1546            --m_enabledTrackCount;
1547            m_hasUnsupportedTracks = true;
1548        }
1549
1550        // Disable chapter tracks. These are most likely to lead to trouble, as
1551        // they will be composited under the video tracks, forcing QT to do extra
1552        // work.
1553        QTTrack *chapterTrack = [track performSelector:@selector(chapterlist)];
1554        if (!chapterTrack)
1555            continue;
1556
1557        // Try to grab the media for the track.
1558        QTMedia *chapterMedia = [chapterTrack media];
1559        if (!chapterMedia)
1560            continue;
1561
1562        // Grab the media type for this track.
1563        id chapterMediaType = [chapterMedia attributeForKey:QTMediaTypeAttribute];
1564        if (!chapterMediaType)
1565            continue;
1566
1567        // Check to see if the track is a video track. We don't care about
1568        // other non-video tracks.
1569        if (![chapterMediaType isEqual:QTMediaTypeVideo])
1570            continue;
1571
1572        // Check to see if the track is already disabled. If it is, we
1573        // should move along.
1574        if (![chapterTrack isEnabled])
1575            continue;
1576
1577        // Disable the evil, evil track.
1578        [chapterTrack setEnabled:NO];
1579        --m_enabledTrackCount;
1580        m_hasUnsupportedTracks = true;
1581    }
1582}
1583
1584void MediaPlayerPrivateQTKit::sawUnsupportedTracks()
1585{
1586    m_hasUnsupportedTracks = true;
1587    m_player->mediaPlayerClient()->mediaPlayerSawUnsupportedTracks(m_player);
1588}
1589
1590#if USE(ACCELERATED_COMPOSITING)
1591bool MediaPlayerPrivateQTKit::supportsAcceleratedRendering() const
1592{
1593    return isReadyForVideoSetup() && getQTMovieLayerClass() != Nil;
1594}
1595
1596void MediaPlayerPrivateQTKit::acceleratedRenderingStateChanged()
1597{
1598    // Set up or change the rendering path if necessary.
1599    setUpVideoRendering();
1600}
1601#endif
1602
1603bool MediaPlayerPrivateQTKit::hasSingleSecurityOrigin() const
1604{
1605    // We tell quicktime to disallow resources that come from different origins
1606    // so we know all media is single origin.
1607    return true;
1608}
1609
1610MediaPlayer::MovieLoadType MediaPlayerPrivateQTKit::movieLoadType() const
1611{
1612    if (!m_qtMovie)
1613        return MediaPlayer::Unknown;
1614
1615    MediaPlayer::MovieLoadType movieType = (MediaPlayer::MovieLoadType)wkQTMovieGetType(m_qtMovie.get());
1616
1617    // Can't include WebKitSystemInterface from WebCore so we can't get the enum returned
1618    // by wkQTMovieGetType, but at least verify that the value is in the valid range.
1619    ASSERT(movieType >= MediaPlayer::Unknown && movieType <= MediaPlayer::LiveStream);
1620
1621    return movieType;
1622}
1623
1624void MediaPlayerPrivateQTKit::setPreload(MediaPlayer::Preload preload)
1625{
1626    m_preload = preload;
1627    if (m_delayingLoad && m_preload != MediaPlayer::None)
1628        resumeLoad();
1629}
1630
1631float MediaPlayerPrivateQTKit::mediaTimeForTimeValue(float timeValue) const
1632{
1633    if (!metaDataAvailable())
1634        return timeValue;
1635
1636    QTTime qttime = createQTTime(timeValue);
1637    return static_cast<float>(qttime.timeValue) / qttime.timeScale;
1638}
1639
1640void MediaPlayerPrivateQTKit::setPrivateBrowsingMode(bool privateBrowsing)
1641{
1642    m_privateBrowsing = privateBrowsing;
1643    if (!m_qtMovie)
1644        return;
1645    [m_qtMovie.get() setAttribute:[NSNumber numberWithBool:!privateBrowsing] forKey:@"QTMovieAllowPersistentCacheAttribute"];
1646}
1647
1648
1649} // namespace WebCore
1650
1651@implementation WebCoreMovieObserver
1652
1653- (id)initWithCallback:(MediaPlayerPrivateQTKit*)callback
1654{
1655    m_callback = callback;
1656    return [super init];
1657}
1658
1659- (void)disconnect
1660{
1661    [NSObject cancelPreviousPerformRequestsWithTarget:self];
1662    m_callback = 0;
1663}
1664
1665-(NSMenu*)menuForEventDelegate:(NSEvent*)theEvent
1666{
1667    // Get the contextual menu from the QTMovieView's superview, the frame view
1668    return [[m_view superview] menuForEvent:theEvent];
1669}
1670
1671-(void)setView:(NSView*)view
1672{
1673    m_view = view;
1674}
1675
1676-(void)repaint
1677{
1678    if (m_delayCallbacks)
1679        [self performSelector:_cmd withObject:nil afterDelay:0.];
1680    else if (m_callback)
1681        m_callback->repaint();
1682}
1683
1684- (void)loadStateChanged:(NSNotification *)unusedNotification
1685{
1686    UNUSED_PARAM(unusedNotification);
1687    if (m_delayCallbacks)
1688        [self performSelector:_cmd withObject:nil afterDelay:0];
1689    else
1690        m_callback->loadStateChanged();
1691}
1692
1693- (void)rateChanged:(NSNotification *)unusedNotification
1694{
1695    UNUSED_PARAM(unusedNotification);
1696    if (m_delayCallbacks)
1697        [self performSelector:_cmd withObject:nil afterDelay:0];
1698    else
1699        m_callback->rateChanged();
1700}
1701
1702- (void)sizeChanged:(NSNotification *)unusedNotification
1703{
1704    UNUSED_PARAM(unusedNotification);
1705    if (m_delayCallbacks)
1706        [self performSelector:_cmd withObject:nil afterDelay:0];
1707    else
1708        m_callback->sizeChanged();
1709}
1710
1711- (void)timeChanged:(NSNotification *)unusedNotification
1712{
1713    UNUSED_PARAM(unusedNotification);
1714    if (m_delayCallbacks)
1715        [self performSelector:_cmd withObject:nil afterDelay:0];
1716    else
1717        m_callback->timeChanged();
1718}
1719
1720- (void)didEnd:(NSNotification *)unusedNotification
1721{
1722    UNUSED_PARAM(unusedNotification);
1723    if (m_delayCallbacks)
1724        [self performSelector:_cmd withObject:nil afterDelay:0];
1725    else
1726        m_callback->didEnd();
1727}
1728
1729- (void)newImageAvailable:(NSNotification *)unusedNotification
1730{
1731    UNUSED_PARAM(unusedNotification);
1732    [self repaint];
1733}
1734
1735- (void)setDelayCallbacks:(BOOL)shouldDelay
1736{
1737    m_delayCallbacks = shouldDelay;
1738}
1739
1740@end
1741
1742#endif
1743