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