Vuo  2.1.1
VuoAvPlayerObject.m
Go to the documentation of this file.
1 
10 #import "VuoAvPlayerObject.h"
11 #include "VuoCompositionState.h"
12 #include "VuoImageRenderer.h"
13 #include <OpenGL/OpenGL.h>
14 #include <OpenGL/CGLMacro.h>
15 #include "module.h"
16 #include "VuoGlPool.h"
17 #include "VuoVideoFrame.h"
18 #include <QuartzCore/CoreImage.h>
19 #include <Quartz/Quartz.h>
20 #include <QuartzCore/CVImageBuffer.h>
21 #include "VuoAvPlayerInterface.h"
22 #include <Accelerate/Accelerate.h>
23 #include "VuoWindow.h"
24 #include "VuoOsStatus.h"
25 #include "VuoApp.h"
26 #include "HapInAVFoundation.h"
27 
28 #ifdef VUO_COMPILER
30  "title" : "VuoAvPlayerObject",
31  "dependencies" : [
32  "VuoImage",
33  "VuoWindow",
34  "AVFoundation.framework",
35  "VuoImageRenderer",
36  "CoreVideo.framework",
37  "CoreMedia.framework",
38  "VuoGlContext",
39  "Accelerate.framework",
40  "VuoOsStatus",
41 
42  // @todo
43  "QuartzCore.framework",
44  "Quartz.framework"
45 
46  ]
47  });
48 #endif
49 
52 const unsigned int AUDIO_BUFFER_SIZE = 16384;
53 
55 const unsigned int REVERSE_PLAYBACK_FRAME_ADVANCE = 10;
56 
57 
64 @property (readonly) OSType codecSubType;
65 @property (readonly) int dxtPlaneCount;
66 @property (readonly) void** dxtDatas;
67 @property (readonly) OSType* dxtPixelFormats;
68 @property (readonly) NSSize dxtImgSize;
69 @property (readonly) CMTime presentationTime;
71 @end
72 
79 - (id)initWithHapAssetTrack:(AVAssetTrack *)track;
80 - (NSObject<VuoAvTrackHapFrame> *) allocFrameForHapSampleBuffer:(CMSampleBufferRef)n;
82 @end
83 
87 @interface VuoAvTrackOutput : AVAssetReaderTrackOutput
88 {
89  NSObject<VuoAvTrackHapOutput> *hapOutput;
90 }
91 @end
92 
93 @implementation VuoAvTrackOutput
94 
95 - (id)initWithTrack:(AVAssetTrack *)track outputSettings:(NSDictionary *)settings
96 {
97  bool isHap = false;
98  for (id formatDescription in track.formatDescriptions)
99  {
100  CMFormatDescriptionRef desc = (CMFormatDescriptionRef) formatDescription;
101  CMVideoCodecType codec = CMFormatDescriptionGetMediaSubType(desc);
102  if (codec == 'Hap1'
103  || codec == 'Hap5'
104  || codec == 'HapY'
105  || codec == 'HapM')
106  {
107  isHap = true;
108  break;
109  }
110  }
111 
112  if (self = [super initWithTrack:track outputSettings:(isHap ? nil : settings)])
113  {
114  if (isHap)
115  {
116  NSBundle *f = [NSBundle bundleWithPath:[NSString stringWithFormat:@"%s/Vuo.framework/Versions/%s/Frameworks/HapInAVFoundation.framework",
117  VuoGetFrameworkPath(),
118  VUO_FRAMEWORK_VERSION_STRING]];
119  if (!f)
120  {
121  f = [NSBundle bundleWithPath:[NSString stringWithFormat:@"%s/VuoRunner.framework/Versions/%s/Frameworks/HapInAVFoundation.framework",
122  VuoGetRunnerFrameworkPath(),
123  VUO_FRAMEWORK_VERSION_STRING]];
124  if (!f)
125  {
126  VUserLog("Error: Playing this movie requires HapInAVFoundation.framework, but I can't find it.");
127  return nil;
128  }
129  }
130 
131  if (![f isLoaded])
132  {
133  NSError *error;
134  bool status = [f loadAndReturnError:&error];
135  if (!status)
136  {
137  NSError *underlyingError = [[error userInfo] objectForKey:NSUnderlyingErrorKey];
138  if (underlyingError)
139  error = underlyingError;
140  VUserLog("Error: Playing this movie requires HapInAVFoundation.framework, but it wouldn't load: %s", [[error localizedDescription] UTF8String]);
141  return nil;
142  }
143  }
144 
145  hapOutput = [[NSClassFromString(@"AVPlayerItemHapDXTOutput") alloc] initWithHapAssetTrack:track];
146  }
147  }
148  return self;
149 }
150 
151 - (NSObject<VuoAvTrackHapFrame> *)newHapFrame
152 {
153  if (!hapOutput)
154  return nil;
155 
156  CMSampleBufferRef buffer = [super copyNextSampleBuffer];
157  if (!buffer)
158  return nil;
159  VuoDefer(^{ CFRelease(buffer); });
160 
161  return [hapOutput allocFrameForHapSampleBuffer:buffer];
162 }
163 
164 - (void)dealloc
165 {
166  [hapOutput release];
167  [super dealloc];
168 }
169 
170 @end
171 
172 
173 @implementation VuoAvPlayerObject
174 
178 - (id) init
179 {
180  // It seems we need an NSApplication instance to exist prior to using AVFoundation; otherwise, the app beachballs.
181  // https://b33p.net/kosada/node/11006
182  VuoApp_init(false);
183 
184  self = [super init];
185 
186  if (self)
187  {
188  CGLPixelFormatObj pf = VuoGlContext_makePlatformPixelFormat(false, false, -1);
189  __block CVReturn ret;
190  VuoGlContext_perform(^(CGLContextObj cgl_ctx){
191  ret = CVOpenGLTextureCacheCreate(NULL, NULL, cgl_ctx, pf, NULL, &textureCache);
192  });
193  CGLReleasePixelFormat(pf);
194  if (ret != kCVReturnSuccess)
195  {
196  VUserLog("Error: Couldn't create texture cache: %d", ret);
197  return nil;
198  }
199 
200  isReady = false;
201  playOnReady = false;
202  videoTimestamp = 0;
203  audioTimestamp = 0;
204  assetReader = nil;
205  containsUnplayableAudio = false;
206 
207  readyToPlayCallback = NULL;
208  avDecoderCppObject = NULL;
209 
210  videoQueue = [[NSMutableArray alloc] init];
211 
212  audioBufferCapacity = sizeof(Float32) * AUDIO_BUFFER_SIZE;
213  audioBuffer = (Float32*) malloc(audioBufferCapacity);
214  }
215 
216  return self;
217 }
218 
219 + (void) releaseAssetReader:(AVAssetReader*)reader
220 {
221  AVAssetReader* readerToRelease = reader;
222 
223  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, .2 * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
224  {
225  if (readerToRelease.status == AVAssetReaderStatusReading)
226  [readerToRelease cancelReading];
227 
228  [readerToRelease release];
229  });
230 }
231 
232 - (void) dealloc
233 {
234  if(assetReader != nil)
236 
237  if(asset != nil)
238  {
239  [asset cancelLoading];
240  [asset release];
241  }
242 
243  readyToPlayCallback = NULL;
244  avDecoderCppObject = NULL;
245 
246  VuoGlContext_perform(^(CGLContextObj cgl_ctx){
247  CVOpenGLTextureCacheRelease(textureCache);
248  });
249 
250  [self clearFrameQueue];
251 
252  [videoQueue release];
253 
254  if(audioBuffer != nil)
255  free(audioBuffer);
256 
257  [super dealloc];
258 }
259 
263 - (bool) setURL:(NSURL*)url
264 {
265  asset = [AVAsset assetWithURL:url];
266 
267  if(asset == nil)
268  {
269  VUserLog("VuoVideoDecoder: AvFoundation decoder could not find movie file at path: %s", [[url absoluteString] UTF8String]);
270  return false;
271  }
272 
273  [asset retain];
274 
275  // Movies using the Hap codec are not "playable" yet we can still play them.
276  // Also, https://developer.apple.com/videos/play/wwdc2017/511?time=293 says `isPlayable`
277  // means that the current hardware is capable of playing back the current media in realtime,
278  // but we can ignore that since we want to try to play regardless.
279  bool isPlayable = true; //[asset isPlayable];
280 
281  // "Currently, only Apple can 'make' protected content and applications that can play it."
282  // http://www.cocoabuilder.com/archive/cocoa/301980-avfoundation-authorization-for-playback-of-protected-content.html#301981
283  bool isProtected = [asset hasProtectedContent];
284 
285  if (!isPlayable || isProtected || ![asset isReadable])
286  {
287  VUserLog("AvFoundation cannot play this asset (isPlayable=%d, hasProtectedContent=%d, isReadable=%d).",
288  [asset isPlayable], [asset hasProtectedContent], [asset isReadable]);
289  isReady = false;
290  return false;
291  }
292 
293 
294  {
295  AVAssetTrack *videoTrack = [asset tracksWithMediaType:AVMediaTypeVideo][0];
296  CMFormatDescriptionRef desc = (CMFormatDescriptionRef)videoTrack.formatDescriptions[0];
297  CMVideoCodecType codec = CMFormatDescriptionGetMediaSubType(desc);
298  if (codec == 'ap4h' // AVVideoCodecTypeAppleProRes4444
299  || codec == 'Hap5' // Hap Alpha
300  || codec == 'HapM' // Hap Q Alpha
301  || codec == 'HapA') // Hap Alpha-only
302  hasAlpha = true;
303  if (codec == 'hvc1') // HEVC movies may have an alpha attachment.
304  {
305  CFBooleanRef containsAlphaChannel = CMFormatDescriptionGetExtension(desc, CFSTR("ContainsAlphaChannel")); // kCMFormatDescriptionExtension_ContainsAlphaChannel (macOS 10.15+)
306  if (containsAlphaChannel == kCFBooleanTrue)
307  hasAlpha = true;
308  }
309 
310  if (VuoIsDebugEnabled())
311  {
312  char *codecZ = VuoOsStatus_getText(codec);
313  int bitsPerComponent = ((NSNumber *)CMFormatDescriptionGetExtension(desc, CFSTR("BitsPerComponent"))).intValue;
314  int depth = ((NSNumber *)CMFormatDescriptionGetExtension(desc, CFSTR("Depth"))).intValue;
315 
316  VuoText codecName = VuoText_makeFromCFString(CMFormatDescriptionGetExtension(desc, CFSTR("FormatName")));
317  VuoRetain(codecName);
318  VuoText alphaMode = VuoText_makeFromCFString(CMFormatDescriptionGetExtension(desc, CFSTR("AlphaChannelMode")));
319  VuoRetain(alphaMode);
320  VUserLog("codec=\"%s\" (%s) bpc=%d depth=%d hasAlpha=%d %s",
321  codecName,
322  codecZ,
323  bitsPerComponent,
324  depth,
325  hasAlpha,
326  (hasAlpha && alphaMode) ? alphaMode : "");
327  VuoRelease(codecName);
328  VuoRelease(alphaMode);
329  free(codecZ);
330  }
331  }
332 
333 
334  NSArray* assetKeys = @[@"playable", @"hasProtectedContent", @"tracks"];
335 
336  void *compositionState = vuoCopyCompositionStateFromThreadLocalStorage();
337 
339 
343 
345 [asset loadValuesAsynchronouslyForKeys: assetKeys completionHandler:^(void)
346  {
347  vuoAddCompositionStateToThreadLocalStorage(compositionState);
348 
349  // First test whether the values of each of the keys we need have been successfully loaded.
350  NSError *error = nil;
351  bool failedLoading = false;
352 
353  for (NSString *key in assetKeys)
354  {
355  NSInteger status = [asset statusOfValueForKey:key error:&error];
356 
357  if( error != nil ||
358  status == AVKeyValueStatusFailed ||
359  status == AVKeyValueStatusCancelled )
360  {
361  VUserLog("AVFoundation failed loading asset.");
362  failedLoading = true;
363  break;
364  }
365  }
366  isReady = !failedLoading && [self setAssetReaderTimeRange:CMTimeRangeMake(kCMTimeZero, [asset duration])];
367 
368  if(isReady)
369  {
370  for(int i = 0; i < 3; i++)
371  {
372  if(![self copyNextVideoSampleBuffer])
373  {
374  VUserLog("AvFoundation successfully loaded but failed to extract a video buffer.");
375  isReady = false;
376  break;
377  }
378  }
379  }
380  // dispatch_async(dispatch_get_main_queue(), ^{
381  if( self != nil && readyToPlayCallback != NULL && avDecoderCppObject != NULL )
382  readyToPlayCallback( avDecoderCppObject, isReady );
383  // });
384 
385  vuoRemoveCompositionStateFromThreadLocalStorage();
386  }];
387 
388  return true;
389 }
390 
391 - (bool) canBeginPlayback
392 {
393  return isReady;
394 }
395 
396 - (bool) canPlayAudio
397 {
398  return !containsUnplayableAudio;
399 }
400 
401 - (double) getFrameRate
402 {
403  return nominalFrameRate;
404 }
405 
406 - (bool) setAssetReaderTimeRange:(CMTimeRange)timeRange
407 {
408  NSError *error = nil;
409 
410  if(assetReader != nil)
411  {
413 
416  }
417 
418  NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
419 
420  if ([videoTracks count] != 0)
421  {
422  AVAssetTrack* vidtrack = [videoTracks objectAtIndex:0];
423 
424  // for (id formatDescription in vidtrack.formatDescriptions)
425  // {
426  // CMFormatDescriptionRef desc = (CMFormatDescriptionRef) formatDescription;
427  // CMVideoCodecType codec = CMFormatDescriptionGetMediaSubType(desc);
428 
429  // /// jpeg files either don't play or have terrible performance with AVAssetReader,
430  // if(codec == kCMVideoCodecType_JPEG || codec == kCMVideoCodecType_JPEG_OpenDML)
431  // {
432  // VUserLog("AVFoundation detected video codec is JPEG or MJPEG.");
433  // return false;
434  // }
435  // }
436 
437  nominalFrameRate = [vidtrack nominalFrameRate];
438 
439  assetReader = [AVAssetReader assetReaderWithAsset:asset error:&error];
440 
441  [assetReader retain];
442 
443  if(error) {
444  VUserLog("AVAssetReader failed initialization: %s", [[error localizedDescription] UTF8String]);
445  return false;
446  }
447 
448  assetReader.timeRange = timeRange;
449 
450  NSDictionary *videoOutputSettings = @{
451  (NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
452  (NSString *)kCVPixelBufferOpenGLCompatibilityKey: @(YES),
453  };
454 
455  assetReaderVideoTrackOutput = [VuoAvTrackOutput assetReaderTrackOutputWithTrack:vidtrack
456  outputSettings:videoOutputSettings];
458  return false;
459 
460  if([assetReader canAddOutput:assetReaderVideoTrackOutput])
461  {
462  [assetReader addOutput:assetReaderVideoTrackOutput];
463  }
464  else
465  {
466  VUserLog("AVFoundation Video Decoder: assetReaderVideoTrackOutput cannot be added to assetReader.");
467  return false;
468  }
469 
471 
472  NSDictionary* audioOutputSettings;
473 
474  audioOutputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
475  [NSNumber numberWithInt: kAudioFormatLinearPCM ], AVFormatIDKey,
476  [NSNumber numberWithFloat: VuoAudioSamples_sampleRate], AVSampleRateKey,
477  [NSNumber numberWithInt:32], AVLinearPCMBitDepthKey,
478  [NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey,
479  [NSNumber numberWithBool:YES], AVLinearPCMIsFloatKey,
480  [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
481  nil];
482 
483  // AVAssetTrack.playable isn't available pre-10.8, so just try to create the AVAssetTrackReaderOutput out
484  // of every audio track until either one sticks or we run out of tracks.
485  for(AVAssetTrack* track in [asset tracksWithMediaType:AVMediaTypeAudio])
486  {
487  if(track == nil)
488  continue;
489 
490  audioChannelCount = [self getAudioChannelCount:track];
491 
492  if(audioChannelCount < 1)
493  {
495  continue;
496  }
497 
498  assetReaderAudioTrackOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:track outputSettings:audioOutputSettings];
499 
501  {
502  [assetReader addOutput:assetReaderAudioTrackOutput];
503  containsUnplayableAudio = false;
504  break;
505  }
506  else
507  {
508  VUserLog("AVFoundation Video Decoder: AssetReaderAudioTrackOutput cannot be added to assetReader.");
509  audioChannelCount = 0;
511  continue;
512  }
513  }
514 
515  return [assetReader startReading];
516  }
517  else
518  {
519  VUserLog("No video track found!");
520  return false;
521  }
522 }
523 
524 - (unsigned int) getAudioChannelCount:(AVAssetTrack*) track
525 {
526  NSArray* formatDesc = track.formatDescriptions;
527 
528  for(unsigned int i = 0; i < [formatDesc count]; ++i)
529  {
530  CMAudioFormatDescriptionRef item = (CMAudioFormatDescriptionRef)[formatDesc objectAtIndex:i];
531  const AudioStreamBasicDescription* audioDescription = CMAudioFormatDescriptionGetStreamBasicDescription (item);
532 
533  if(audioDescription != nil)
534  {
535  return audioDescription->mChannelsPerFrame;
536  }
537  }
538 
539  return 0;
540 }
541 
542 - (void) setPlaybackRate:(double)rate
543 {
545  if(playbackRate < 0 != rate < 0)
546  {
547  // if was reverse and now is forward
548  if(rate > 0)
549  [self seekToSecond:videoTimestamp withRange:-1 frame:NULL];
550  else
551  [self clearFrameQueue];
552  }
553 
554  playbackRate = rate;
555 }
556 
557 - (bool) audioEnabled
558 {
559  return VuoReal_areEqual(playbackRate, 1);
560 }
561 
562 - (unsigned int) audioChannels
563 {
564  return audioChannelCount;
565 }
566 
567 - (void) clearFrameQueue
568 {
569  for(int i = 0; i < [videoQueue count]; i++)
570  {
571  VuoVideoFrame* frame = [[videoQueue objectAtIndex:i] pointerValue];
572  VuoVideoFrame_release(*frame);
573  free(frame);
574  }
575 
576  [videoQueue removeAllObjects];
577 
580 }
581 
582 - (bool) seekToSecond:(float)second withRange:(float)range frame:(VuoVideoFrame *)frame
583 {
584  if(![self canBeginPlayback])
585  return false;
586 
587  [self clearFrameQueue];
588 
589  if (playbackRate < 0 && VuoReal_areEqual(second, CMTimeGetSeconds(asset.duration)))
590  {
591  float preroll = (1./nominalFrameRate * REVERSE_PLAYBACK_FRAME_ADVANCE);
592  second -= preroll;
593  }
594 
595  CMTime cmsec = CMTimeMakeWithSeconds(second, NSEC_PER_SEC);
596 
597  [self setAssetReaderTimeRange: CMTimeRangeMake(cmsec, range < 0 ? [asset duration] : CMTimeMakeWithSeconds(range, NSEC_PER_SEC))];
598 
599  // read one sample to get an accurate timestamp
600  [self copyNextVideoSampleBuffer];
601  [self copyNextAudioSampleBuffer];
602 
603  if([videoQueue count] > 0)
604  {
605  videoTimestamp = ((VuoVideoFrame*)[[videoQueue objectAtIndex:0] pointerValue])->timestamp;
606  }
607  else if(second >= [self getDuration])
608  {
609  videoTimestamp = [self getDuration];
610  }
611 
612  if (frame)
613  if (![self nextVideoFrame:frame])
614  {
615  frame->image = NULL;
616  frame->timestamp = 0;
617  frame->duration = 0;
618  }
619 
620  return true;
621 }
622 
623 - (void) setPlayerCallback:(void (*)(void* functionPtr, bool canPlayMedia))callback target:(void*)decoderCppObject
624 {
625  readyToPlayCallback = callback;
626  avDecoderCppObject = decoderCppObject;
627 }
628 
630 {
631  return (VuoReal) videoTimestamp;
632 }
633 
635 - (bool) nextVideoFrame:(VuoVideoFrame*)frame
636 {
637  if([videoQueue count] < 1)
638  {
639  if( playbackRate < 0)
640  {
641  if(![self decodePreceedingVideoSamples])
642  return false;
643  }
644  else
645  {
646  if(![self copyNextVideoSampleBuffer] )
647  return false;
648  }
649 
650  if ([videoQueue count] < 1)
651  return false;
652  }
653 
654  int index = playbackRate < 0 ? [videoQueue count] - 1 : 0;
655 
656  NSValue* value = [videoQueue objectAtIndex:index];
657  VuoVideoFrame* framePointer = [value pointerValue];
658  *frame = *framePointer;
659  videoTimestamp = frame->timestamp;
660  [videoQueue removeObjectAtIndex:index];
661 
662  free(framePointer);
663 
664  return true;
665 }
666 
668 {
669  float rewindIncrement = (1./nominalFrameRate * REVERSE_PLAYBACK_FRAME_ADVANCE);
670 
671  if(videoTimestamp <= 0.)
672  return false;
673 
674  float seek = fmax(0, videoTimestamp - rewindIncrement);
675  [self seekToSecond:seek withRange:rewindIncrement frame:NULL];
676 
677  while([self copyNextVideoSampleBuffer]) {}
678 
679  return [videoQueue count] > 0;
680 }
681 
683 {
684  return (VuoReal) CMTimeGetSeconds([asset duration]);
685 }
686 
687 - (bool) nextAudioFrame:(VuoAudioFrame*) frame
688 {
689  if(![self audioEnabled])
690  return false;
691 
692  int sampleIndex = 0;
693  int sampleCapacity = VuoAudioSamples_bufferSize;
694 
695  for(int i = 0; i < audioChannelCount; i++)
696  {
699  VuoListAppendValue_VuoAudioSamples(frame->channels, samples);
700  }
701 
702  while(sampleIndex < sampleCapacity - 1)
703  {
705  {
706  if(![self copyNextAudioSampleBuffer])
707  {
708  return false;
709  }
710  }
711 
712  unsigned int copyLength = MIN(sampleCapacity - sampleIndex, audioBufferSamplesPerChannel - audioBufferSampleIndex);
713 
714  for(int i = 0; i < audioChannelCount; i++)
715  {
716  VuoReal* frame_samples = VuoListGetValue_VuoAudioSamples(frame->channels, i+1).samples;
717  int frame_sample_index = sampleIndex, buffer_sample_index = audioBufferSampleIndex;
718 
719  for(int iterator = 0; iterator < copyLength; iterator++)
720  {
721  frame_samples[frame_sample_index] = audioBuffer[buffer_sample_index * audioChannelCount + i];
722 
723  frame_sample_index++;
724  buffer_sample_index++;
725  }
726  }
727 
728  sampleIndex += copyLength;
729  audioBufferSampleIndex += copyLength;
730  }
731 
732  frame->timestamp = audioTimestamp;
733 
734  return true;
735 }
736 
740 static void VuoAvPlayerObject_freeCallback(VuoImage imageToFree)
741 {
742 }
743 
746 {
747  if( [assetReader status] == AVAssetReaderStatusReading )
748  {
749  __block VuoImage image = NULL;
750  float timestamp;
751  float duration;
752  NSObject<VuoAvTrackHapFrame> *hapFrame = [assetReaderVideoTrackOutput newHapFrame];
753  if (hapFrame)
754  {
755  // For Hap frames, get the DXT buffer in CPU memory, and upload it to the GPU.
756 
757  timestamp = CMTimeGetSeconds(hapFrame.presentationTime);
758  duration = 1./nominalFrameRate;
759  int dxtPlaneCount = hapFrame.dxtPlaneCount;
760  OSType *dxtPixelFormats = hapFrame.dxtPixelFormats;
761  void **dxtBaseAddresses = hapFrame.dxtDatas;
762 
763  if (dxtPlaneCount > 2)
764  {
765  VUserLog("Error: This image has %d planes, which isn't part of the Hap spec.", dxtPlaneCount);
766  return false;
767  }
768 
769  VuoGlContext_perform(^(CGLContextObj cgl_ctx){
770  VuoImage dxtImage[dxtPlaneCount];
771  for (int i = 0; i < dxtPlaneCount; ++i)
772  {
773  GLuint internalFormat;
774  unsigned int bitsPerPixel;
775  switch (dxtPixelFormats[i])
776  {
777  case kHapCVPixelFormat_RGB_DXT1:
778  internalFormat = HapTextureFormat_RGB_DXT1;
779  bitsPerPixel = 4;
780  break;
781 
782  case kHapCVPixelFormat_RGBA_DXT5:
783  case kHapCVPixelFormat_YCoCg_DXT5:
784  internalFormat = HapTextureFormat_RGBA_DXT5;
785  bitsPerPixel = 8;
786  break;
787 
788  case kHapCVPixelFormat_CoCgXY:
789  if (i == 0)
790  {
791  internalFormat = HapTextureFormat_RGBA_DXT5;
792  bitsPerPixel = 8;
793  }
794  else
795  {
796  internalFormat = HapTextureFormat_A_RGTC1;
797  bitsPerPixel = 4;
798  }
799  break;
800 
801  case kHapCVPixelFormat_YCoCg_DXT5_A_RGTC1:
802  if (i == 0)
803  {
804  internalFormat = HapTextureFormat_RGBA_DXT5;
805  bitsPerPixel = 8;
806  }
807  else
808  {
809  internalFormat = HapTextureFormat_A_RGTC1;
810  bitsPerPixel = 4;
811  }
812  break;
813 
814  case kHapCVPixelFormat_A_RGTC1:
815  internalFormat = HapTextureFormat_A_RGTC1;
816  bitsPerPixel = 4;
817  break;
818 
819  default:
820  VUserLog("Error: Unknown Hap dxtPixelFormat %s.", VuoOsStatus_getText(dxtPixelFormats[i]));
821  return;
822  }
823 
824  GLuint texture = VuoGlTexturePool_use(cgl_ctx, VuoGlTexturePool_NoAllocation, GL_TEXTURE_2D, internalFormat, hapFrame.dxtImgSize.width, hapFrame.dxtImgSize.height, GL_RGBA, NULL);
825  glBindTexture(GL_TEXTURE_2D, texture);
826 
827  size_t bytesPerRow = (hapFrame.dxtImgSize.width * bitsPerPixel) / 8;
828  GLsizei dataLength = (int)(bytesPerRow * hapFrame.dxtImgSize.height);
829 
830  glPixelStorei(GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);
831 
832  glCompressedTexImage2D(GL_TEXTURE_2D, 0, internalFormat, hapFrame.dxtImgSize.width, hapFrame.dxtImgSize.height, 0, dataLength, dxtBaseAddresses[i]);
833 
834  glPixelStorei(GL_UNPACK_CLIENT_STORAGE_APPLE, GL_FALSE);
835  glBindTexture(GL_TEXTURE_2D, 0);
836 
837  dxtImage[i] = VuoImage_make(texture, internalFormat, hapFrame.dxtImgSize.width, hapFrame.dxtImgSize.height);
838  }
839 
840  if (hapFrame.codecSubType == kHapCodecSubType
841  || hapFrame.codecSubType == kHapAlphaCodecSubType)
842  image = VuoImage_makeCopy(dxtImage[0], true, 0, 0, false);
843  else if (hapFrame.codecSubType == kHapYCoCgCodecSubType)
844  {
845  // Based on https://github.com/Vidvox/hap-in-avfoundation/blob/master/HapInAVF%20Test%20App/ScaledCoCgYToRGBA.frag
846  static const char *fragmentShader = VUOSHADER_GLSL_SOURCE(120,
847  uniform sampler2D cocgsy_src;
848  const vec4 offsets = vec4(-0.50196078431373, -0.50196078431373, 0.0, 0.0);
849  varying vec2 fragmentTextureCoordinate;
850  void main()
851  {
852  vec4 CoCgSY = texture2D(cocgsy_src, vec2(fragmentTextureCoordinate.x, 1. - fragmentTextureCoordinate.y));
853 
854  CoCgSY += offsets;
855 
856  float scale = ( CoCgSY.z * ( 255.0 / 8.0 ) ) + 1.0;
857 
858  float Co = CoCgSY.x / scale;
859  float Cg = CoCgSY.y / scale;
860  float Y = CoCgSY.w;
861 
862  gl_FragColor = vec4(Y + Co - Cg, Y + Cg, Y - Co - Cg, 1.0);
863  }
864  );
865 
866  VuoShader shader = VuoShader_make("YCoCg to RGB Shader");
867  VuoRetain(shader);
868  VuoShader_addSource(shader, VuoMesh_IndividualTriangles, NULL, NULL, fragmentShader);
869  VuoShader_setUniform_VuoImage(shader, "cocgsy_src", dxtImage[0]);
870  image = VuoImageRenderer_render(shader, dxtImage[0]->pixelsWide, dxtImage[0]->pixelsHigh, VuoImageColorDepth_8);
871  VuoRelease(shader);
872  }
873  else if (hapFrame.codecSubType == kHapYCoCgACodecSubType)
874  {
875  // Based on https://github.com/Vidvox/hap-in-avfoundation/blob/master/HapInAVF%20Test%20App/ScaledCoCgYPlusAToRGBA.frag
876  static const char *fragmentShader = VUOSHADER_GLSL_SOURCE(120,
877  uniform sampler2D cocgsy_src;
878  uniform sampler2D alpha_src;
879  const vec4 offsets = vec4(-0.50196078431373, -0.50196078431373, 0.0, 0.0);
880  varying vec2 fragmentTextureCoordinate;
881  void main()
882  {
883  vec2 tc = vec2(fragmentTextureCoordinate.x, 1. - fragmentTextureCoordinate.y);
884  vec4 CoCgSY = texture2D(cocgsy_src, tc);
885  float alpha = texture2D(alpha_src, tc).r;
886 
887  CoCgSY += offsets;
888 
889  float scale = ( CoCgSY.z * ( 255.0 / 8.0 ) ) + 1.0;
890 
891  float Co = CoCgSY.x / scale;
892  float Cg = CoCgSY.y / scale;
893  float Y = CoCgSY.w;
894 
895  gl_FragColor = vec4(Y + Co - Cg, Y + Cg, Y - Co - Cg, alpha);
896  }
897  );
898 
899  VuoShader shader = VuoShader_make("YCoCg+A to RGBA Shader");
900  VuoRetain(shader);
901  VuoShader_addSource(shader, VuoMesh_IndividualTriangles, NULL, NULL, fragmentShader);
902  VuoShader_setUniform_VuoImage(shader, "cocgsy_src", dxtImage[0]);
903  VuoShader_setUniform_VuoImage(shader, "alpha_src", dxtImage[1]);
904  image = VuoImageRenderer_render(shader, dxtImage[0]->pixelsWide, dxtImage[0]->pixelsHigh, VuoImageColorDepth_8);
905  VuoRelease(shader);
906  }
907  else
908  {
909  VUserLog("Error: Unknown codecSubType '%s'.", VuoOsStatus_getText(hapFrame.codecSubType));
910  return;
911  }
912  });
913 
914  [hapFrame release];
915 
916  if (!image)
917  return false;
918  }
919  else
920  {
921  // For non-Hap frames, AV Foundation can directly give us an OpenGL texture.
922 
923  CMSampleBufferRef sampleBuffer = [assetReaderVideoTrackOutput copyNextSampleBuffer];
924 
925  if (!sampleBuffer)
926  return false;
927 
928  timestamp = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer));
929  duration = CMTimeGetSeconds(CMSampleBufferGetDuration(sampleBuffer));
930  if (isnan(duration))
931  duration = 1./nominalFrameRate;
932 
933  CVPixelBufferRef buffer = (CVPixelBufferRef) CMSampleBufferGetImageBuffer(sampleBuffer);
934  if (!buffer)
935  {
936  CMSampleBufferInvalidate(sampleBuffer);
937  CFRelease(sampleBuffer);
938  return false;
939  }
940 
941  __block CVOpenGLTextureRef texture;
942  __block CVReturn ret;
943  VuoGlContext_perform(^(CGLContextObj cgl_ctx){
944  ret = CVOpenGLTextureCacheCreateTextureFromImage(NULL, textureCache, buffer, NULL, &texture);
945  });
946  if (ret != kCVReturnSuccess)
947  {
948  char *error = VuoOsStatus_getText(ret);
949  VUserLog("Error: Couldn't convert CVImageBuffer to texture: %s", error);
950  free(error);
951  CMSampleBufferInvalidate(sampleBuffer);
952  CFRelease(sampleBuffer);
953  return false;
954  }
956  CVOpenGLTextureGetName(texture),
957  hasAlpha ? GL_RGBA : GL_RGB,
958  CVPixelBufferGetWidth(buffer),
959  CVPixelBufferGetHeight(buffer),
960  VuoAvPlayerObject_freeCallback, NULL);
961  VuoRetain(rectImage);
962  image = VuoImage_makeCopy(rectImage, CVOpenGLTextureIsFlipped(texture), 0, 0, false);
963  CVOpenGLTextureRelease(texture);
964  VuoRelease(rectImage);
965  VuoGlContext_perform(^(CGLContextObj cgl_ctx){
966  CVOpenGLTextureCacheFlush(textureCache, 0);
967  });
968 
969  CMSampleBufferInvalidate(sampleBuffer);
970  CFRelease(sampleBuffer);
971  }
972 
973  VuoVideoFrame* frame = (VuoVideoFrame*) malloc(sizeof(VuoVideoFrame));
974  *frame = VuoVideoFrame_make(image, timestamp, duration);
975  VuoVideoFrame_retain(*frame);
976  NSValue* val = [NSValue valueWithPointer:frame];
977  [videoQueue addObject:val];
978 
979  return true;
980  }
981  else if ([assetReader status] == AVAssetReaderStatusFailed)
982  {
983  VUserLog("Error: AVAssetReader failed: %s. %s.",
984  [[[assetReader error] localizedDescription] UTF8String],
985  [[[assetReader error] localizedFailureReason] UTF8String]);
986  return false;
987  }
988  else
989  {
990  VUserLog("Error: AVAssetReader status %ld", [assetReader status]);
991  return false;
992  }
993 }
994 
995 - (bool) copyNextAudioSampleBuffer
996 {
997  if([assetReader status] == AVAssetReaderStatusReading)
998  {
999  CMSampleBufferRef audioSampleBuffer = [assetReaderAudioTrackOutput copyNextSampleBuffer];
1000 
1002 
1003  if(audioSampleBuffer == NULL)
1004  {
1006  return false;
1007  }
1008 
1009  // CMItemCount numSamplesInAudioBuffer = CMSampleBufferGetNumSamples(audioSampleBuffer);
1010  audioTimestamp = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(audioSampleBuffer));
1011 
1012  CMBlockBufferRef audioBlockBuffer = CMSampleBufferGetDataBuffer( audioSampleBuffer );
1013  // AudioBufferList audioBufferList;
1014 
1015  size_t bufferSize = 0;
1016 
1017  CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
1018  audioSampleBuffer,
1019  &bufferSize,
1020  NULL,
1021  0,
1022  NULL,
1023  NULL,
1024  0,
1025  NULL
1026  );
1027 
1028  AudioBufferList* audioBufferList = malloc(bufferSize);
1029 
1030  OSStatus err = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
1031  audioSampleBuffer,
1032  NULL,
1033  audioBufferList,
1034  bufferSize,
1035  NULL,
1036  NULL,
1037  kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
1038  &audioBlockBuffer
1039  );
1040 
1041  if(err != 0)
1042  {
1043  VUserLog("AvFoundation failed extracting audio buffer: %i", err);
1044 
1046  free(audioBufferList);
1047  CFRelease(audioSampleBuffer);
1048 
1049  return false;
1050  }
1051 
1052  // CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
1053  // audioSampleBuffer,
1054  // NULL,
1055  // &audioBufferList,
1056  // sizeof(audioBufferList),
1057  // NULL,
1058  // NULL,
1059  // kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
1060  // &audioBlockBuffer);
1061 
1062  // I haven't been able to find anything about when multiple buffers would be used. In every format
1063  // so far there's only ever one, with channels interleaved in mData. I wasn't sure whether multiple
1064  // buffers would mean additional channels, or more data for the same channels, or a different track
1065  // entirely. So for now just look at the first buffer and check here first when something inevitably
1066  // goes wrong.
1067 
1068  // for (int curBuffer = 0; curBuffer < audioBufferList.mNumberBuffers; curBuffer++)
1069  const int curBuffer = 0;
1070 
1071  audioBufferChannelCount = audioBufferList->mBuffers[curBuffer].mNumberChannels;
1072  size_t dataByteSize = audioBufferList->mBuffers[curBuffer].mDataByteSize;
1073  audioBufferSamplesPerChannel = (dataByteSize / sizeof(Float32)) / audioBufferChannelCount;
1074 
1075  if( dataByteSize > audioBufferCapacity )
1076  {
1077  if (!(audioBuffer = realloc(audioBuffer, dataByteSize)))
1078  {
1079  free(audioBufferList);
1080  CFRelease(audioSampleBuffer);
1082  VUserLog("AvFoundation video decoder is out of memory.");
1083  return false;
1084  }
1085 
1086  audioBufferCapacity = dataByteSize;
1087  }
1088 
1089  Float32* samples = (Float32 *)audioBufferList->mBuffers[curBuffer].mData;
1090  memcpy(audioBuffer, samples, audioBufferList->mBuffers[curBuffer].mDataByteSize);
1091 
1092  free(audioBufferList);
1093  CFRelease(audioSampleBuffer);
1094 
1095 
1096  // Sometimes AVFoundation says the movie has a certain number of audio channels,
1097  // but actually decodes a different number of audio channels.
1098  // https://b33p.net/kosada/node/12952
1100  {
1101  VUserLog("Warning: AVFoundation reported %d audio channel%s, but actually decoded %d channel%s.",
1102  audioChannelCount, audioChannelCount == 1 ? "" : "s",
1105  }
1106 
1107  return true;
1108  }
1109  return false;
1110 }
1111 
1112 @end