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