1 /*
   2  * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 #import "QTKMediaPlayer.h"
  27 #import <Utils/MTObjectProxy.h>
  28 #import <jni/Logger.h>
  29 #import "CVVideoFrame.h"
  30 #import <PipelineManagement/NullAudioEqualizer.h>
  31 #import <PipelineManagement/NullAudioSpectrum.h>
  32 
  33 #import <limits.h>
  34 
  35 #define DUMP_TRACK_INFO 0
  36 
  37 // this is annoying... all because we had to have a STOPPED state...
  38 #define kPlaybackState_Stop 0
  39 #define kPlaybackState_Play 1
  40 #define kPlaybackState_Pause 2
  41 #define kPlaybackState_Finished 3
  42 
  43 // Non-public selectors for QTMovie
  44 // WARNING: These aren't guaranteed to be there, you must check
  45 // the movie object with respondsToSelector: first!
  46 @interface QTMovie(HiddenStuff)
  47 
  48 - (void) setAudioDevice:(id)device error:(NSError**)err;
  49 
  50 - (float) balance;
  51 - (void) setBalance:(float)b;
  52 
  53 - (BOOL) isBuffering;
  54 - (BOOL) hasEqualizer;
  55 
  56 - (NSSet*) imageConsumers;
  57 - (void) removeImageConsumer:(id)consumer flush:(BOOL)flush;
  58 - (void) addImageConsumer:(id)consumer;
  59 
  60 - (NSArray *) availableRanges;
  61 - (NSArray *) loadedRanges;
  62 
  63 @end
  64 
  65 @interface QTTrack(HiddenStuff)
  66 
  67 - (NSString*) channels; // ex: "Stereo (L R)"
  68 - (int) audioChannelCount;
  69 - (float) floatFrameRate;
  70 - (float) audioSampleRate;
  71 - (NSString*) codecName; // ex: "H.264"
  72 - (NSString*) isoLanguageCodeAsString;
  73 
  74 @end
  75 
  76 @interface QTKMediaPlayer(PrivateStuff)
  77 
  78 - (void) sendVideoFrame:(CVPixelBufferRef)pixelBuffer hostTime:(uint64_t)hostTime;
  79 
  80 @end
  81 
  82 
  83 @interface ImageConsumerProxy : NSObject
  84 {
  85     QTKMediaPlayer *player;
  86 }
  87 
  88 @end
  89 
  90 @implementation ImageConsumerProxy
  91 
  92 - (id) initWithPlayer:(QTKMediaPlayer*)inPlayer
  93 {
  94     if ((self = [super init]) != nil) {
  95         // don't retain the player or we'll cause a retain loop
  96         player = inPlayer;
  97     }
  98     return self;
  99 }
 100 
 101 - (void) dealloc
 102 {
 103     [super dealloc];
 104 }
 105 
 106 - (NSDictionary *) preferredAttributes
 107 {
 108     NSDictionary *pba = [NSDictionary dictionaryWithObjectsAndKeys:
 109                          [NSNumber numberWithBool:YES], @"IOSurfaceCoreAnimationCompatibility", // doesn't seem necessary
 110                          [NSArray arrayWithObjects:
 111                          [NSNumber numberWithLong:k2vuyPixelFormat],
 112                           nil], @"PixelFormatType",
 113                          nil];
 114     
 115     NSDictionary *attr = [NSDictionary dictionaryWithObjectsAndKeys:
 116                           [NSColorSpace genericRGBColorSpace], @"colorspace",
 117                           pba, @"pixelBufferAttributes",
 118                           nil];
 119     return attr;
 120 }
 121 
 122 - (void) flushImageBuffersAfterHostTime:(unsigned long long)hostTime
 123 {
 124     // FIXME: Can't do anything? All the frames are pushed up...
 125 }
 126 
 127 - (void) setImageBuffer:(CVBufferRef)buf forHostTime:(unsigned long long)hostTime
 128 {
 129     [player sendVideoFrame:buf hostTime:hostTime];
 130 }
 131 
 132 @end
 133 
 134 
 135 @implementation QTKMediaPlayer
 136 
 137 - (id) initWithURL:(NSURL *)source eventHandler:(CJavaPlayerEventDispatcher*)hdlr
 138 {
 139     if ((self = [self init]) != nil) {
 140         movieURL = [source retain];
 141         
 142         movie = nil;
 143         movieReady = NO;
 144         
 145         frameHandler = [[ImageConsumerProxy alloc] initWithPlayer:self];
 146         
 147         notificationCookies = [[NSMutableSet alloc] init];
 148         
 149         isLiveStream = NO; // we'll determine later
 150         
 151         audioSyncDelay = 0;
 152         requestedRate = 1.0;
 153         updateHostTimeBase = NO;
 154         currentTime = 0.0;
 155         suppressDurationEvents = NO;
 156         mute = NO;
 157         volume = 1.0;
 158         balance = 0.0;
 159         
 160         eventHandler = hdlr;
 161         
 162         previousWidth = -1;
 163         previousHeight = -1;
 164         
 165         previousPlayerState = kPlayerState_UNKNOWN;
 166         
 167         requestedState = kPlaybackState_Stop;
 168         
 169         isDisposed = NO;
 170 
 171         _audioEqualizer = new CNullAudioEqualizer();
 172         _audioSpectrum = new CNullAudioSpectrum();
 173 
 174         // create the movie on the main thread, but don't wait for it to happen
 175         if (![NSThread isMainThread]) {
 176             [self performSelectorOnMainThread:@selector(createMovie) withObject:nil waitUntilDone:NO];
 177         } else {
 178             [self createMovie];
 179         }
 180     }
 181     return self;
 182 }
 183 
 184 - (void) dealloc
 185 {
 186     [self dispose]; // just in case
 187     
 188     [frameHandler release];
 189     frameHandler = nil;
 190     
 191     [movieURL release];
 192     
 193     if (_audioEqualizer) {
 194         delete _audioEqualizer;
 195     }
 196 
 197     if (_audioSpectrum) {
 198         delete _audioSpectrum;
 199     }
 200 
 201     [super dealloc];
 202 }
 203 
 204 - (CAudioEqualizer*) audioEqualizer
 205 {
 206     return _audioEqualizer;
 207 }
 208 
 209 - (CAudioSpectrum*) audioSpectrum
 210 {
 211     return _audioSpectrum;
 212 }
 213 
 214 - (void) dispose
 215 {
 216     @synchronized(self) {
 217         [movie invalidate];
 218         if (frameHandler) {
 219             // remove the image consumer
 220             [movie removeImageConsumer:frameHandler flush:NO];
 221         }
 222         [movie release];
 223         movie = nil;
 224         
 225         if (notificationCookies) {
 226             NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
 227             for (id cookie in notificationCookies) {
 228                 [center removeObserver:cookie];
 229             }
 230             [notificationCookies release];
 231             notificationCookies = nil;
 232         }
 233         
 234         // eventHandler is cleaned up separately, just drop the reference
 235         eventHandler = NULL;
 236         
 237         isDisposed = YES;
 238     }
 239 }
 240 
 241 - (void) registerForNotification:(NSString*)name object:(id)object withBlock:(void (^)(NSNotification*))block
 242 {
 243     id cookie = [[NSNotificationCenter defaultCenter]
 244                  addObserverForName:name
 245                  object:object
 246                  queue:nil
 247                  usingBlock:block];
 248     
 249     if (cookie) {
 250         [notificationCookies addObject:cookie];
 251     }
 252 }
 253 
 254 - (void) createMovie
 255 {
 256     @synchronized(self) {
 257         if (isDisposed) {
 258             return;
 259         }
 260         
 261         if (![NSThread isMainThread]) {
 262             LOGGER_ERRORMSG_CM("QTKMediaPlayer", "createMovie", "was NOT called on the main app thread!\n");
 263             if (eventHandler) {
 264                 eventHandler->SendPlayerMediaErrorEvent(ERROR_OSX_INIT);
 265             }
 266             return;
 267         }
 268         
 269         NSError *err = nil;
 270         QTMovie *qtMovie =
 271         [QTMovie movieWithAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
 272                                       movieURL, QTMovieURLAttribute,
 273                                       [NSNumber numberWithBool:YES], QTMovieOpenForPlaybackAttribute,
 274                                       [NSNumber numberWithBool:YES], QTMovieOpenAsyncOKAttribute,
 275                                       //                                  [NSNumber numberWithBool:NO], QTMovieAskUnresolvedDataRefsAttribute,
 276                                       [NSNumber numberWithBool:YES], QTMovieDontInteractWithUserAttribute,
 277                                       nil]
 278                                error:&err];
 279         if (err || !qtMovie) {
 280             LOGGER_ERRORMSG_CM("QTKMediaPlayer", "createMovie", ([[NSString stringWithFormat:@"Error creating QTMovie: %@\n", err] UTF8String]));
 281             if (eventHandler) {
 282                 eventHandler->SendPlayerMediaErrorEvent(ERROR_OSX_INIT);
 283             }
 284             qtMovie = nil;
 285         }
 286         
 287         /*
 288          *******************************************************************************
 289          BIG FAT WARNING!!!!!!!!!!!!!
 290          *******************************************************************************
 291          
 292          Do NOT reference "self" inside a block registered with the
 293          Notification Center or you will create a retain loop and prevent this
 294          object from ever releasing. Instead, use the stack variable "blockSelf"
 295          defined below, this will prevent the retain loop.
 296          
 297          *******************************************************************************
 298          */
 299         
 300         __block __typeof__(self) blockSelf = self;
 301         [self registerForNotification:QTMovieDidEndNotification
 302                                object:qtMovie
 303                             withBlock:
 304          ^(NSNotification*note) {
 305              [blockSelf finish];
 306          }];
 307         
 308         [self registerForNotification:QTMovieLoadStateDidChangeNotification
 309                                object:qtMovie
 310                             withBlock:
 311          ^(NSNotification *note) {
 312              /*
 313               * QTMovieLoadStateError - an error occurred while loading the movie
 314               * QTMovieLoadStateLoading - the movie is loading
 315               * QTMovieLoadStateLoaded - the movie atom has loaded; it's safe to query movie properties
 316               * QTMovieLoadStatePlayable - the movie has loaded enough media data to begin playing
 317               * QTMovieLoadStatePlaythroughOK - the movie has loaded enough media data to play through to the end
 318               * QTMovieLoadStateComplete - the movie has loaded completely
 319               */
 320              long loadState = [(NSNumber*)[movie attributeForKey:QTMovieLoadStateAttribute] longValue];
 321              NSError *loadError = (NSError*)[movie attributeForKey:QTMovieLoadStateErrorAttribute];
 322              if (loadError) {
 323                  LOGGER_ERRORMSG(([[NSString stringWithFormat:@"Error loading QTMovie: %@\n", loadError] UTF8String]));
 324                  if (eventHandler) {
 325                      if (loadError.domain == NSOSStatusErrorDomain) {
 326                          eventHandler->SendPlayerMediaErrorEvent(ERROR_LOCATOR_CONNECTION_LOST);
 327                      } else {
 328                          eventHandler->SendPlayerMediaErrorEvent(ERROR_OSX_INIT);
 329                      }
 330                  }
 331              }
 332              
 333              if (!movieReady) {
 334                  if (loadState > QTMovieLoadStateLoaded) {
 335                      [blockSelf setMovieReady];
 336                  }
 337              } else if (requestedState == kPlaybackState_Play) {
 338                  // if state is QTMovieLoadStatePlayable then we've stalled
 339                  // if state is QTMovieLoadStatePlaythroughOK then we're playing
 340                  if (loadState == QTMovieLoadStatePlayable && previousPlayerState == kPlayerState_PLAYING) {
 341                      [blockSelf setPlayerState:kPlayerState_STALLED];
 342                  } else if (loadState == QTMovieLoadStatePlaythroughOK) {
 343                      [blockSelf setPlayerState:kPlayerState_PLAYING];
 344                  }
 345              }
 346          }];
 347         
 348         [self registerForNotification:QTMovieTimeDidChangeNotification
 349                                object:qtMovie
 350                             withBlock:
 351          ^(NSNotification *note) {
 352              // grab currentTime and current host time and set our host time base accordingly
 353              double now = blockSelf.currentTime;
 354              uint64_t hostTime = CVGetCurrentHostTime();
 355              hostTimeFreq = CVGetHostClockFrequency();
 356              uint64_t nowDelta = (uint64_t)(now * hostTimeFreq); // current time in host frequency units
 357              hostTimeBase = hostTime - nowDelta; // Host time at movie time zero
 358              LOGGER_DEBUGMSG(([[NSString stringWithFormat:@"Movie time changed %lf", currentTime] UTF8String]));
 359              
 360              // http://javafx-jira.kenai.com/browse/RT-27041
 361              // TODO: flush video buffers
 362          }];
 363         
 364         [self registerForNotification:QTMovieRateDidChangeNotification
 365                                object:qtMovie
 366                             withBlock:
 367          ^(NSNotification *note) {
 368              NSNumber *newRate = [note.userInfo objectForKey:QTMovieRateDidChangeNotificationParameter];
 369              [blockSelf rateChanged:newRate.floatValue];
 370          }];
 371         
 372         // QTMovieNaturalSizeDidChangeNotification is unreliable, especially with HTTP live streaming
 373         // so just use the pixel buffer sizes to send frame size changed events
 374         
 375         // QTMovieAvailableRangesDidChangeNotification
 376         [self registerForNotification:@"QTMovieAvailableRangesDidChangeNotification"
 377                                object:qtMovie
 378                             withBlock:
 379          ^(NSNotification *note) {
 380              NSArray *ranges = nil;
 381              if ([movie respondsToSelector:@selector(availableRanges)]) {
 382                  ranges = [movie performSelector:@selector(availableRanges)];
 383              }
 384              if (!suppressDurationEvents && ranges) {
 385                  for (NSValue *rangeVal in ranges) {
 386                      QTTimeRange timeRange = [rangeVal QTTimeRangeValue]; // .time, .duration
 387                      // if duration is indefinite then it's a live stream and we need to report as such
 388                      if (QTTimeIsIndefinite(timeRange.duration)) {
 389                          eventHandler->SendDurationUpdateEvent(INFINITY);
 390                          // and suppress all other subsequent events
 391                          suppressDurationEvents = YES;
 392                          isLiveStream = YES;
 393                          break;
 394                      }
 395                  }
 396              }
 397          }];
 398         
 399         // QTMovieLoadedRangesDidChangeNotification
 400         [self registerForNotification:@"QTMovieLoadedRangesDidChangeNotification"
 401                                object:qtMovie
 402                             withBlock:
 403          ^(NSNotification *note) {
 404              NSArray *ranges = nil;
 405              if ([movie respondsToSelector:@selector(loadedRanges)]) {
 406                  ranges = [movie performSelector:@selector(loadedRanges)];
 407              }
 408              // don't emit progress events for live streams
 409              if (!suppressDurationEvents && ranges) {
 410                  int64_t total = 0;
 411                  for (NSValue *rangeVal in ranges) {
 412                      QTTimeRange timeRange = [rangeVal QTTimeRangeValue]; // .time, .duration
 413                      NSTimeInterval duration;
 414                      QTGetTimeInterval(timeRange.duration, &duration);
 415                      
 416                      total += (int64_t)(duration * 1000);
 417                  }
 418                  // send buffer progress event
 419                  double movieDur = blockSelf.duration;
 420                  eventHandler->SendBufferProgressEvent(movieDur, 0, (int64_t)(movieDur * 1000), total);
 421              }
 422          }];
 423         
 424 #if 0
 425         // show all notifications, use to find possibly missed notifications
 426         [[NSNotificationCenter defaultCenter]
 427          addObserverForName:nil
 428          object:qtMovie
 429          queue:nil
 430          usingBlock:^(NSNotification *note) {
 431              NSLog(@"Movie notification: %@", note.name);
 432          }
 433          ];
 434 #endif
 435         
 436 #if 0
 437         // Template notification block, remember to use blockSelf instead of self
 438         [[NSNotificationCenter defaultCenter]
 439          addObserverForName:QTMovieXXX
 440          object:qtMovie
 441          queue:nil
 442          usingBlock:
 443          ^(NSNotification *note) {
 444              
 445          }
 446          ];
 447 #endif
 448         // http://javafx-jira.kenai.com/browse/RT-27041
 449         // TODO: test for addImageConsumer first, fall back on CARenderer hack if it's not available
 450         [qtMovie addImageConsumer:frameHandler];
 451         
 452         movie = (QTMovie*)[[MTObjectProxy objectProxyWithTarget:qtMovie] retain];
 453     }
 454 }
 455 
 456 
 457 - (void) play
 458 {
 459     requestedState = kPlaybackState_Play;
 460     if (movie && movieReady) {
 461         [movie play];
 462         [movie setRate:requestedRate];
 463     }
 464 }
 465 
 466 - (void) pause
 467 {
 468     if (requestedState == kPlaybackState_Stop) {
 469         requestedState = kPlaybackState_Pause;
 470         [self setPlayerState:kPlayerState_PAUSED];
 471     } else {
 472         requestedState = kPlaybackState_Pause;
 473         if (movie && movieReady) {
 474             [movie stop];
 475         }
 476         
 477         if (previousPlayerState == kPlayerState_STALLED) {
 478             [self setPlayerState:kPlayerState_PAUSED];
 479         }
 480     }
 481 }
 482 
 483 - (void) finish
 484 {
 485     requestedState = kPlaybackState_Finished;
 486     [self setPlayerState:kPlayerState_FINISHED];
 487     if (movie && movieReady) {
 488         [movie stop];
 489     }
 490 }
 491 
 492 - (void) stop
 493 {
 494     if (requestedState == kPlaybackState_Finished || requestedState == kPlaybackState_Pause) {
 495         requestedState = kPlaybackState_Stop;
 496         [self setPlayerState:kPlayerState_STOPPED];
 497     } else {
 498         requestedState = kPlaybackState_Stop;
 499         if (movie && movieReady) {
 500             // we need to just nuke the "STOPPED" state...
 501             [movie stop];
 502         } else {
 503             currentTime = 0.0;
 504         }
 505         
 506         if (previousPlayerState == kPlayerState_STALLED) {
 507             [self setPlayerState:kPlayerState_STOPPED];
 508         }
 509     }
 510 }
 511 
 512 
 513 - (void) rateChanged:(float)newRate
 514 {
 515     /*
 516      * Relevant PlayerState values:
 517      *      PLAYING - rate != 0
 518      *      PAUSED  - reqRate == 0, rate == 0
 519      *      STOPPED - stopFlag && reqRate == 0, rate == 0
 520      *      STALLED - detected by load state or reqRate != 0, rate == 0 and state is PLAYING
 521      */
 522     if (newRate == 0.0) {
 523         // slop for FP/timescale error
 524         if (requestedState == kPlaybackState_Stop) {
 525             [self setPlayerState:kPlayerState_STOPPED];
 526         } else if (requestedState == kPlaybackState_Play && previousPlayerState == kPlayerState_PLAYING && requestedRate != 0.0) {
 527             [self setPlayerState:kPlayerState_STALLED];        
 528         } else if (requestedState != kPlaybackState_Finished) {
 529             [self setPlayerState:kPlayerState_PAUSED];
 530         }
 531         
 532     } else {
 533         // non-zero is always playing
 534         [self setPlayerState:kPlayerState_PLAYING];
 535     }
 536 }
 537 
 538 @synthesize audioSyncDelay;
 539 
 540 - (BOOL) mute
 541 {
 542     if (movie && movieReady) {
 543         mute = movie.muted;
 544         return mute;
 545     }
 546     return mute;
 547 }
 548 
 549 - (void) setMute:(BOOL)state
 550 {
 551     mute = state;
 552     if (movie && movieReady) {
 553         movie.muted = state;
 554     }
 555 }
 556 
 557 - (float) volume
 558 {
 559     if (movie && movieReady) {
 560         volume = movie.volume;
 561     }
 562     return volume;
 563 }
 564 
 565 - (void) setVolume:(float)newVolume
 566 {
 567     volume = newVolume;
 568     if (movie && movieReady) {
 569         movie.volume = (float)volume;
 570     }
 571 }
 572 
 573 - (float) balance
 574 {
 575     if (movie && movieReady) {
 576         if ([movie respondsToSelector:@selector(balance)]) {
 577             balance = [movie balance];
 578         } else if (eventHandler) {
 579             eventHandler->Warning(WARNING_JFXMEDIA_BALANCE, NULL);
 580         }
 581     }
 582     return balance;
 583 }
 584 
 585 - (void) setBalance:(float)newBalance
 586 {
 587     balance = newBalance;
 588     if (movie && movieReady) {
 589         if ([movie respondsToSelector:@selector(setBalance:)]) {
 590             [movie setBalance:balance];
 591         } else if (eventHandler) {
 592             eventHandler->Warning(WARNING_JFXMEDIA_BALANCE, NULL);
 593         }
 594     }
 595 }
 596 
 597 - (double) duration
 598 {
 599     if (movie && movieReady) {
 600         NSNumber *hasDuration = [movie attributeForKey:QTMovieHasDurationAttribute];
 601         if (hasDuration.boolValue) {
 602             QTTime movieDur = movie.duration;
 603             NSTimeInterval duration;
 604             if (QTGetTimeInterval(movieDur, &duration)) {
 605                 return duration;
 606             }
 607         }
 608     }
 609     return -1.0; // hack value for UNKNOWN, since duration must be >= 0
 610 }
 611 
 612 - (float) rate
 613 {
 614     return requestedRate;
 615 }
 616 
 617 - (void) setRate:(float)newRate
 618 {
 619     if (isLiveStream) {
 620         LOGGER_WARNMSG("Cannot set playback rate on LIVE stream!");
 621         return;
 622     }
 623 
 624     requestedRate = newRate;
 625     if (movie && movieReady && requestedState == kPlaybackState_Play) {
 626         [movie setRate:requestedRate];
 627     }
 628 }
 629 
 630 - (double) currentTime
 631 {
 632     if (movie && movieReady) {
 633         QTTime time = movie.currentTime;
 634         NSTimeInterval timeIval;
 635         if (QTGetTimeInterval(time, &timeIval)) {
 636             currentTime = timeIval;
 637         }
 638     }
 639     return currentTime;
 640 }
 641 
 642 - (void) setCurrentTime:(double)newTime
 643 {
 644     if (isLiveStream) {
 645         LOGGER_WARNMSG("Cannot seek LIVE stream!");
 646         return;
 647     }
 648     
 649     currentTime = newTime;
 650     
 651     if (movie && movieReady) {
 652         movie.currentTime = QTMakeTimeWithTimeInterval(newTime);
 653         
 654         // make sure we're playing if requested
 655         if (requestedState == kPlaybackState_Play) {
 656             [movie play];
 657             [movie setRate:requestedRate];
 658         } else if (requestedState == kPlaybackState_Finished) {
 659             requestedState = kPlaybackState_Play;
 660             [movie play];
 661             [movie setRate:requestedRate];
 662         }
 663     }
 664 }
 665 
 666 - (void) setPlayerState:(int)newState
 667 {
 668     if (newState != previousPlayerState) {
 669         if (newState == kPlayerState_PLAYING) {
 670             updateHostTimeBase = YES;
 671         }
 672         // For now just send up to client
 673         eventHandler->SendPlayerStateEvent(newState, 0.0);
 674         previousPlayerState = newState;
 675     }
 676 }
 677 
 678 #if DUMP_TRACK_INFO
 679 static void append_log(NSMutableString *s, NSString *fmt, ...) {
 680     va_list args;
 681     va_start(args, fmt);
 682     NSString *appString = [[NSString alloc] initWithFormat:fmt arguments:args];
 683     [s appendFormat:@"%@\n", appString];
 684     va_end(args);
 685     [appString release];
 686 }
 687 #define TRACK_LOG(fmt, ...) append_log(trackLog, fmt, ##__VA_ARGS__)
 688 #else
 689 #define TRACK_LOG(...) {}
 690 #endif
 691 
 692 - (void) parseMovieTracks
 693 {
 694 #if DUMP_TRACK_INFO
 695     NSMutableString *trackLog = [[NSMutableString alloc] initWithFormat:@"Parsing tracks for movie %@:\n", movie];
 696 #endif    
 697     /*
 698      * Track properties we care about at the FX level:
 699      *
 700      * track:
 701      *   + trackEnabled (boolean)
 702      *   + trackID (jlong)
 703      *   + name (string)
 704      *   + locale (Locale) - language is derived from Locale or null
 705      *   + language (3 char iso code)
 706      * video track:
 707      *   + width (int)
 708      *   + height (int)
 709      * audio track: (no additional properties)
 710      *
 711      *
 712      * Track properties at the com.sun level:
 713      *
 714      * track:
 715      *   X trackEnabled (boolean) == QTTrackEnabledAttribute
 716      *   X trackID (long) == QTTrackIDAttribute
 717      *   X name (string) == QTTrackDisplayNameAttribute
 718      *   X encoding (enum) == non-public selector: - (NSString*) codecName
 719      * video track: (QTTrackMediaTypeAttribute == 'vide')
 720      *   X frame size (w,h) == QTTrackDimensionsAttribute (NSValue:NSSize)
 721      *   X frame rate
 722      *   X hasAlpha (boolean) == false (for now)
 723      * audio track: (QTTrackMediaTypeAttribute == 'soun')
 724      *   X language == non-public selector: - (NSString*) isoLanguageCodeAsString
 725      *   X channels (int) == non-public selector: - (int) audioChannelCount
 726      *   X channel mask (int) == parsed from channel count (or ASBD)
 727      *   X sample rate (float) == non-public selector: - (float) audioSampleRate
 728      *
 729      * just create CVideoTrack or CAudioTrack and send them up with eventHandler and we're good
 730      */
 731     NSArray *tracks = movie.tracks;
 732     if (tracks) {
 733         // get video tracks
 734         NSArray *tracks = [movie tracksOfMediaType:QTMediaTypeVideo];
 735         for (QTTrack *track in tracks) {
 736             long trackID = [[track attributeForKey:QTTrackIDAttribute] longValue];
 737             BOOL trackEnabled = [[track attributeForKey:QTTrackEnabledAttribute] boolValue];
 738             NSSize videoSize = [[track attributeForKey:QTTrackDimensionsAttribute] sizeValue];
 739             QTMedia *trackMedia;
 740             float frameRate = 29.97; // default
 741             NSString *codecName = nil;
 742             
 743             TRACK_LOG(@"Video QTTrack: %@", track);
 744             TRACK_LOG(@" - id %ld (%sabled)", trackID, trackEnabled ? "en" : "dis");
 745 
 746             CTrack::Encoding encoding = CTrack::CUSTOM;
 747             if ([track respondsToSelector:@selector(codecName)]) {
 748                 codecName = [[track codecName] lowercaseString];
 749                 if ([codecName hasPrefix:@"h.264"] || [codecName hasPrefix:@"avc"]) {
 750                     encoding = CTrack::H264;
 751                 }
 752             }
 753             TRACK_LOG(@" - encoding %d (name %@)", encoding, codecName);
 754             
 755             if ([track respondsToSelector:@selector(floatFrameRate)]) {
 756                 frameRate = [track floatFrameRate];
 757                 TRACK_LOG(@" - provided frame rate %0.2f", frameRate);
 758             } else if ((trackMedia = track.media) != nil) {
 759                 // estimate frame rate based on sample count and track duration
 760                 if ([trackMedia hasCharacteristic:QTMediaCharacteristicHasVideoFrameRate]) {
 761                     QTTime duration = [[trackMedia attributeForKey:QTMediaDurationAttribute] QTTimeValue];
 762                     float samples = (float)[[trackMedia attributeForKey:QTMediaSampleCountAttribute] longValue];
 763                     frameRate = samples * ((float)duration.timeScale / (float)duration.timeValue);
 764                     TRACK_LOG(@" - estimated frame rate %0.2f", frameRate);
 765                 } else {
 766                     TRACK_LOG(@" - Unable to determine frame rate!");
 767                 }
 768             }
 769             
 770             // If we will support more media formats in OS X Platform, then select apropriate name.
 771             // Now only "video/x-h264" is supported
 772             CVideoTrack *cvt = new CVideoTrack((int64_t)trackID, "video/x-h264", encoding, trackEnabled,
 773                                                (int)videoSize.width, (int)videoSize.height, frameRate, false);
 774             eventHandler->SendVideoTrackEvent(cvt);
 775             delete cvt;
 776         }
 777 
 778         // get audio tracks
 779         tracks = [movie tracksOfMediaType:QTMediaTypeSound];
 780         for (QTTrack *track in tracks) {
 781             long trackID = [[track attributeForKey:QTTrackIDAttribute] longValue];
 782             BOOL trackEnabled = [[track attributeForKey:QTTrackEnabledAttribute] boolValue];
 783             NSString *codecName = nil;
 784             
 785             TRACK_LOG(@"Audio QTTrack: %@", track);
 786             TRACK_LOG(@" - id %ld (%sabled)", trackID, trackEnabled ? "en" : "dis");
 787 
 788             CTrack::Encoding encoding = CTrack::CUSTOM;
 789             if ([track respondsToSelector:@selector(codecName)]) {
 790                 codecName = [[track codecName] lowercaseString];
 791                 
 792                 if ([codecName hasPrefix:@"aac"]) {
 793                     encoding = CTrack::AAC;
 794                 } else if ([codecName hasPrefix:@"mp3"]) { // FIXME: verify these values, if we ever officially support them
 795                     encoding = CTrack::MPEG1LAYER3;
 796                 } else if ([codecName hasPrefix:@"mpeg"] || [codecName hasPrefix:@"mp2"]) {
 797                     encoding = CTrack::MPEG1AUDIO;
 798                 }
 799             }
 800             TRACK_LOG(@" - encoding %d (name %@)", encoding, codecName);
 801             
 802             float rate = 44100.0; // sane default
 803             if ([track respondsToSelector:@selector(audioSampleRate)]) {
 804                 rate = floor([track audioSampleRate] * 1000.0); // audioSampleRate returns KHz
 805             }
 806             TRACK_LOG(@" - sample rate %0.0f", rate);
 807             
 808             int channelCount = 2;
 809             if ([track respondsToSelector:@selector(audioChannelCount)]) {
 810                 channelCount = [track audioChannelCount];
 811                 if (channelCount == 0) {
 812                     // we may not know (happens with some HLS streams) so just report stereo and hope for the best
 813                     channelCount = 2;
 814                 }
 815             }
 816             TRACK_LOG(@" - channels %d", channelCount);
 817             
 818             int channelMask;
 819             switch (channelCount) {
 820                 default:
 821                     channelMask = CAudioTrack::FRONT_LEFT | CAudioTrack::FRONT_RIGHT;
 822                     break;
 823                 case 5:
 824                 case 6:
 825                     // FIXME: Umm.. why don't we have a SUBWOOFER channel, which is what 5.1 (aka 6) channel audio is???
 826                     channelMask = CAudioTrack::FRONT_LEFT | CAudioTrack::FRONT_RIGHT
 827                     | CAudioTrack::REAR_LEFT | CAudioTrack::REAR_RIGHT
 828                     | CAudioTrack::FRONT_CENTER;
 829                     break;
 830             }
 831             TRACK_LOG(@" - channel mask %02x", channelMask);
 832             
 833             NSString *lang = @"und";
 834             if ([track respondsToSelector:@selector(isoLanguageCodeAsString)]) {
 835                 NSString *newLang = [track isoLanguageCodeAsString];
 836                 // it could return nil, in which case it's undetermined
 837                 if (newLang) {
 838                     lang = newLang;
 839                 }
 840             }
 841             TRACK_LOG(@" - language %@", lang);
 842             
 843             // If we will support more media formats in OS X Platform, then select apropriate name.
 844             // Now only "audio/mpeg" is supported
 845             CAudioTrack *cat = new CAudioTrack((int64_t)trackID, "audio/mpeg", encoding, (bool)trackEnabled,
 846                                                [lang UTF8String], channelCount, channelMask, rate);
 847             eventHandler->SendAudioTrackEvent(cat);
 848             delete cat;
 849         }
 850 
 851         // get subtitle tracks
 852         // FIXME: also QTMediaType{ClosedCaption,Text,etc...}
 853         tracks = [movie tracksOfMediaType:QTMediaTypeSubtitle];
 854         for (QTTrack *track in tracks) {
 855             long trackID = [[track attributeForKey:QTTrackIDAttribute] longValue];
 856             BOOL trackEnabled = [[track attributeForKey:QTTrackEnabledAttribute] boolValue];
 857             NSString *name = [track attributeForKey:QTTrackDisplayNameAttribute];
 858             NSString *codecName = nil;
 859 
 860             TRACK_LOG(@"Subtitle QTTrack: %@", track);
 861             TRACK_LOG(@" - id %ld (%sabled)", trackID, trackEnabled ? "en" : "dis");
 862 
 863             CTrack::Encoding encoding = CTrack::CUSTOM;
 864             if ([track respondsToSelector:@selector(codecName)]) {
 865                 codecName = [[track codecName] lowercaseString];
 866 
 867                 if ([codecName hasPrefix:@"aac"]) {
 868                     encoding = CTrack::AAC;
 869                 } else if ([codecName hasPrefix:@"mp3"]) { // FIXME: verify these values, if we ever officially support them
 870                     encoding = CTrack::MPEG1LAYER3;
 871                 } else if ([codecName hasPrefix:@"mpeg"] || [codecName hasPrefix:@"mp2"]) {
 872                     encoding = CTrack::MPEG1AUDIO;
 873                 }
 874             }
 875             TRACK_LOG(@" - encoding %d (name %@)", encoding, codecName);
 876 
 877             NSString *lang = nil;
 878             if ([track respondsToSelector:@selector(isoLanguageCodeAsString)]) {
 879                 NSString *newLang = [track isoLanguageCodeAsString];
 880                 // it could return nil, in which case it's undetermined
 881                 if (newLang) {
 882                     lang = newLang;
 883                 }
 884             }
 885             TRACK_LOG(@" - language %@", lang);
 886 
 887             CSubtitleTrack *cat = new CSubtitleTrack((int64_t)trackID, [name UTF8String], encoding, (bool)trackEnabled,
 888                                                      [lang UTF8String]);
 889             eventHandler->SendSubtitleTrackEvent(cat);
 890             delete cat;
 891         }
 892     }
 893     
 894 #if DUMP_TRACK_INFO
 895     LOGGER_INFOMSG([trackLog UTF8String]);
 896     [trackLog release];
 897 #endif
 898 }
 899 
 900 - (void) setMovieReady
 901 {
 902     if (movieReady) {
 903         return;
 904     }
 905     
 906     movieReady = YES;
 907     
 908     // send player ready
 909     [self setPlayerState:kPlayerState_READY];
 910     
 911     // get duration
 912     NSNumber *hasDuration = [movie attributeForKey:QTMovieHasDurationAttribute];
 913     if (!suppressDurationEvents && hasDuration.boolValue) {
 914         QTTime movieDur = movie.duration;
 915         // send INFINITY if it's indefinite
 916         if (QTTimeIsIndefinite(movieDur)) {
 917             eventHandler->SendDurationUpdateEvent(INFINITY);
 918             // and suppress all other duration events
 919             suppressDurationEvents = YES;
 920         } else {
 921             // otherwise send duration
 922             NSTimeInterval duration;
 923             if (QTGetTimeInterval(movieDur, &duration)) {
 924                 eventHandler->SendDurationUpdateEvent(self.duration);
 925             }
 926         }
 927     }
 928     
 929     // Get movie tracks (deferred)
 930     [self parseMovieTracks];
 931     
 932     // Assert settings
 933     if (currentTime != 0.0) {
 934         movie.currentTime = QTMakeTimeWithTimeInterval(self.currentTime);
 935     }
 936     
 937     if (mute) {
 938         movie.muted = YES;
 939     }
 940     
 941     if (volume != 1.0) {
 942         movie.volume = volume;
 943     }
 944     
 945     if (requestedState == kPlaybackState_Play) {
 946         [movie play];
 947         [movie setRate:requestedRate];
 948     }
 949 }
 950 
 951 - (void) sendVideoFrame:(CVPixelBufferRef)buf hostTime:(uint64_t)hostTime
 952 {
 953     // http://javafx-jira.kenai.com/browse/RT-27041
 954     // TODO: send off to a work queue for processing on a separate thread to avoid deadlock issues during shutdown
 955     
 956     if (movie && movieReady && eventHandler) {
 957         if (updateHostTimeBase) {
 958             double now = currentTime;
 959             uint64_t hostTime = CVGetCurrentHostTime();
 960             hostTimeFreq = CVGetHostClockFrequency();
 961             uint64_t nowDelta = (uint64_t)(now * hostTimeFreq); // current time in host frequency units
 962             hostTimeBase = hostTime - nowDelta; // Host time at movie time zero
 963             updateHostTimeBase = NO;
 964         }
 965         double frameTime = (double)(hostTime - hostTimeBase) / hostTimeFreq;
 966 
 967         CVVideoFrame *frame = NULL;
 968         try {
 969             frame = new CVVideoFrame(buf, frameTime, hostTime);
 970         } catch (const char *message) {
 971             LOGGER_DEBUGMSG(message);
 972             return;
 973         }
 974         
 975         if (previousWidth < 0 || previousHeight < 0
 976             || previousWidth != frame->GetWidth() || previousHeight != frame->GetHeight())
 977         {
 978             // Send/Queue frame size changed event
 979             previousWidth = frame->GetWidth();
 980             previousHeight = frame->GetHeight();
 981             eventHandler->SendFrameSizeChangedEvent(previousWidth, previousHeight);
 982         }
 983         eventHandler->SendNewFrameEvent(frame);
 984     }
 985 }
 986 
 987 @end