1 /*
   2  * Copyright (c) 2010, 2014, 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                      eventHandler->SendPlayerMediaErrorEvent(ERROR_OSX_INIT);
 326                  }
 327              }
 328              
 329              if (!movieReady) {
 330                  if (loadState > QTMovieLoadStateLoaded) {
 331                      [blockSelf setMovieReady];
 332                  }
 333              } else if (requestedState == kPlaybackState_Play) {
 334                  // if state is QTMovieLoadStatePlayable then we've stalled
 335                  // if state is QTMovieLoadStatePlaythroughOK then we're playing
 336                  if (loadState == QTMovieLoadStatePlayable && previousPlayerState == kPlayerState_PLAYING) {
 337                      [blockSelf setPlayerState:kPlayerState_STALLED];
 338                  } else if (loadState == QTMovieLoadStatePlaythroughOK) {
 339                      [blockSelf setPlayerState:kPlayerState_PLAYING];
 340                  }
 341              }
 342          }];
 343         
 344         [self registerForNotification:QTMovieTimeDidChangeNotification
 345                                object:qtMovie
 346                             withBlock:
 347          ^(NSNotification *note) {
 348              // grab currentTime and current host time and set our host time base accordingly
 349              double now = blockSelf.currentTime;
 350              uint64_t hostTime = CVGetCurrentHostTime();
 351              hostTimeFreq = CVGetHostClockFrequency();
 352              uint64_t nowDelta = (uint64_t)(now * hostTimeFreq); // current time in host frequency units
 353              hostTimeBase = hostTime - nowDelta; // Host time at movie time zero
 354              LOGGER_DEBUGMSG(([[NSString stringWithFormat:@"Movie time changed %lf", currentTime] UTF8String]));
 355              
 356              // http://javafx-jira.kenai.com/browse/RT-27041
 357              // TODO: flush video buffers
 358          }];
 359         
 360         [self registerForNotification:QTMovieRateDidChangeNotification
 361                                object:qtMovie
 362                             withBlock:
 363          ^(NSNotification *note) {
 364              NSNumber *newRate = [note.userInfo objectForKey:QTMovieRateDidChangeNotificationParameter];
 365              [blockSelf rateChanged:newRate.floatValue];
 366          }];
 367         
 368         // QTMovieNaturalSizeDidChangeNotification is unreliable, especially with HTTP live streaming
 369         // so just use the pixel buffer sizes to send frame size changed events
 370         
 371         // QTMovieAvailableRangesDidChangeNotification
 372         [self registerForNotification:@"QTMovieAvailableRangesDidChangeNotification"
 373                                object:qtMovie
 374                             withBlock:
 375          ^(NSNotification *note) {
 376              NSArray *ranges = nil;
 377              if ([movie respondsToSelector:@selector(availableRanges)]) {
 378                  ranges = [movie performSelector:@selector(availableRanges)];
 379              }
 380              if (!suppressDurationEvents && ranges) {
 381                  for (NSValue *rangeVal in ranges) {
 382                      QTTimeRange timeRange = [rangeVal QTTimeRangeValue]; // .time, .duration
 383                      // if duration is indefinite then it's a live stream and we need to report as such
 384                      if (QTTimeIsIndefinite(timeRange.duration)) {
 385                          eventHandler->SendDurationUpdateEvent(INFINITY);
 386                          // and suppress all other subsequent events
 387                          suppressDurationEvents = YES;
 388                          isLiveStream = YES;
 389                          break;
 390                      }
 391                  }
 392              }
 393          }];
 394         
 395         // QTMovieLoadedRangesDidChangeNotification
 396         [self registerForNotification:@"QTMovieLoadedRangesDidChangeNotification"
 397                                object:qtMovie
 398                             withBlock:
 399          ^(NSNotification *note) {
 400              NSArray *ranges = nil;
 401              if ([movie respondsToSelector:@selector(loadedRanges)]) {
 402                  ranges = [movie performSelector:@selector(loadedRanges)];
 403              }
 404              // don't emit progress events for live streams
 405              if (!suppressDurationEvents && ranges) {
 406                  int64_t total = 0;
 407                  for (NSValue *rangeVal in ranges) {
 408                      QTTimeRange timeRange = [rangeVal QTTimeRangeValue]; // .time, .duration
 409                      NSTimeInterval duration;
 410                      QTGetTimeInterval(timeRange.duration, &duration);
 411                      
 412                      total += (int64_t)(duration * 1000);
 413                  }
 414                  // send buffer progress event
 415                  double movieDur = blockSelf.duration;
 416                  eventHandler->SendBufferProgressEvent(movieDur, 0, (int64_t)(movieDur * 1000), total);
 417              }
 418          }];
 419         
 420 #if 0
 421         // show all notifications, use to find possibly missed notifications
 422         [[NSNotificationCenter defaultCenter]
 423          addObserverForName:nil
 424          object:qtMovie
 425          queue:nil
 426          usingBlock:^(NSNotification *note) {
 427              NSLog(@"Movie notification: %@", note.name);
 428          }
 429          ];
 430 #endif
 431         
 432 #if 0
 433         // Template notification block, remember to use blockSelf instead of self
 434         [[NSNotificationCenter defaultCenter]
 435          addObserverForName:QTMovieXXX
 436          object:qtMovie
 437          queue:nil
 438          usingBlock:
 439          ^(NSNotification *note) {
 440              
 441          }
 442          ];
 443 #endif
 444         // http://javafx-jira.kenai.com/browse/RT-27041
 445         // TODO: test for addImageConsumer first, fall back on CARenderer hack if it's not available
 446         [qtMovie addImageConsumer:frameHandler];
 447         
 448         movie = (QTMovie*)[[MTObjectProxy objectProxyWithTarget:qtMovie] retain];
 449     }
 450 }
 451 
 452 
 453 - (void) play
 454 {
 455     requestedState = kPlaybackState_Play;
 456     if (movie && movieReady) {
 457         [movie play];
 458         [movie setRate:requestedRate];
 459     }
 460 }
 461 
 462 - (void) pause
 463 {
 464     if (requestedState == kPlaybackState_Stop) {
 465         requestedState = kPlaybackState_Pause;
 466         [self setPlayerState:kPlayerState_PAUSED];
 467     } else {
 468         requestedState = kPlaybackState_Pause;
 469         if (movie && movieReady) {
 470             [movie stop];
 471         }
 472         
 473         if (previousPlayerState == kPlayerState_STALLED) {
 474             [self setPlayerState:kPlayerState_PAUSED];
 475         }
 476     }
 477 }
 478 
 479 - (void) finish
 480 {
 481     requestedState = kPlaybackState_Finished;
 482     [self setPlayerState:kPlayerState_FINISHED];
 483     if (movie && movieReady) {
 484         [movie stop];
 485     }
 486 }
 487 
 488 - (void) stop
 489 {
 490     if (requestedState == kPlaybackState_Finished || requestedState == kPlaybackState_Pause) {
 491         requestedState = kPlaybackState_Stop;
 492         [self setPlayerState:kPlayerState_STOPPED];
 493     } else {
 494         requestedState = kPlaybackState_Stop;
 495         if (movie && movieReady) {
 496             // we need to just nuke the "STOPPED" state...
 497             [movie stop];
 498         } else {
 499             currentTime = 0.0;
 500         }
 501         
 502         if (previousPlayerState == kPlayerState_STALLED) {
 503             [self setPlayerState:kPlayerState_STOPPED];
 504         }
 505     }
 506 }
 507 
 508 
 509 - (void) rateChanged:(float)newRate
 510 {
 511     /*
 512      * Relevant PlayerState values:
 513      *      PLAYING - rate != 0
 514      *      PAUSED  - reqRate == 0, rate == 0
 515      *      STOPPED - stopFlag && reqRate == 0, rate == 0
 516      *      STALLED - detected by load state or reqRate != 0, rate == 0 and state is PLAYING
 517      */
 518     if (newRate == 0.0) {
 519         // slop for FP/timescale error
 520         if (requestedState == kPlaybackState_Stop) {
 521             [self setPlayerState:kPlayerState_STOPPED];
 522         } else if (requestedState == kPlaybackState_Play && previousPlayerState == kPlayerState_PLAYING && requestedRate != 0.0) {
 523             [self setPlayerState:kPlayerState_STALLED];        
 524         } else if (requestedState != kPlaybackState_Finished) {
 525             [self setPlayerState:kPlayerState_PAUSED];
 526         }
 527         
 528     } else {
 529         // non-zero is always playing
 530         [self setPlayerState:kPlayerState_PLAYING];
 531     }
 532 }
 533 
 534 @synthesize audioSyncDelay;
 535 
 536 - (BOOL) mute
 537 {
 538     if (movie && movieReady) {
 539         mute = movie.muted;
 540         return mute;
 541     }
 542     return mute;
 543 }
 544 
 545 - (void) setMute:(BOOL)state
 546 {
 547     mute = state;
 548     if (movie && movieReady) {
 549         movie.muted = state;
 550     }
 551 }
 552 
 553 - (float) volume
 554 {
 555     if (movie && movieReady) {
 556         volume = movie.volume;
 557     }
 558     return volume;
 559 }
 560 
 561 - (void) setVolume:(float)newVolume
 562 {
 563     volume = newVolume;
 564     if (movie && movieReady) {
 565         movie.volume = (float)volume;
 566     }
 567 }
 568 
 569 - (float) balance
 570 {
 571     if (movie && movieReady) {
 572         if ([movie respondsToSelector:@selector(balance)]) {
 573             balance = [movie balance];
 574         } else if (eventHandler) {
 575             eventHandler->Warning(WARNING_JFXMEDIA_BALANCE, NULL);
 576         }
 577     }
 578     return balance;
 579 }
 580 
 581 - (void) setBalance:(float)newBalance
 582 {
 583     balance = newBalance;
 584     if (movie && movieReady) {
 585         if ([movie respondsToSelector:@selector(setBalance:)]) {
 586             [movie setBalance:balance];
 587         } else if (eventHandler) {
 588             eventHandler->Warning(WARNING_JFXMEDIA_BALANCE, NULL);
 589         }
 590     }
 591 }
 592 
 593 - (double) duration
 594 {
 595     if (movie && movieReady) {
 596         NSNumber *hasDuration = [movie attributeForKey:QTMovieHasDurationAttribute];
 597         if (hasDuration.boolValue) {
 598             QTTime movieDur = movie.duration;
 599             NSTimeInterval duration;
 600             if (QTGetTimeInterval(movieDur, &duration)) {
 601                 return duration;
 602             }
 603         }
 604     }
 605     return -1.0; // hack value for UNKNOWN, since duration must be >= 0
 606 }
 607 
 608 - (float) rate
 609 {
 610     return requestedRate;
 611 }
 612 
 613 - (void) setRate:(float)newRate
 614 {
 615     if (isLiveStream) {
 616         LOGGER_WARNMSG("Cannot set playback rate on LIVE stream!");
 617         return;
 618     }
 619 
 620     requestedRate = newRate;
 621     if (movie && movieReady && requestedState == kPlaybackState_Play) {
 622         [movie setRate:requestedRate];
 623     }
 624 }
 625 
 626 - (double) currentTime
 627 {
 628     if (movie && movieReady) {
 629         QTTime time = movie.currentTime;
 630         NSTimeInterval timeIval;
 631         if (QTGetTimeInterval(time, &timeIval)) {
 632             currentTime = timeIval;
 633         }
 634     }
 635     return currentTime;
 636 }
 637 
 638 - (void) setCurrentTime:(double)newTime
 639 {
 640     if (isLiveStream) {
 641         LOGGER_WARNMSG("Cannot seek LIVE stream!");
 642         return;
 643     }
 644     
 645     currentTime = newTime;
 646     
 647     if (movie && movieReady) {
 648         movie.currentTime = QTMakeTimeWithTimeInterval(newTime);
 649         
 650         // make sure we're playing if requested
 651         if (requestedState == kPlaybackState_Play) {
 652             [movie play];
 653             [movie setRate:requestedRate];
 654         } else if (requestedState == kPlaybackState_Finished) {
 655             requestedState = kPlaybackState_Play;
 656             [movie play];
 657             [movie setRate:requestedRate];
 658         }
 659     }
 660 }
 661 
 662 - (void) setPlayerState:(int)newState
 663 {
 664     if (newState != previousPlayerState) {
 665         if (newState == kPlayerState_PLAYING) {
 666             updateHostTimeBase = YES;
 667         }
 668         // For now just send up to client
 669         eventHandler->SendPlayerStateEvent(newState, 0.0);
 670         previousPlayerState = newState;
 671     }
 672 }
 673 
 674 #if DUMP_TRACK_INFO
 675 static void append_log(NSMutableString *s, NSString *fmt, ...) {
 676     va_list args;
 677     va_start(args, fmt);
 678     NSString *appString = [[NSString alloc] initWithFormat:fmt arguments:args];
 679     [s appendFormat:@"%@\n", appString];
 680     va_end(args);
 681     [appString release];
 682 }
 683 #define TRACK_LOG(fmt, ...) append_log(trackLog, fmt, ##__VA_ARGS__)
 684 #else
 685 #define TRACK_LOG(...) {}
 686 #endif
 687 
 688 - (void) parseMovieTracks
 689 {
 690 #if DUMP_TRACK_INFO
 691     NSMutableString *trackLog = [[NSMutableString alloc] initWithFormat:@"Parsing tracks for movie %@:\n", movie];
 692 #endif    
 693     /*
 694      * Track properties we care about at the FX level:
 695      *
 696      * track:
 697      *   + trackEnabled (boolean)
 698      *   + trackID (jlong)
 699      *   + name (string)
 700      *   + locale (Locale) - language is derived from Locale or null
 701      *   + language (3 char iso code)
 702      * video track:
 703      *   + width (int)
 704      *   + height (int)
 705      * audio track: (no additional properties)
 706      *
 707      *
 708      * Track properties at the com.sun level:
 709      *
 710      * track:
 711      *   X trackEnabled (boolean) == QTTrackEnabledAttribute
 712      *   X trackID (long) == QTTrackIDAttribute
 713      *   X name (string) == QTTrackDisplayNameAttribute
 714      *   X encoding (enum) == non-public selector: - (NSString*) codecName
 715      * video track: (QTTrackMediaTypeAttribute == 'vide')
 716      *   X frame size (w,h) == QTTrackDimensionsAttribute (NSValue:NSSize)
 717      *   X frame rate
 718      *   X hasAlpha (boolean) == false (for now)
 719      * audio track: (QTTrackMediaTypeAttribute == 'soun')
 720      *   X language == non-public selector: - (NSString*) isoLanguageCodeAsString
 721      *   X channels (int) == non-public selector: - (int) audioChannelCount
 722      *   X channel mask (int) == parsed from channel count (or ASBD)
 723      *   X sample rate (float) == non-public selector: - (float) audioSampleRate
 724      *
 725      * just create CVideoTrack or CAudioTrack and send them up with eventHandler and we're good
 726      */
 727     NSArray *tracks = movie.tracks;
 728     if (tracks) {
 729         // get video tracks
 730         NSArray *tracks = [movie tracksOfMediaType:QTMediaTypeVideo];
 731         for (QTTrack *track in tracks) {
 732             long trackID = [[track attributeForKey:QTTrackIDAttribute] longValue];
 733             BOOL trackEnabled = [[track attributeForKey:QTTrackEnabledAttribute] boolValue];
 734             NSSize videoSize = [[track attributeForKey:QTTrackDimensionsAttribute] sizeValue];
 735             QTMedia *trackMedia;
 736             float frameRate = 29.97; // default
 737             NSString *codecName = nil;
 738             
 739             TRACK_LOG(@"Video QTTrack: %@", track);
 740             TRACK_LOG(@" - id %ld (%sabled)", trackID, trackEnabled ? "en" : "dis");
 741 
 742             CTrack::Encoding encoding = CTrack::CUSTOM;
 743             if ([track respondsToSelector:@selector(codecName)]) {
 744                 codecName = [[track codecName] lowercaseString];
 745                 if ([codecName hasPrefix:@"h.264"] || [codecName hasPrefix:@"avc"]) {
 746                     encoding = CTrack::H264;
 747                 }
 748             }
 749             TRACK_LOG(@" - encoding %d (name %@)", encoding, codecName);
 750             
 751             if ([track respondsToSelector:@selector(floatFrameRate)]) {
 752                 frameRate = [track floatFrameRate];
 753                 TRACK_LOG(@" - provided frame rate %0.2f", frameRate);
 754             } else if ((trackMedia = track.media) != nil) {
 755                 // estimate frame rate based on sample count and track duration
 756                 if ([trackMedia hasCharacteristic:QTMediaCharacteristicHasVideoFrameRate]) {
 757                     QTTime duration = [[trackMedia attributeForKey:QTMediaDurationAttribute] QTTimeValue];
 758                     float samples = (float)[[trackMedia attributeForKey:QTMediaSampleCountAttribute] longValue];
 759                     frameRate = samples * ((float)duration.timeScale / (float)duration.timeValue);
 760                     TRACK_LOG(@" - estimated frame rate %0.2f", frameRate);
 761                 } else {
 762                     TRACK_LOG(@" - Unable to determine frame rate!");
 763                 }
 764             }
 765             
 766             // If we will support more media formats in OS X Platform, then select apropriate name.
 767             // Now only "video/x-h264" is supported
 768             CVideoTrack *cvt = new CVideoTrack((int64_t)trackID, "video/x-h264", encoding, trackEnabled,
 769                                                (int)videoSize.width, (int)videoSize.height, frameRate, false);
 770             eventHandler->SendVideoTrackEvent(cvt);
 771             delete cvt;
 772         }
 773 
 774         // get audio tracks
 775         tracks = [movie tracksOfMediaType:QTMediaTypeSound];
 776         for (QTTrack *track in tracks) {
 777             long trackID = [[track attributeForKey:QTTrackIDAttribute] longValue];
 778             BOOL trackEnabled = [[track attributeForKey:QTTrackEnabledAttribute] boolValue];
 779             NSString *codecName = nil;
 780             
 781             TRACK_LOG(@"Audio QTTrack: %@", track);
 782             TRACK_LOG(@" - id %ld (%sabled)", trackID, trackEnabled ? "en" : "dis");
 783 
 784             CTrack::Encoding encoding = CTrack::CUSTOM;
 785             if ([track respondsToSelector:@selector(codecName)]) {
 786                 codecName = [[track codecName] lowercaseString];
 787                 
 788                 if ([codecName hasPrefix:@"aac"]) {
 789                     encoding = CTrack::AAC;
 790                 } else if ([codecName hasPrefix:@"mp3"]) { // FIXME: verify these values, if we ever officially support them
 791                     encoding = CTrack::MPEG1LAYER3;
 792                 } else if ([codecName hasPrefix:@"mpeg"] || [codecName hasPrefix:@"mp2"]) {
 793                     encoding = CTrack::MPEG1AUDIO;
 794                 }
 795             }
 796             TRACK_LOG(@" - encoding %d (name %@)", encoding, codecName);
 797             
 798             float rate = 44100.0; // sane default
 799             if ([track respondsToSelector:@selector(audioSampleRate)]) {
 800                 rate = floor([track audioSampleRate] * 1000.0); // audioSampleRate returns KHz
 801             }
 802             TRACK_LOG(@" - sample rate %0.0f", rate);
 803             
 804             int channelCount = 2;
 805             if ([track respondsToSelector:@selector(audioChannelCount)]) {
 806                 channelCount = [track audioChannelCount];
 807                 if (channelCount == 0) {
 808                     // we may not know (happens with some HLS streams) so just report stereo and hope for the best
 809                     channelCount = 2;
 810                 }
 811             }
 812             TRACK_LOG(@" - channels %d", channelCount);
 813             
 814             int channelMask;
 815             switch (channelCount) {
 816                 default:
 817                     channelMask = CAudioTrack::FRONT_LEFT | CAudioTrack::FRONT_RIGHT;
 818                     break;
 819                 case 5:
 820                 case 6:
 821                     // FIXME: Umm.. why don't we have a SUBWOOFER channel, which is what 5.1 (aka 6) channel audio is???
 822                     channelMask = CAudioTrack::FRONT_LEFT | CAudioTrack::FRONT_RIGHT
 823                     | CAudioTrack::REAR_LEFT | CAudioTrack::REAR_RIGHT
 824                     | CAudioTrack::FRONT_CENTER;
 825                     break;
 826             }
 827             TRACK_LOG(@" - channel mask %02x", channelMask);
 828             
 829             NSString *lang = @"und";
 830             if ([track respondsToSelector:@selector(isoLanguageCodeAsString)]) {
 831                 NSString *newLang = [track isoLanguageCodeAsString];
 832                 // it could return nil, in which case it's undetermined
 833                 if (newLang) {
 834                     lang = newLang;
 835                 }
 836             }
 837             TRACK_LOG(@" - language %@", lang);
 838             
 839             // If we will support more media formats in OS X Platform, then select apropriate name.
 840             // Now only "audio/mpeg" is supported
 841             CAudioTrack *cat = new CAudioTrack((int64_t)trackID, "audio/mpeg", encoding, (bool)trackEnabled,
 842                                                [lang UTF8String], channelCount, channelMask, rate);
 843             eventHandler->SendAudioTrackEvent(cat);
 844             delete cat;
 845         }
 846 
 847         // get subtitle tracks
 848         // FIXME: also QTMediaType{ClosedCaption,Text,etc...}
 849         tracks = [movie tracksOfMediaType:QTMediaTypeSubtitle];
 850         for (QTTrack *track in tracks) {
 851             long trackID = [[track attributeForKey:QTTrackIDAttribute] longValue];
 852             BOOL trackEnabled = [[track attributeForKey:QTTrackEnabledAttribute] boolValue];
 853             NSString *name = [track attributeForKey:QTTrackDisplayNameAttribute];
 854             NSString *codecName = nil;
 855 
 856             TRACK_LOG(@"Subtitle QTTrack: %@", track);
 857             TRACK_LOG(@" - id %ld (%sabled)", trackID, trackEnabled ? "en" : "dis");
 858 
 859             CTrack::Encoding encoding = CTrack::CUSTOM;
 860             if ([track respondsToSelector:@selector(codecName)]) {
 861                 codecName = [[track codecName] lowercaseString];
 862 
 863                 if ([codecName hasPrefix:@"aac"]) {
 864                     encoding = CTrack::AAC;
 865                 } else if ([codecName hasPrefix:@"mp3"]) { // FIXME: verify these values, if we ever officially support them
 866                     encoding = CTrack::MPEG1LAYER3;
 867                 } else if ([codecName hasPrefix:@"mpeg"] || [codecName hasPrefix:@"mp2"]) {
 868                     encoding = CTrack::MPEG1AUDIO;
 869                 }
 870             }
 871             TRACK_LOG(@" - encoding %d (name %@)", encoding, codecName);
 872 
 873             NSString *lang = nil;
 874             if ([track respondsToSelector:@selector(isoLanguageCodeAsString)]) {
 875                 NSString *newLang = [track isoLanguageCodeAsString];
 876                 // it could return nil, in which case it's undetermined
 877                 if (newLang) {
 878                     lang = newLang;
 879                 }
 880             }
 881             TRACK_LOG(@" - language %@", lang);
 882 
 883             CSubtitleTrack *cat = new CSubtitleTrack((int64_t)trackID, [name UTF8String], encoding, (bool)trackEnabled,
 884                                                      [lang UTF8String]);
 885             eventHandler->SendSubtitleTrackEvent(cat);
 886             delete cat;
 887         }
 888     }
 889     
 890 #if DUMP_TRACK_INFO
 891     LOGGER_INFOMSG([trackLog UTF8String]);
 892     [trackLog release];
 893 #endif
 894 }
 895 
 896 - (void) setMovieReady
 897 {
 898     if (movieReady) {
 899         return;
 900     }
 901     
 902     movieReady = YES;
 903     
 904     // send player ready
 905     [self setPlayerState:kPlayerState_READY];
 906     
 907     // get duration
 908     NSNumber *hasDuration = [movie attributeForKey:QTMovieHasDurationAttribute];
 909     if (!suppressDurationEvents && hasDuration.boolValue) {
 910         QTTime movieDur = movie.duration;
 911         // send INFINITY if it's indefinite
 912         if (QTTimeIsIndefinite(movieDur)) {
 913             eventHandler->SendDurationUpdateEvent(INFINITY);
 914             // and suppress all other duration events
 915             suppressDurationEvents = YES;
 916         } else {
 917             // otherwise send duration
 918             NSTimeInterval duration;
 919             if (QTGetTimeInterval(movieDur, &duration)) {
 920                 eventHandler->SendDurationUpdateEvent(self.duration);
 921             }
 922         }
 923     }
 924     
 925     // Get movie tracks (deferred)
 926     [self parseMovieTracks];
 927     
 928     // Assert settings
 929     if (currentTime != 0.0) {
 930         movie.currentTime = QTMakeTimeWithTimeInterval(self.currentTime);
 931     }
 932     
 933     if (mute) {
 934         movie.muted = YES;
 935     }
 936     
 937     if (volume != 1.0) {
 938         movie.volume = volume;
 939     }
 940     
 941     if (requestedState == kPlaybackState_Play) {
 942         [movie play];
 943         [movie setRate:requestedRate];
 944     }
 945 }
 946 
 947 - (void) sendVideoFrame:(CVPixelBufferRef)buf hostTime:(uint64_t)hostTime
 948 {
 949     // http://javafx-jira.kenai.com/browse/RT-27041
 950     // TODO: send off to a work queue for processing on a separate thread to avoid deadlock issues during shutdown
 951     
 952     if (movie && movieReady && eventHandler) {
 953         if (updateHostTimeBase) {
 954             double now = currentTime;
 955             uint64_t hostTime = CVGetCurrentHostTime();
 956             hostTimeFreq = CVGetHostClockFrequency();
 957             uint64_t nowDelta = (uint64_t)(now * hostTimeFreq); // current time in host frequency units
 958             hostTimeBase = hostTime - nowDelta; // Host time at movie time zero
 959             updateHostTimeBase = NO;
 960         }
 961         double frameTime = (double)(hostTime - hostTimeBase) / hostTimeFreq;
 962 
 963         CVVideoFrame *frame = NULL;
 964         try {
 965             frame = new CVVideoFrame(buf, frameTime, hostTime);
 966         } catch (const char *message) {
 967             LOGGER_DEBUGMSG(message);
 968             return;
 969         }
 970         
 971         if (previousWidth < 0 || previousHeight < 0
 972             || previousWidth != frame->GetWidth() || previousHeight != frame->GetHeight())
 973         {
 974             // Send/Queue frame size changed event
 975             previousWidth = frame->GetWidth();
 976             previousHeight = frame->GetHeight();
 977             eventHandler->SendFrameSizeChangedEvent(previousWidth, previousHeight);
 978         }
 979         eventHandler->SendNewFrameEvent(frame);
 980     }
 981 }
 982 
 983 @end