1// Modern QTKit/Core Video plugin for OSG.
2// Eric Wing
3
4#include <osg/ImageStream>
5#include <osg/Notify>
6#include <osg/Geode>
7
8#include <osgDB/Registry>
9#include <osgDB/FileNameUtils>
10#include <osgDB/FileUtils>
11
12#import <Foundation/Foundation.h>
13#import <QTKit/QTKit.h>
14
15// Optimization to share the CoreVideo pixelbuffer with the ImageStream data avoiding memcpy's.
16// Risks are that we are more exposed to race conditions when we update the image
17// since Core Video updates happen on a background thread.
18#define SHARE_CVPIXELBUFFER 1
19
20/* Implementation notes:
21 * The first problem is that the whole OSG design is centered around pumping
22 * osg::ImageStreams through the system.
23 * But Core Video actual can give us OpenGL textures that are ready-to-go.
24 * (Core Video can also give us PBO's, but there seems to be an issue elsewhere
25 * in OSG w.r.t. PBOs on OS X.)
26 * OSG is not friendly with dealing with textures created from the outside.
27 * In the interests of getting something working (but not optimal),
28 * I use the Core Video PixelBuffer APIs to fetch data to main memory to provide osg::ImageStream
29 * with data let it upload the texture data. What a waste!
30 *
31 * The second problem is that Apple still hasn't updated their QTKit/Core Video glue API
32 * to be 64-bit ready as of Snow Leopard.
33 * This means that this plugin only works in 32-bit until Apple gets their act together.
34 * We also can't activate the Quicktime X optimized playback renderer.
35 *
36 * The third problem is that OSG makes some really bad assumptions about when the movie size (width,height)
37 * becomes available. Particularly with live streams, movie sizes are allowed to change inflight and
38 * you may not get a valid movie size until after you initially start playing the stream. OSG assumes
39 * you have the correct movie size right on open and that it will never change.
40 *
41 * The forth problem is that there are so many plugins competing for the same movie types
42 * I don't know what's going on and it seems rather rigid being a compile time things we must set.
43 *
44 * Also note for audio, this plugin completely ignores/bypasses the AudioStream class.
45 */
46
47static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef display_link,
48                               const CVTimeStamp* in_now,
49                               const CVTimeStamp* in_output_time,
50                               CVOptionFlags flags_in,
51                               CVOptionFlags* flags_out,
52                               void* user_data);
53//@class MovieNotificationHandler;
54
55namespace osgQTKit
56{
57    class QTKitImageStream;
58}
59
60@interface MovieNotificationHandler : NSObject
61{
62    osgQTKit::QTKitImageStream* imageStream;
63}
64
65- (void) setImageStream:(osgQTKit::QTKitImageStream*)image_stream;
66- (void) movieNaturalSizeDidChange:(NSNotification*)the_notification;
67- (void) movieLoadStateDidChange:(NSNotification*)the_notification;
68- (void) movieDidEnd:(NSNotification*)the_notification;
69
70@end
71
72
73namespace osgQTKit
74{
75
76class QTKitImageStream : public osg::ImageStream
77{
78    public:
79        QTKitImageStream():
80        ImageStream(),
81            displayLink(NULL),
82#if SHARE_CVPIXELBUFFER
83            currentSwapFrameIndex(0),
84#else
85            currentFrame(NULL),
86#endif
87            qtMovie(nil),
88            pixelBufferContextForQTOpenGL(NULL)
89        {
90#if SHARE_CVPIXELBUFFER
91            swapFrame[0] = NULL;
92            swapFrame[1] = NULL;
93#endif
94               setOrigin(osg::Image::TOP_LEFT);
95            initDisplayLink();
96//            Class movie_notification_class = NSClassFromString(@"MovieNotificationHandler");
97//            movieNotificationHandler = [[movie_notification_class alloc] init];
98            movieNotificationHandler = [[MovieNotificationHandler alloc] init];
99            [movieNotificationHandler setImageStream:this];
100            // for optional garbage collection dancing
101            CFRetain(movieNotificationHandler);
102            [movieNotificationHandler release];
103        }
104
105        /** Copy constructor using CopyOp to manage deep vs shallow copy. */
106        QTKitImageStream(const QTKitImageStream& image,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY):
107            ImageStream(image,copyop) {}
108
109        // Core Video requires the CGLContent and CGLPixelFormat
110        // to setup the displaylink
111
112        void initDisplayLink()
113        {
114            NSAutoreleasePool* autorelease_pool = [[NSAutoreleasePool alloc] init];
115            if (NULL != displayLink)
116            {
117                if(CVDisplayLinkIsRunning(displayLink))
118                {
119                    CVDisplayLinkStop(displayLink);
120                }
121                CVDisplayLinkRelease(displayLink);
122                displayLink = NULL;
123            }
124
125            // Because we don't have easy access to CGDisplayIDs, we create a displaylink which
126            // will work with all the active displays. This is the most flexible option, though maybe
127            // not always the fastest.
128            CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
129            if (NULL != displayLink)
130            {
131                // set the renderer output callback function
132                CVDisplayLinkSetOutputCallback(displayLink, &MyDisplayLinkCallback, this);
133            }
134
135            [autorelease_pool drain];
136        }
137
138        META_Object(osgQTKit,QTKitImageStream);
139
140        void setVolume(float the_volume)
141        {
142            [qtMovie setVolume:the_volume];
143        }
144
145        float getVolume() const
146        {
147            return [qtMovie volume];
148        }
149
150        bool open(const std::string& file_name)
151        {
152            NSAutoreleasePool* autorelease_pool = [[NSAutoreleasePool alloc] init];
153            NSString* ns_string = [NSString stringWithUTF8String:file_name.c_str()];
154            NSError* the_error = nil;
155
156            if(nil != qtMovie)
157            {
158                // Do we allow this?
159                // Don't return without cleaning up autorelease pool
160                // shutdown displaylink if this is allowed
161                //        CFRelease(objCData->qtMovie);
162                [autorelease_pool drain];
163                return false;
164            }
165
166
167            // Use QTMovieOpenForPlaybackAttribute to activate Quicktime X
168            // (Disabled because we can't use this while some APIs we use are 32-bit)
169            NSDictionary* movie_attributes =
170                [NSDictionary dictionaryWithObjectsAndKeys:
171                ns_string, QTMovieFileNameAttribute,
172//                [NSNumber numberWithBool:YES], QTMovieLoopsAttribute,
173//                [NSNumber numberWithBool:YES], QTMovieOpenForPlaybackAttribute,
174                nil];
175            qtMovie = [[QTMovie alloc] initWithAttributes:movie_attributes error:&the_error];
176            if(nil != qtMovie)
177            {
178                // For garbage collection, we need to make sure to hold the reference
179                // This code is designed to work for both modes.
180                CFRetain(qtMovie);
181                [qtMovie release];
182            }
183            else
184            {
185//                NSLog(@"Failed to open file: %@, %@", ns_string, [the_error localizedDescription]);
186                OSG_WARN<<"Failed to open file: " << file_name << std::endl;
187
188            }
189
190            [[NSNotificationCenter defaultCenter] addObserver:movieNotificationHandler
191                selector:@selector(movieNaturalSizeDidChange:)
192#if defined(MAC_OS_X_VERSION_10_6) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6)
193                name:QTMovieNaturalSizeDidChangeNotification
194#else
195                name:QTMovieSizeDidChangeNotification
196#endif
197                object:qtMovie];
198
199            [[NSNotificationCenter defaultCenter] addObserver:movieNotificationHandler
200                selector:@selector(movieLoadStateDidChange:)
201                name:QTMovieLoadStateDidChangeNotification
202                object:qtMovie];
203
204            [[NSNotificationCenter defaultCenter] addObserver:movieNotificationHandler
205                selector:@selector(movieDidEnd:)
206                name:QTMovieDidEndNotification
207                object:qtMovie];
208
209            [autorelease_pool drain];
210            return true;
211
212
213        }
214
215
216        virtual void play()
217        {
218            NSAutoreleasePool* autorelease_pool = [[NSAutoreleasePool alloc] init];
219
220            _status=PLAYING;
221            if(NULL == pixelBufferContextForQTOpenGL)
222            {
223                // This isn't guaranteed to yield a valid size.
224                // We are supposed to wait for a callback after the video stream connection has been established.
225                NSSize movie_size = [[qtMovie attributeForKey:QTMovieNaturalSizeAttribute] sizeValue];
226//                NSLog(@"movie_size=%f, %f", movie_size.width, movie_size.height);
227                NSDictionary* pixel_buffer_attributes =
228                    [NSDictionary dictionaryWithObjectsAndKeys:
229#if __BIG_ENDIAN__
230                        [NSNumber numberWithInteger:k32ARGBPixelFormat], kCVPixelBufferPixelFormatTypeKey,
231#else
232                        [NSNumber numberWithInteger:k32BGRAPixelFormat], kCVPixelBufferPixelFormatTypeKey,
233#endif
234                        // Seems that Core Video will figure out the size automatically.
235                        // Probably better that way since our values may be wrong.
236//                        [NSNumber numberWithFloat:movie_size.width], kCVPixelBufferWidthKey,
237//                        [NSNumber numberWithFloat:movie_size.height], kCVPixelBufferHeightKey,
238                        [NSNumber numberWithInteger:1], kCVPixelBufferBytesPerRowAlignmentKey,
239                        [NSNumber numberWithBool:YES], kCVPixelBufferOpenGLCompatibilityKey,
240                        nil
241                     ];
242                NSDictionary* visual_context_options =
243                    [NSDictionary dictionaryWithObjectsAndKeys:
244                        pixel_buffer_attributes, kQTVisualContextPixelBufferAttributesKey,
245                        nil
246                    ];
247
248                OSStatus the_error = QTPixelBufferContextCreate(
249                    kCFAllocatorDefault,                                        // an allocator to Create functions
250                    (CFDictionaryRef)visual_context_options,                                                        // a CF Dictionary of attributes
251                    &pixelBufferContextForQTOpenGL);
252                if(noErr != the_error)
253                {
254                    NSLog(@"Error calling QTPixelBufferContextCreate: os_status=%d, pixelBufferContextForQTOpenGL=%x", the_error, pixelBufferContextForQTOpenGL);
255
256                }
257                // Because of bad osgmovie assumptions, we need to set the size now even though we may not have correct information.
258#if SHARE_CVPIXELBUFFER
259                setImage((int)movie_size.width,(int)movie_size.height,1,
260                         GL_RGBA8,
261                         GL_BGRA,
262                         GL_UNSIGNED_INT_8_8_8_8_REV,
263                         NULL,
264                         osg::Image::NO_DELETE,
265                         1);
266#else
267                allocateImage((int)movie_size.width,(int)movie_size.height,1,GL_BGRA,GL_UNSIGNED_INT_8_8_8_8_REV,1);
268                setInternalTextureFormat(GL_RGBA8);
269#endif
270
271                SetMovieVisualContext([qtMovie quickTimeMovie], pixelBufferContextForQTOpenGL);
272
273            }
274
275
276
277            if(!CVDisplayLinkIsRunning(displayLink))
278            {
279                CVReturn err_flag = CVDisplayLinkStart(displayLink);
280                if(kCVReturnSuccess != err_flag)
281                {
282                    NSLog(@"Error CVDisplayLinkStart()");
283                }
284
285
286                [qtMovie play];
287            }
288            else
289            {
290//                NSLog(@"Alreadying playing");
291            }
292
293
294
295            [autorelease_pool drain];
296
297        }
298
299        virtual void pause()
300        {
301            NSAutoreleasePool* autorelease_pool = [[NSAutoreleasePool alloc] init];
302            _status=PAUSED;
303            if(CVDisplayLinkIsRunning(displayLink))
304            {
305                CVDisplayLinkStop(displayLink);
306                [qtMovie stop];
307            }
308            [autorelease_pool drain];
309        }
310
311        virtual void rewind()
312        {
313            NSAutoreleasePool* autorelease_pool = [[NSAutoreleasePool alloc] init];
314            _status=REWINDING; // seriously? This means that the movie will continue to be in this state until played/paused
315            [qtMovie gotoBeginning];
316            [autorelease_pool drain];
317        }
318
319        // OSG Documentation doesn't say what time is.
320        // Is it an absolute time in the movie, or an offset to move based on the current time (which can be negative)?
321        // And what are the units? seconds? milliseconds, minutes?
322        virtual void seek(double seek_time)
323        {
324
325        /*
326        http://developer.apple.com/mac/library/technotes/tn2005/tn2138.html
327          QTTime oldTime = [qtMovie currentTime];
328         QTTime incTime = QTTimeFromString( @"00:02:00.00" );
329         QTTime newTime = QTTimeIncrement( oldTime, incTime );
330
331         NSLog( QTStringFromTime( oldTime ) );
332         NSLog( QTStringFromTime( incTime ) );
333         NSLog( QTStringFromTime( newtime ) );
334         I get the following results:
335
336         0:00:00:00.00/48000
337         0:00:00:00.00/1000000
338         0:00:00:00.00/1000000
339         I have also tried setting the time string to @"0:00:02:00.00", @"0:0:2:0.0", and other variations. No luck.
340
341         What am I doing wrong?
342
343         A: You'll notice the following comment in QTTime.h:
344
345         // ,,,dd:hh:mm:ss.ff/ts
346         which translates into:
347
348         days:hours:minutes:seconds:frames/timescale
349         So you should try a string like:
350
351         QTTime incTime = QTTimeFromString( @"00:00:02:00.00/600" );
352         NSLog( QTStringFromTime( incTime ) );
353         */
354
355        }
356
357        virtual void quit()
358        {
359            close();
360        }
361
362        virtual void setTimeMultiplier(double the_rate)
363        {
364            NSAutoreleasePool* autorelease_pool = [[NSAutoreleasePool alloc] init];
365            [qtMovie setRate:the_rate];
366            [autorelease_pool drain];
367        }
368        virtual double getTimeMultiplier() const
369        {
370            NSAutoreleasePool* autorelease_pool = [[NSAutoreleasePool alloc] init];
371            float the_rate = [qtMovie rate];
372            [autorelease_pool drain];
373            return the_rate;
374        }
375
376        CVReturn handleCoreVideoCallback(
377            CVDisplayLinkRef display_link,
378            const CVTimeStamp* in_now,
379            const CVTimeStamp* in_output_time,
380            CVOptionFlags flags_in,
381            CVOptionFlags* flags_out,
382            void* user_data
383        )
384        {
385//            NSLog(@"In handleCoreVideoCallback");
386            if(NULL == pixelBufferContextForQTOpenGL)
387            {
388//                NSLog(@"pixelBufferContextForQTOpenGL is NULL");
389                return kCVReturnSuccess;
390            }
391            // CoreVideo callbacks happen on a secondary thread.
392            // So we need a new autorelease pool for this thread.
393            NSAutoreleasePool* autorelease_pool = [[NSAutoreleasePool alloc] init];
394            // check for new frame
395
396
397            /*
398             * Notes: The SHARE_CVPIXELBUFFER stuff reuses the same memory allocated by Core Video
399             * for the osg::Image data. This avoids extra memcpy's.
400             * FIXME: This probably needs locking. What is the locking model for osg::Image?
401             * Since Core Video operates on a high priority background thread, it is possible
402             * that the osg::Image could be utilized while we are updating with new frame data.
403             * Experimentally, I have not gotten any crashes, but I have noticed flickering
404             * in my original implementation where I would first set the osg::Image data to NULL
405             * before releasing the CVPixelBuffer. To avoid the flickering, I have now implemented
406             * a double-buffering technique where I immediately provide the new data and then
407             * clean up after the swap.
408             */
409            if(QTVisualContextIsNewImageAvailable(pixelBufferContextForQTOpenGL, in_output_time))
410            {
411#if SHARE_CVPIXELBUFFER
412                size_t previous_swap_frame_index = currentSwapFrameIndex;
413
414                // flip the active swap buffer
415                if(0 == currentSwapFrameIndex)
416                {
417                    currentSwapFrameIndex = 1;
418                }
419                else
420                {
421                    currentSwapFrameIndex = 0;
422                }
423
424                OSStatus error_status = QTVisualContextCopyImageForTime(pixelBufferContextForQTOpenGL, NULL, in_output_time, &swapFrame[currentSwapFrameIndex]);
425                // the above call may produce a null frame so check for this first
426                // if we have a frame, then draw it
427                if ((noErr == error_status) && (NULL != swapFrame[currentSwapFrameIndex]))
428                {
429                    size_t buffer_width = CVPixelBufferGetWidth(swapFrame[currentSwapFrameIndex]);
430                    size_t buffer_height = CVPixelBufferGetHeight(swapFrame[currentSwapFrameIndex]);
431//                                        NSLog(@"CVPixelBuffer w=%d, h=%d", buffer_width, buffer_height);
432                    //                    buffer_width = 480;
433                    //                    buffer_height = 320;
434
435#if defined(MAC_OS_X_VERSION_10_6) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6)
436                    CVPixelBufferLockBaseAddress( swapFrame[currentSwapFrameIndex], kCVPixelBufferLock_ReadOnly );
437#else
438                    CVPixelBufferLockBaseAddress( swapFrame[currentSwapFrameIndex], 0 );
439#endif
440                    void* raw_pixel_data = CVPixelBufferGetBaseAddress(swapFrame[currentSwapFrameIndex]);
441
442                    setImage(buffer_width,buffer_height,1,
443                             GL_RGBA8,
444                             GL_BGRA,
445                             GL_UNSIGNED_INT_8_8_8_8_REV,
446                             (unsigned char *)raw_pixel_data,
447                             osg::Image::NO_DELETE,
448                             1);
449                    // seems to have no effect. Flip image the hard way
450                    //                    setOrigin(osg::Image::TOP_LEFT);
451                    //    flipVertical();
452
453
454                    CVPixelBufferUnlockBaseAddress( swapFrame[currentSwapFrameIndex], 0 );
455                }
456
457                // Now clean up previous frame
458                // Release the previous frame. (This is safe to call even if it is NULL.)
459                CVPixelBufferRelease(swapFrame[previous_swap_frame_index]);
460                swapFrame[previous_swap_frame_index] = NULL;
461
462#else
463                // if we have a previous frame release it
464                if (NULL != currentFrame)
465                {
466                    CVPixelBufferRelease(currentFrame);
467                    currentFrame = NULL;
468                }
469
470                // get a "frame" (image buffer) from the Visual Context, indexed by the provided time
471                OSStatus error_status = QTVisualContextCopyImageForTime(pixelBufferContextForQTOpenGL, NULL, in_output_time, &currentFrame);
472                // the above call may produce a null frame so check for this first
473                // if we have a frame, then draw it
474                if ((noErr == error_status) && (NULL != currentFrame))
475                {
476                    size_t buffer_width = CVPixelBufferGetWidth(currentFrame);
477                    size_t buffer_height = CVPixelBufferGetHeight(currentFrame);
478                    //                    NSLog(@"CVPixelBuffer w=%d, h=%d", buffer_width, buffer_height);
479                    //                    buffer_width = 480;
480                    //                    buffer_height = 320;
481
482                    CVPixelBufferLockBaseAddress( currentFrame, kCVPixelBufferLock_ReadOnly );
483
484                    void* raw_pixel_data = CVPixelBufferGetBaseAddress(currentFrame);
485
486
487                    /*
488                     NSLog(@"CVPixelBufferGetDataSize(currentFrame)=%d", CVPixelBufferGetDataSize(currentFrame));
489                     NSLog(@"CVPixelBufferIsPlanar(currentFrame)=%d", CVPixelBufferIsPlanar(currentFrame));
490                     NSLog(@"CVPixelBufferGetBytesPerRow(currentFrame)=%d", CVPixelBufferGetBytesPerRow(currentFrame));
491                     */
492
493                    // Don't understand why CVPixelBufferGetDataSize returns a slightly bigger size
494                    // e.g. for a 480x320 movie, it is 32-bytes larger than 480*320*4
495                    //                    memcpy(data(),raw_pixel_data,CVPixelBufferGetDataSize(currentFrame));
496                    memcpy(data(),raw_pixel_data,buffer_width*buffer_height*4);
497
498                    //                    flipVertical();
499                    dirty();
500                    CVPixelBufferUnlockBaseAddress( currentFrame, 0 );
501                }
502#endif
503            } // end QTVisualContextIsNewImageAvailable()
504
505
506            [autorelease_pool drain];
507            return kCVReturnSuccess;
508        }
509
510        // TODO: OSG really needs some kind of notification callback for this so your OSG can find out that
511        // the movie size has changed.
512        void handleMovieNaturalSizeDidChange()
513        {
514//            NSSize movie_size = [[qtMovie attributeForKey:QTMovieNaturalSizeAttribute] sizeValue];
515//            NSLog(@"handleMovieNaturalSizeDidChange=%f, %f", movie_size.width, movie_size.height);
516            pause();
517            QTVisualContextRelease(pixelBufferContextForQTOpenGL);
518            pixelBufferContextForQTOpenGL = NULL;
519            play();
520        }
521
522        // Untested: I think the important case to handle is usually live streaming and there is underrun.
523        void handleMovieLoadStateDidChange()
524        {
525//            NSLog(@"handleMovieLoadStateDidChange");
526            if( (PLAYING == _status) && ([qtMovie rate] == 0.0) ) // if should be playing, but not playing
527            {
528//                NSLog(@"not playing");
529                if([[qtMovie attributeForKey:QTMovieLoadStateAttribute] longValue] >= kMovieLoadStatePlaythroughOK)
530                {
531//                    NSLog(@"handleMovieLoadStateDidChangeCallback play");
532
533                    [qtMovie play];
534                }
535            }
536            else
537            {
538//                NSLog(@"playing");
539            }
540        }
541
542        void handleMovieDidEnd()
543        {
544            pause();
545            QTVisualContextRelease(pixelBufferContextForQTOpenGL);
546            pixelBufferContextForQTOpenGL = NULL;
547            // should I rewind? What is the expected behavior?
548        }
549
550    protected:
551        CVDisplayLinkRef displayLink;
552#if SHARE_CVPIXELBUFFER
553        CVPixelBufferRef swapFrame[2];
554        size_t currentSwapFrameIndex;
555#else
556        CVPixelBufferRef currentFrame;
557#endif
558        QTMovie* qtMovie;
559        QTVisualContextRef pixelBufferContextForQTOpenGL;
560//        id movieNotificationHandler;
561        MovieNotificationHandler* movieNotificationHandler;
562
563        virtual ~QTKitImageStream()
564        {
565            close();
566            if (NULL != displayLink)
567            {
568                if(CVDisplayLinkIsRunning(displayLink))
569                {
570                    CVDisplayLinkStop(displayLink);
571                }
572                CVDisplayLinkRelease(displayLink);
573                displayLink = NULL;
574            }
575            CFRelease(movieNotificationHandler);
576            movieNotificationHandler = nil;
577        }
578
579        void close()
580        {
581            [[NSNotificationCenter defaultCenter] removeObserver:movieNotificationHandler
582                name:QTMovieLoadStateDidChangeNotification object:qtMovie];
583
584            [[NSNotificationCenter defaultCenter] removeObserver:movieNotificationHandler
585#if defined(MAC_OS_X_VERSION_10_6) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6)
586                name:QTMovieNaturalSizeDidChangeNotification
587#else
588                name:QTMovieSizeDidChangeNotification
589#endif
590
591                object:qtMovie];
592
593            [[NSNotificationCenter defaultCenter] removeObserver:movieNotificationHandler
594                name:QTMovieDidEndNotification object:qtMovie];
595
596            if(CVDisplayLinkIsRunning(displayLink))
597            {
598                CVDisplayLinkStop(displayLink);
599            }
600
601            QTVisualContextRelease(pixelBufferContextForQTOpenGL);
602            pixelBufferContextForQTOpenGL = NULL;
603#if SHARE_CVPIXELBUFFER
604            CVPixelBufferRelease(swapFrame[0]);
605            swapFrame[0] = NULL;
606            CVPixelBufferRelease(swapFrame[1]);
607            swapFrame[1] = NULL;
608            currentSwapFrameIndex = 0;
609#else
610            CVPixelBufferRelease(currentFrame);
611            currentFrame = NULL;
612#endif
613
614            if(nil != qtMovie)
615            {
616                CFRelease(qtMovie);
617                qtMovie = nil;
618            }
619        }
620        virtual void applyLoopingMode()
621        {
622            if(NO_LOOPING == _loopingMode)
623            {
624                [qtMovie setAttribute:[NSNumber numberWithBool:NO] forKey:QTMovieLoopsAttribute];
625            }
626            else
627            {
628                [qtMovie setAttribute:[NSNumber numberWithBool:YES] forKey:QTMovieLoopsAttribute];
629            }
630        }
631
632
633
634};
635
636
637}
638
639static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef display_link,
640                               const CVTimeStamp* in_now,
641                               const CVTimeStamp* in_output_time,
642                               CVOptionFlags flags_in,
643                               CVOptionFlags* flags_out,
644                               void* user_data)
645{
646    if(NULL != user_data)
647    {
648        osgQTKit::QTKitImageStream* qtkit_image_stream = reinterpret_cast<osgQTKit::QTKitImageStream*>(user_data);
649        return qtkit_image_stream->handleCoreVideoCallback(display_link, in_now, in_output_time, flags_in, flags_out, NULL);
650    }
651    return kCVReturnSuccess;
652}
653
654class ReaderWriterQTKit : public osgDB::ReaderWriter
655{
656    public:
657
658        ReaderWriterQTKit()
659        {
660            supportsExtension("mov","Quicktime movie format");
661            supportsExtension("mpg","Mpeg movie format");
662            supportsExtension("mp4","Mpeg movie format");
663            supportsExtension("mpv","Mpeg movie format");
664            supportsExtension("mpeg","Mpeg movie format");
665
666            // only with Perian
667            supportsExtension("avi","");
668            supportsExtension("xvid","");
669            // only with Flip4Mac
670            supportsExtension("wmv","");
671
672        }
673        virtual bool acceptsExtension(const std::string& extension) const
674        {
675            return
676                osgDB::equalCaseInsensitive(extension,"mov") ||
677                osgDB::equalCaseInsensitive(extension,"mpg") ||
678                osgDB::equalCaseInsensitive(extension,"mp4") ||
679                osgDB::equalCaseInsensitive(extension,"mpv") ||
680                osgDB::equalCaseInsensitive(extension,"mpeg") ||
681                osgDB::equalCaseInsensitive(extension,"avi") ||
682                osgDB::equalCaseInsensitive(extension,"xvid") ||
683                osgDB::equalCaseInsensitive(extension,"wmv");
684
685        }
686
687        virtual ~ReaderWriterQTKit()
688        {
689            OSG_INFO<<"~ReaderWriterQTKit()"<<std::endl;
690        }
691
692        virtual const char* className() const { return "QTKit ImageStream Reader"; }
693
694        virtual ReadResult readImage(const std::string& file, const osgDB::ReaderWriter::Options* options) const
695        {
696            std::string ext = osgDB::getLowerCaseFileExtension(file);
697            if (!acceptsExtension(ext)) return ReadResult::FILE_NOT_HANDLED;
698
699            std::string fileName;
700            if (ext=="QTKit")
701            {
702                fileName = osgDB::findDataFile( osgDB::getNameLessExtension(file), options);
703                OSG_INFO<<"QTKit stipped filename = "<<fileName<<std::endl;
704            }
705            else
706            {
707                fileName = osgDB::findDataFile( file, options );
708                if (fileName.empty()) return ReadResult::FILE_NOT_FOUND;
709            }
710
711            OSG_INFO<<"ReaderWriterQTKit::readImage "<< file<< std::endl;
712
713            osg::ref_ptr<osgQTKit::QTKitImageStream> imageStream = new osgQTKit::QTKitImageStream();
714
715            if (!imageStream->open(fileName)) return ReadResult::FILE_NOT_HANDLED;
716
717            return imageStream.release();
718        }
719
720    protected:
721
722
723};
724
725
726@implementation MovieNotificationHandler
727
728- (void) setImageStream:(osgQTKit::QTKitImageStream*)image_stream
729{
730    imageStream = image_stream;
731}
732
733// I need to verify if this is being called back on a non-main thread.
734// My initial observation led me to think it was being called on a background thread,
735// but it could be that I was just confused which thread was the main thread since
736// CoreVideo was doing a bunch of stuff on its own background thread.
737- (void) movieNaturalSizeDidChange:(NSNotification*)the_notification
738{
739    imageStream->handleMovieNaturalSizeDidChange();
740}
741
742- (void) movieLoadStateDidChange:(NSNotification*)the_notification
743{
744    imageStream->handleMovieLoadStateDidChange();
745}
746
747- (void) movieDidEnd:(NSNotification*)the_notification
748{
749    imageStream->handleMovieDidEnd();
750}
751@end
752
753
754// now register with Registry to instantiate the above
755// reader/writer.
756REGISTER_OSGPLUGIN(QTKit, ReaderWriterQTKit)
757