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