From 0080a32cebc5aaf46834b812c771e91b4a8ce911 Mon Sep 17 00:00:00 2001 From: Patrick Piemonte Date: Wed, 4 Sep 2013 23:33:10 -0700 Subject: [PATCH] PBJVision: onion skinning (ghosting), fixes GH-1 --- PBJVision.podspec | 7 +- Project/Vision.xcodeproj/project.pbxproj | 42 +- Project/Vision/PBJViewController.m | 70 ++-- Project/Vision/UI/capture_onion@2x.png | Bin 0 -> 28469 bytes .../Vision/UI/capture_onion_selected@2x.png | Bin 0 -> 28469 bytes Project/Vision/Vision-Info.plist | 2 - Source/PBJVision.h | 4 + Source/PBJVision.m | 377 +++++++++++++++++- Source/Shaders/Shader.fsh | 22 + Source/Shaders/Shader.vsh | 11 + 10 files changed, 494 insertions(+), 41 deletions(-) create mode 100644 Project/Vision/UI/capture_onion@2x.png create mode 100644 Project/Vision/UI/capture_onion_selected@2x.png create mode 100644 Source/Shaders/Shader.fsh create mode 100644 Source/Shaders/Shader.vsh diff --git a/PBJVision.podspec b/PBJVision.podspec index e4a7ca8..eead096 100644 --- a/PBJVision.podspec +++ b/PBJVision.podspec @@ -1,13 +1,14 @@ Pod::Spec.new do |s| s.name = "PBJVision" - s.version = "0.1.1" + s.version = "0.1.2" s.summary = "iOS camera engine, supports touch-to-record video and photo capture." s.homepage = "https://github.com/piemonte/PBJVision" s.license = "MIT" s.authors = { "Patrick Piemonte" => "piemonte@alumni.cmu.edu" } - s.source = { :git => "https://github.com/piemonte/PBJVision.git", :tag => "v0.1.1" } - s.frameworks = 'Foundation', 'AVFoundation', 'CoreGraphics', 'CoreMedia', 'CoreVideo', 'MobileCoreServices', 'ImageIO', 'QuartzCore' + s.source = { :git => "https://github.com/piemonte/PBJVision.git", :tag => "v0.1.2" } + s.frameworks = 'Foundation', 'AVFoundation', 'CoreGraphics', 'CoreMedia', 'CoreVideo', 'MobileCoreServices', 'ImageIO', 'QuartzCore', 'OpenGLES', 'UIKit' s.platform = :ios, '6.0' s.source_files = 'Source' + s.resources = 'Source/Shaders/*' s.requires_arc = true end diff --git a/Project/Vision.xcodeproj/project.pbxproj b/Project/Vision.xcodeproj/project.pbxproj index 5248a5f..7a22f9d 100644 --- a/Project/Vision.xcodeproj/project.pbxproj +++ b/Project/Vision.xcodeproj/project.pbxproj @@ -14,6 +14,12 @@ 060F7B19179F50B800E27091 /* capture_rec_off@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 060F7B15179F50B800E27091 /* capture_rec_off@2x.png */; }; 060F7B1A179F50B800E27091 /* capture_yep@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 060F7B16179F50B800E27091 /* capture_yep@2x.png */; }; 060F7B1D179F550800E27091 /* capture_flip@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 060F7B1C179F550800E27091 /* capture_flip@2x.png */; }; + 0624D09717D43BB500665930 /* capture_onion_selected@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0624D09517D43BB500665930 /* capture_onion_selected@2x.png */; }; + 0624D09817D43BB500665930 /* capture_onion@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0624D09617D43BB500665930 /* capture_onion@2x.png */; }; + 0624D09A17D43D5D00665930 /* OpenGLES.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0624D09917D43D5D00665930 /* OpenGLES.framework */; }; + 067D52F817D8574200541B5E /* GLKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 067D52F717D8574200541B5E /* GLKit.framework */; }; + 067D52FC17D857DC00541B5E /* Shader.fsh in Resources */ = {isa = PBXBuildFile; fileRef = 067D52FA17D857AB00541B5E /* Shader.fsh */; }; + 067D52FD17D857DC00541B5E /* Shader.vsh in Resources */ = {isa = PBXBuildFile; fileRef = 067D52FB17D857AB00541B5E /* Shader.vsh */; }; 0683D1C2179F2E1700EE66D6 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0683D1C1179F2E1700EE66D6 /* Foundation.framework */; }; 0683D1C4179F2E1700EE66D6 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0683D1C3179F2E1700EE66D6 /* CoreGraphics.framework */; }; 0683D1C6179F2E1700EE66D6 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0683D1C5179F2E1700EE66D6 /* UIKit.framework */; }; @@ -45,6 +51,12 @@ 060F7B15179F50B800E27091 /* capture_rec_off@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "capture_rec_off@2x.png"; path = "UI/capture_rec_off@2x.png"; sourceTree = ""; }; 060F7B16179F50B800E27091 /* capture_yep@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "capture_yep@2x.png"; path = "UI/capture_yep@2x.png"; sourceTree = ""; }; 060F7B1C179F550800E27091 /* capture_flip@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "capture_flip@2x.png"; path = "UI/capture_flip@2x.png"; sourceTree = ""; }; + 0624D09517D43BB500665930 /* capture_onion_selected@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "capture_onion_selected@2x.png"; path = "UI/capture_onion_selected@2x.png"; sourceTree = ""; }; + 0624D09617D43BB500665930 /* capture_onion@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "capture_onion@2x.png"; path = "UI/capture_onion@2x.png"; sourceTree = ""; }; + 0624D09917D43D5D00665930 /* OpenGLES.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGLES.framework; path = System/Library/Frameworks/OpenGLES.framework; sourceTree = SDKROOT; }; + 067D52F717D8574200541B5E /* GLKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GLKit.framework; path = System/Library/Frameworks/GLKit.framework; sourceTree = SDKROOT; }; + 067D52FA17D857AB00541B5E /* Shader.fsh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.glsl; name = Shader.fsh; path = ../Source/Shaders/Shader.fsh; sourceTree = ""; }; + 067D52FB17D857AB00541B5E /* Shader.vsh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.glsl; name = Shader.vsh; path = ../Source/Shaders/Shader.vsh; sourceTree = ""; }; 0683D1BE179F2E1700EE66D6 /* Vision.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vision.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0683D1C1179F2E1700EE66D6 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 0683D1C3179F2E1700EE66D6 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -79,6 +91,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 067D52F817D8574200541B5E /* GLKit.framework in Frameworks */, + 0624D09A17D43D5D00665930 /* OpenGLES.framework in Frameworks */, 060F7B0F179F459900E27091 /* AssetsLibrary.framework in Frameworks */, 06B7D031179F33B500F3F527 /* ImageIO.framework in Frameworks */, 06B7D02F179F333400F3F527 /* MobileCoreServices.framework in Frameworks */, @@ -102,10 +116,21 @@ 060F7B15179F50B800E27091 /* capture_rec_off@2x.png */, 060F7B16179F50B800E27091 /* capture_yep@2x.png */, 060F7B1C179F550800E27091 /* capture_flip@2x.png */, + 0624D09517D43BB500665930 /* capture_onion_selected@2x.png */, + 0624D09617D43BB500665930 /* capture_onion@2x.png */, ); name = UI; sourceTree = ""; }; + 067D52F917D8577D00541B5E /* Shaders */ = { + isa = PBXGroup; + children = ( + 067D52FA17D857AB00541B5E /* Shader.fsh */, + 067D52FB17D857AB00541B5E /* Shader.vsh */, + ); + name = Shaders; + sourceTree = ""; + }; 0683D1B5179F2E1700EE66D6 = { isa = PBXGroup; children = ( @@ -128,13 +153,15 @@ isa = PBXGroup; children = ( 060F7B0E179F459900E27091 /* AssetsLibrary.framework */, - 06B7D030179F33B500F3F527 /* ImageIO.framework */, - 06B7D02D179F330E00F3F527 /* MobileCoreServices.framework */, - 06B7D02B179F313800F3F527 /* CoreVideo.framework */, - 06B7D029179F312F00F3F527 /* CoreMedia.framework */, 06B7D027179F307D00F3F527 /* AVFoundation.framework */, - 0683D1C1179F2E1700EE66D6 /* Foundation.framework */, 0683D1C3179F2E1700EE66D6 /* CoreGraphics.framework */, + 06B7D029179F312F00F3F527 /* CoreMedia.framework */, + 06B7D02B179F313800F3F527 /* CoreVideo.framework */, + 0683D1C1179F2E1700EE66D6 /* Foundation.framework */, + 067D52F717D8574200541B5E /* GLKit.framework */, + 06B7D030179F33B500F3F527 /* ImageIO.framework */, + 06B7D02D179F330E00F3F527 /* MobileCoreServices.framework */, + 0624D09917D43D5D00665930 /* OpenGLES.framework */, 0683D1C5179F2E1700EE66D6 /* UIKit.framework */, ); name = Frameworks; @@ -176,6 +203,7 @@ 06B7D026179F2F0800F3F527 /* Vision */ = { isa = PBXGroup; children = ( + 067D52F917D8577D00541B5E /* Shaders */, 06B7D01A179F2E7700F3F527 /* PBJVision.h */, 06B7D01B179F2E7700F3F527 /* PBJVision.m */, 06B7D01C179F2E7700F3F527 /* PBJVisionUtilities.h */, @@ -237,8 +265,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 067D52FC17D857DC00541B5E /* Shader.fsh in Resources */, + 067D52FD17D857DC00541B5E /* Shader.vsh in Resources */, 06B7D025179F2EDC00F3F527 /* Release.xcconfig in Resources */, 060F7B18179F50B800E27091 /* capture_rec_blink@2x.png in Resources */, + 0624D09717D43BB500665930 /* capture_onion_selected@2x.png in Resources */, 06B7D036179F357600F3F527 /* Default.png in Resources */, 0683D1CC179F2E1700EE66D6 /* InfoPlist.strings in Resources */, 060F7B17179F50B800E27091 /* capture_rec_base@2x.png in Resources */, @@ -249,6 +280,7 @@ 06B7D037179F357600F3F527 /* Default@2x.png in Resources */, 060F7B19179F50B800E27091 /* capture_rec_off@2x.png in Resources */, 06B7D024179F2EDC00F3F527 /* Debug.xcconfig in Resources */, + 0624D09817D43BB500665930 /* capture_onion@2x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Project/Vision/PBJViewController.m b/Project/Vision/PBJViewController.m index f7573d6..0bf20c6 100644 --- a/Project/Vision/PBJViewController.m +++ b/Project/Vision/PBJViewController.m @@ -11,6 +11,7 @@ #import "PBJStrobeView.h" #import +#import @interface UIButton (ExtendedHit) @@ -37,10 +38,14 @@ @interface PBJViewController () < { PBJStrobeView *_strobeView; UIButton *_doneButton; + UIButton *_flipButton; + UIButton *_onionButton; UIView *_previewView; AVCaptureVideoPreviewLayer *_previewLayer; + GLKViewController *_effectsViewController; + UILabel *_instructionLabel; UILongPressGestureRecognizer *_longPressGestureRecognizer; @@ -99,21 +104,28 @@ - (void)_setup // preview _previewView = [[UIView alloc] initWithFrame:CGRectZero]; _previewView.backgroundColor = [UIColor blackColor]; - CGRect previewFrame = CGRectZero; - previewFrame.origin = CGPointMake(0, 60.0f); - CGFloat previewWidth = self.view.frame.size.width; - previewFrame.size = CGSizeMake(previewWidth, previewWidth); + CGRect previewFrame = CGRectMake(0, 60.0f, CGRectGetWidth(self.view.frame), CGRectGetWidth(self.view.frame)); _previewView.frame = previewFrame; - - // add AV layer _previewLayer = [[PBJVision sharedInstance] previewLayer]; - CGRect previewBounds = _previewView.layer.bounds; - _previewLayer.bounds = previewBounds; + _previewLayer.frame = _previewView.bounds; _previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; - _previewLayer.position = CGPointMake(CGRectGetMidX(previewBounds), CGRectGetMidY(previewBounds)); [_previewView.layer addSublayer:_previewLayer]; [self.view addSubview:_previewView]; + // onion skin + _effectsViewController = [[GLKViewController alloc] init]; + _effectsViewController.preferredFramesPerSecond = 60; + + GLKView *view = (GLKView *)_effectsViewController.view; + CGRect viewFrame = _previewView.bounds; + view.frame = viewFrame; + view.context = [[PBJVision sharedInstance] context]; + view.contentScaleFactor = [[UIScreen mainScreen] scale]; + view.alpha = 0.5f; + view.hidden = YES; + [[PBJVision sharedInstance] setPresentationFrame:_previewView.frame]; + [_previewView addSubview:_effectsViewController.view]; + // instruction label _instructionLabel = [[UILabel alloc] initWithFrame:self.view.bounds]; _instructionLabel.textAlignment = NSTextAlignmentCenter; @@ -145,17 +157,18 @@ - (void)_setup // flip button _flipButton = [UIButton buttonWithType:UIButtonTypeCustom]; - - UIImage *flipImage = [UIImage imageNamed:@"capture_flip"]; - [_flipButton setImage:flipImage forState:UIControlStateNormal]; - - CGRect flipFrame = _flipButton.frame; - flipFrame.size = CGSizeMake(25.0f, 20.0f); - flipFrame.origin = CGPointMake(10.0f, CGRectGetHeight(self.view.bounds) - 10.0f); - _flipButton.frame = flipFrame; - + [_flipButton setImage:[UIImage imageNamed:@"capture_flip"] forState:UIControlStateNormal]; + _flipButton.frame = CGRectMake(15.0f, CGRectGetHeight(self.view.bounds) - 15.0f, 30.0f, 25.0f); [_flipButton addTarget:self action:@selector(_handleFlipButton:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_flipButton]; + + // onion button + _onionButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [_onionButton setImage:[UIImage imageNamed:@"capture_onion"] forState:UIControlStateNormal]; + [_onionButton setImage:[UIImage imageNamed:@"capture_onion_selected"] forState:UIControlStateSelected]; + _onionButton.frame = CGRectMake(CGRectGetWidth(self.view.bounds) - 25.0f - 15.0f, CGRectGetHeight(self.view.bounds) - 15.0f, 25.0f, 25.0f); + [_onionButton addTarget:self action:@selector(_handleOnionSkinningButton:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:_onionButton]; } #pragma mark - view lifecycle @@ -194,17 +207,20 @@ - (void)_startCapture - (void)_pauseCapture { [[PBJVision sharedInstance] pauseVideoCapture]; + _effectsViewController.view.hidden = !_onionButton.selected; } - (void)_resumeCapture { [[PBJVision sharedInstance] resumeVideoCapture]; + _effectsViewController.view.hidden = YES; } - (void)_endCapture { [UIApplication sharedApplication].idleTimerDisabled = NO; [[PBJVision sharedInstance] endVideoCapture]; + _effectsViewController.view.hidden = YES; } - (void)_resetCapture @@ -218,6 +234,7 @@ - (void)_resetCapture [vision setCameraDevice:PBJCameraDeviceBack]; [vision setCameraOrientation:PBJCameraOrientationPortrait]; [vision setFocusMode:PBJFocusModeAutoFocus]; + [vision setVideoRenderingEnabled:YES]; } #pragma mark - UIButton @@ -232,6 +249,13 @@ - (void)_handleFlipButton:(UIButton *)button } } +- (void)_handleOnionSkinningButton:(UIButton *)button +{ + [_onionButton setSelected:!_onionButton.selected]; + if (_recording) + _effectsViewController.view.hidden = !_onionButton.selected; +} + - (void)_handleDoneButton:(UIButton *)button { // resets long press @@ -287,16 +311,6 @@ - (void)visionSessionDidStop:(PBJVision *)vision { } -- (void)visionPreviewDidStart:(PBJVision *)vision -{ - _longPressGestureRecognizer.enabled = YES; -} - -- (void)visionPreviewWillStop:(PBJVision *)vision -{ - _longPressGestureRecognizer.enabled = NO; -} - - (void)visionModeWillChange:(PBJVision *)vision { } diff --git a/Project/Vision/UI/capture_onion@2x.png b/Project/Vision/UI/capture_onion@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d92896d2156cd6e718015834a2432f5711eab7c4 GIT binary patch literal 28469 zcmd5_2YeJo+utj>OD^To3n36XhTdO#Z_=B9C>??#0*V462-3wDM3ia}P5&pvtr7|mLCg(uE#rV+xF}^X8fe_AC4J6A+U4X zwt*8q7(e=*_eKfAlsh4Pg8TM~@@{UppW3F&f|=iR8QKCpM2MKe8@Rt_rX;QIQ4 za_et<^*ZL(zP;N2?79W2#fxWt>D8;c(-*p1+VFB47SH}+!54oRA5QyW?2W^QCl^io zZnlWrkrJ}^vi7LG)ACOBdNr8ow6#qcx4&nfIeGeFc+FO`bQLEEuJ(IJmisWLfIINavx!xS{QnM#PG*L&lMCd>@UST#){4Em;=mtx&gA<$#gf_B1~roxWP zZ+y{S7*JdA9Npz)W8qc}p?2r~qdN&d9}tdSa&_Dz*gFZedo7sznxLL03_V++!W3c7 zD#5cua{p#Y)pof?1_4v6_B0Ev*tq?yLH0E!YWw%ES*Gfx&OX&V8xB(rTivSW@u{o) z>dvaK|2yS^Ago*Jh1sU$Ou6K;>(Zr$i}$+>sS*9V#$0~*@W-Zz%iAZk5`>47W?V5< zueG<-%%+-|<4hYW#jD2*c3k(7VbSP4`d0gdby0Wvr%WJj+OPjA^!DwD@bFu&pKLQ| z*dP6;n}UxIIodzxz04WSvJGi}|MsZjth#T{>Zs26`}l>_t{Y2V{@P*A@Vifzbj&{B zXwD9}R{mt?;h_V9JRkRRpSZZ)>Mv93ZY;mB&DXD;s&v&n^3j9pMpcUk4TPWHZK0U9 zw8oaVN2t=fRB;_J>F6VaAS53ef9$WS_Ntjl{HaPw{a#EeuOT5>u`SXKcN;Uto;rhrww~x`^ zS<<5Inc7F6`fBT)e6M!R(O{!ev>Wh2h-F2BQR`{`0Yzp+DgXkprj%eyD5-DkBw z>d;UL&8sl*9-R#FPA(>7c#un#2+*IJgVCJk9S}9i*SvoAE6nwvTo~s zKdkP3Z_E0=ZVi^a*=FbZ{k?zcHGhjo{f|~RzBt>vbC-=fH+*}o)EJ*JhIf8?I_X!p zd*7DnzT(gRSIE%H0lw|1jC zF)Jfi=~s>KHy~(#(5GHY|ImH=`nQ$8^$)7mck8xI+wO03?3dE-eBUKo2ltu2t3$tf zeI9Rh_-pz0e%nHN#`N*;6TG$QmigO)w~X)G^2Zqia~5g_p6Gg_N82BJt=m21@!`{f zjwAf`PN`mQtn>DHAI)lTvhwbWw?F+j=+MfXfJ2L#Z`-=Qaoc*Y*XzHxcgi=TTWTM# zoS5<5r0x!pRX49lneoMa&xF?=z2?8Ee%H`Cquv_3ZTX7j1GabC-fMfum?I5$#cdAR zbfo>EfyP-bt9unD=8VhQoYv84fM%~#MNOE~CEsSJ8cZDDETGnzu%upX z-#*c*?Z#o>4qNeX&eG5;&3?Gv;lCa9U%qqtt;ZD~k2^i?^beIamCIG`(k;4M{OaD_ zLN~p(scF}yUC&&+aB;)M8>Q z-*Wn?)a6mNf(Kl7y?m?Q92YlqBz(Dt(m=8@)4%vXfXGb<^ARSRdeo;65m%=qNfa=$Nb*(7XK z`kvcyt-o9s+S;r2lFxU39&v1ExiM#BzuH;O5V`Ww(o65S%5V_N!=4OVbYg~6 zmAUih_Mg{s@q`5fN?ob5qQT@VciVpWpj}qGDH?5?i|lZSnrHQnJV7N{e+N4*3E!LJ+>g6I{CfY!d0qO~2pI6>$c}d#?`bx1YMAb^ zmtD1aZ)kQjJNU)^pmEoGY>(@|V%Ll#l|Sh=cGloye|&yypJHr`V?SNQ-03lQZ^pgd zV?eEe5B3eqnit~V!fW9dyF9~uR(O4OtLoh;5&rYX|MB~(yWjk>b^FQCk(Wnb9Ub-L zevP$*T$XQJ@nnU|`_8xT{NnuJjbGx@(%XmCT5+*z*rD0`+lKz|d$WUwM{PcL>0;BC zeShqm+Bd6j!}zQB_6@t4@&1)~|7nNQ>&IRX8+6!^)8gE3Vf_^Sx?LZAeU!1Yaj&7} zu7%s8GL{?1y}h&hxQTIRR996w+6pfFZ-)MLJ>Y8dzP*>+s20=FRq94jSY?e$#}ylL)ruHdow<$l5^?& zq1%(I#P2-Z2SFG)v~#=GeWxgdo+E!+qbL)ACjWA>v!kUf2#$^}g29j~-e+ZHh}VLD92}e! zPL3|i3>m2xl2ek`DHMu=RRm`zy^xb_6twm_A={V*m}$?{c~u4KR|UAZaLr~@p-t@6 zn(1DirN*Xbq~@fhr|&nJa-s{Z_&+LC{*U^7rdy51Ua+&vpH*s&ua~Fatc)y!ptZAC zd3ty+C;eGxWg!DeT2@c)Ja?d=@}g(ClYg4HLf?d*iiOoNaOBsl3@`oybZd~gf# zB&DJ#SAPGhum3XyW@E>gHSyY|n?$w8Sy;8m?sL*BVILjmIqf=a0r z5|iV3`#($hm;7HEMs{|#;NkA`X--Zyl$?|#>VxhRa&v&4on2{@iL_@);9Zqk?Sss# zkZGlOva_?~TL0>`Pz0LIW-8SHO(v5kGWswHT8)Es_9NwC zC{~G-nC*;=a+8pjo+7>jb=LFoDI1FG>sFQjm0NXTW`S0VV2H>zMnUJ`4B2Ec9pvjv z49#pZjrR2Pdy_2d;-oK=o|ZCQp;U|~NG%kJRZ?&fEj^3Hd{(JcT0W$vCJ9QViVN-R z;qJBD#l_8Et+s0v6BEw3;%{Puf8kG|GEfCsL!^;?Ffg!+LCDI=6tc53!C}@@D*g!F z+`Qf=BFM%>fTN>xmz0!55rOI1DMAh?(UR1#0Yys7M~0R~%*@OX#|SF^#L3CkKONKv zO|Exy^WGL2`LHZt{42&_p$Oyz1}1lBPMMi{9ILCIB@UyGBle$IVS_>Chdr~-{% z!lhzRDwSoyk`~Ph(UGT#9VK~~2MBeA9HSAd2>y`jshu44!zGqBW05liozB@q=jhxI zRo1cIizEd_2me{m6M*t&od>m01m?X)qg)^Wi%CNZ3WkhyP8{z{;(M0cnCd~(+M6s! zGuuP6szHf}FiWP1?4{sYbXRh_M*+v8P%1^i=jG|kE(0(QZ&I^72Ex=iICaFkb*xaF zBrCEU$bBUiC_W)tglng^6BH_y=;e@ztkXXt(7H13<$=3pw&!a;JB7m^Q5L?HU`VY%&z z%E}~;i((}$Et!-oE|>%}PpRe+lmxxl2rO(+1gg`Q#OA<0bkre2p0Et>TA+u2Yc!mAqIL`J3|)X~vNY;Hzf(jnNiwpQ!lVzQEzq-ape zJ!suz1{VN`ZSon8A`rnuIccvDRORq|Ln_Pr3&e`t0NYcvaS4Vt>5HaUfj0%^#Ns$YQKY~if0^`35GW4_TG!Rr zr_6R3yX~@I_>#J~;%c=@WWn%=zk@NM$7XYesDv!<-F);OL#X}@SSO{yli?#hP*65tM;A!e9N5D z>)qSYz?E%3PJ|Q}6B98VVXD<+O=o94C{inEwK~Do)iXq^)!OA)1rgW=bz?E%L!2~} zgqRg7bv?2q5pSKc(H5*e5R0RsEN0H3=L2L@*2!YTy3LE;mna2dU>KjGJn{^0Z~tCO zrP`S;7YEFNll{Pk>CrJ!qd-xciAcI@L;&4Hu-!y=H?Lk~Ga4>(qQXc{Ntmm#(}+f4 zYHG6B20k^mF`i8n8}R2{5RBc1zWbrQF&SYmvVKXYmqs8pC8!H<%pO3%J9@qUSjZqm zSaN`rED?ER86cA;&i$z9@IiP!OEv>*i|!W@1}f`^5Z5A7iZz80>Dc2)gNd}9i$KIO za)WyxAgOLa_LUK2G{Q8|yY~RwuL8V}(I!@YSCZG4Vqpv(0b^|qL{S!Cl`pvJy*`6! z;z{{MKbMIJfL1i88&bZHeEP64sN*aVVSwi270>oS#7Cw4%4`F2RPai2=SQv0oRkzl z0t=Ky`AOsv!C>{JuoYE&E<8!v9~3?el>Qs~7>336C<@_)!}#aY7otG22nJIf+HACw zlYWey8ZrxIa!`{KBy#ATfMw$ldinx8W?vAg_V6@luCP8PUls=VOm{c$p+p=J&vs0N z0n7@q@lO#m%-%{iW_z+dklGYco&I8U=s9yF48nwLt>NwMuogRjM}g2EfyBdr_3$}O zZGKfO1ai{oxEhc)7@?z)_Vzk=mT}KQnztmNsTdgBOHEI{mXsJjQKxe}!tG0ht^|9O zC&6WMbkq+3@;=2PR3i&h$yg(g$j`{kNJ&bH8%Qdoc|x{Cxr4V?=^aEf4Pm*>^JZ)` zW{6r}r*mlqAGZ}erN2~ayHFtb0OoTQbBK9f`{%v08G%?yjkB}sU?6Wg%z-l0hExzt z@+2q^Ng+gDd_v5@q~y3!KvFm(7*GO2DZfClV_bDNH_s`07tcXNIyJORv4q8YNoJ~q zCr|J8M|{v=hE|n)TQiN{b+9RYY5dClvfc9%7l3ft7Z(>utgaIj)uEj~baHYzfpP2u zA`bzPm!1cwuy-~g5P+M5&88ug)scEE3uXajkM#tkiQB=4)d;b1PeT$CP95gACZ3=?} z7mmS*drzaoE0SV37RKYSU={rR0&gH5s3FT2Z2Qb4Y$}ipdr~|(hY6gP3it8?7U`RW zgqWSQLD@fTPmY_}I;mV?N)$|Ngb6yjyZh{PbaHtYCWoUm>HJhDwL|m+Y@yL;5Mm0S zm71FPDJe*rq!^KD;qB$O1IIJ!GF&e!ioy^eO+q1rN8E1@s*NNKkfOQi4$*N8lu}BP zx1`gONK%N5i%D575rRqs5fq5r1&AvTGw1{?iwrCr7E#(@v+lVlGbYH}tJE1Fx0@M) zIuGm54xI9ZlouQO^cyIOnM497$pHaL&EU%I1XY#i7Nn)jy8qmJLgvCn$3%RF{w{%) zL=%W2K&4bqLxiVB$s#YeYw?B)N(53-alUwVbanL_0dMLoDm2SqJMSqU1ri9dS1G@; z=b#dr%JRYzEqmBkLQ+cnZ-(^L!C+ZzSxD>&sw|5L%yCeoduAqCE>GDMgvEhF zxeC}^x{O5_L`0MF*e~GJ^z!oG3W`(|ZSxg``21(~2e<|-whDdZo1FZ}I$T7|&jUe+ z-sy(4`{14%1;?QBaPxwpi-96L=$zFa9zF+ggyJ=_pqTQ*&o?qgJy0(p^68^i0G`4Q zNs)-^I(WGG9D&VMR^C0!*Ra3?sg@EuIy#~|5cPy{Lc(EkclX}!;GipIp$f6=W&RLB zgq@Te|0T8~He>VN4_=ZUW6;o4DwxQ&2u#=miSTMtZbO0QMtE2^mxZY*;WaFDadvNN zz2jdHp!GA|vAFoBpMmycxH4EYQ9A?9Y6J*sQVGZ_YTeHIo^9x@AguKRTH}+1yh%W~ zcgAAWEh*%&Us4vwgtItxAulamu0naw%(Q&JO^?ruJ-Jv{tI(D0VH zDUZ7;t|;q`;JDbhsL~Jt;^vl;nvofH-uJ|+br|V+gH|E@o|=}t1_~mZNFWuVy#mSy zF+0iOe_ngO<2sXFVJ@sxs?@REq2t7uK^@Jp1yD_P5B~{P@!l&$CjeUA6XM}ux*;7V zpvLqx>e0yP=?0{`JE(Ajcfz#}j2=#OT8!Bjf*`CP4#t6jYz-}5*{y_#-BW2o3#o|EF zC(N8fWF=!9Z$Zou0eo`Yj;? zb^o!I@?=P~qp+(L#qC84#qb(XSZ8vZf>#-Vpyaz`duEZENzqDG*?;rwGi4dZU!8M- zlHgGjjwcM_DV&^9SmOAgJ8EEqOzi=*7gndpGOikq*W&Px6g0I6dKbzQ+mq-zr2>(; z`-(gdZAsNk#T=8|@I|iFu$Ys$IZe6j?%^$hfui^uawVWJm^5o}RK-gTq3A(JIrk7s zVYLgC5n*6m9KVIXiViKZqFAn$M1eW+(6pjG#V+XCSdx#eqCjr&a#zu~&ZQ!G?I7!l zJRd@i0r9y&&0s6d2GgbB03q)J5%&{|;c$!B7hvHyer&@;_T``~$&O;8SgR_xe32{I z5kw(<@q_l}JbjQ^^qj;tTADGjRs*-Gdr}_kBzPc-TC~-8+!om=dn2|0v54k03UFq& zHQ64ew7lr9r)nwZ5SyEKu+j2*at2Z3BLOn~OhC7LwMj0G@*Dl3id|f0(8{oWb6)7l^aRTOC3bN=%(!Is8x7P4BbVZ6cFbMD# ztv2PD;$iRHgfM^2WU|eN<)Dcu#`+Mou8qlWK+$fP9^|{iX`YR&603scb+EzQP%LU< zD=+3DDfE>mOBV)1`X-5hB>BO95oj1j#b!1?>se7$k=sCgmz9)dV4tNgAin@y+-V~# zGl;MmJL(BA-SpR=EWS z#LO~FzKmF7G(%mlDv@C5!(;#YLmV77km6A!?gF~Gdi(&^F7nt2DHBIieuC>~5~DHD zq*>UHSRf&=c&tMta?(Z0isxbgo{XJcT{WT`%-q7X=8U+wr~R@age2SU)gzIb9C1KjP(UYI0{pa$*)sU+I1X}< zGP_ zA14T+;x5i3Rz~z?G$?YfmzUpknpRxMf`$agNg2)19rmrKKwRA;qM21VCMIe-$^ixy z@b?lFiXEcF5bolJ4{eqbMVvJ-4h~dM7Oo)ne$CP$NKqbYdlvw~yvVIX$Cl`2;H{2m}AhlhOcyhlwCrJvxX#{aB?AP17 z^l5yB*%(32XC_!A*g{@N$e`bC*>}cV>H8KnHzJCb0u>vOcM$v5oeNsIDx68q?MkFV ztS~2mqij4Sa1l;kZ3lau7vGU&2>?`@+veqOB)E<`CvTjf55po|<3u?rd7}6nVh_5EImE#AheV{>r9iR~vCfI$8{9|?J``Ux{sNmR2A%mq zKDbM4_VS375r#8_?g;HSK|dQ<#!NYdokJom0hGBHUVuicb!^0maf~mMEQ=$`T(Bqi zsJA92#LU2)mw<&IF;-!oPQFz)QJGoen4$kH5G}^k@I^3O}o}5kdf>Bb?<7P1PI%w=N9QCWtTxVvsVzdsd?GOJp5Nuuplk)^3pveV4%!%8d4j>wM zK_>oc2yAAMg#{j9h{ruZ71$zB>KwF=6)>>}GUbq^xO+#J3Uv*Du^vs=6qO9UznQ)K@f6aP(dnY>_B6md4(jHL_l~$5*ij4GBoZXAdh7X*C-JOKg)Rata zJkd+x)&qV+ON7I(oMahI1%!pVh5l|)1aLi6V0#s|8~8~@VaJ$7ASLjp#1Y6OqN(oD2l#i%y-GN{-Lu=5b`WMABV6tZ4Y+2nRy;HJb72sRM~G@>XXwMl1f5a9+E zK`EV}2vP;5$p}_X$8i#fo|G&^uw%h;={R5nPb2{m?kpjobOjtz^5oGu$OyCG1jKML zXcsbzXJ~M=PG<4>vR5rnV@zLn_90hFkWjeBy1br@+$NQ&iYuL4sqna7t#AXk`) z@iT77kmBSr3T`Nuh4?UMA}n!|m{Dt|h1OQ!e6mZHgUKld4tlyyr!e6Y*KBMr&|o+6 zq!e8!U>{jBWn5f5l#ZrsHC#M3SX2##CCW{FN#F$e=mIJO;*?7C=KxKuM3lW7kbPC8 zZhWpGQa%>Bq|*x_kOEz{|41oVagjQ>;w+aFs6p@H;r%h+15s|IM#?AJQ}Tp(pi?|r z3sz-4hV%&sh?c>KEKU|>zica0A1ly4WEtfl+ap3OiUD_5uMjAZ1~L%F7`cJOx=eYC zEZ2CkYu;@Cp*;uB0W$HkEe1d-yg6{blM~%#IYIV~=CN1?Nu{QU`clZ_0XVXhNws0J zONNF>bud~jW{nJs>?^gCl#yJXWwJW{i&u==7?noK#QGR#9Z=b+R8R7a zIXRhPSWe256Yr@USV9$4Hx>?*&@yPCzQH^0Ope8l+ZY302x#$AyJ z6u146fZ=$^Hn74P=h8U#Lt0u(+f(Znl%DgnFt1gaCF;r z3y8q`{1Fh=A%gHp)@{H`g$Sre%F7!WeQW%v1b)~oAu-wo_T0Q%q=C8B;QxXSeBi5{7b%CtMAgx#oFL{VEIyMrAv`wEuE_oxciO*g0 zD2VMD`Gwi?DK_nw0Wp8UKn^4e=OxAqRoKUgn*bZ{INm))8}WAqjGlcp}{>!mu2?}bks}PqEy$r|{<9*PI z0Yt2IVm%nce}es!X%_LvcCcHID%SNR{~5?nrrBi3O#7C|E68#b6*!2C6S#t-bsr^p zua1d%Qj+kjVSQD)RQVP}A}L0xP%{{C1$h#w3?C=*18jHe2D~Q%vyH~<&JZmKpRs5k z(Dh#&ItNQh6ipT`w~z1 zZ3Av#^GCzPS&7sFV;1bmzPz9uB5=IIPMWZ!eTfWgc>Cgglqh?EXVjO02?a~;Bvr}DTKCFj&$|M-u|V8bp1bB^S$#V3yk;-_?q5YHS!A9xAmD6OgPq*Y-`)%)`RpgoBMVd=zOr7Yb`eJ+`fCeU2TSb F{(o;8g_HmQ literal 0 HcmV?d00001 diff --git a/Project/Vision/UI/capture_onion_selected@2x.png b/Project/Vision/UI/capture_onion_selected@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c10add4b4d5d4bf73f2c6c8dc1ae22d1dc0224d8 GIT binary patch literal 28469 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 literal 0 HcmV?d00001 diff --git a/Project/Vision/Vision-Info.plist b/Project/Vision/Vision-Info.plist index 0394c8b..62877c5 100644 --- a/Project/Vision/Vision-Info.plist +++ b/Project/Vision/Vision-Info.plist @@ -31,8 +31,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight diff --git a/Source/PBJVision.h b/Source/PBJVision.h index bcccc02..b3d495f 100644 --- a/Source/PBJVision.h +++ b/Source/PBJVision.h @@ -45,6 +45,7 @@ extern NSString * const PBJVisionPhotoThumbnailKey; // 160x120 extern NSString * const PBJVisionVideoPathKey; extern NSString * const PBJVisionVideoThumbnailKey; +@class EAGLContext; @protocol PBJVisionDelegate; @interface PBJVision : NSObject { @@ -89,6 +90,9 @@ extern NSString * const PBJVisionVideoThumbnailKey; @property (nonatomic, readonly) BOOL supportsVideoCapture; @property (nonatomic, readonly) BOOL canCaptureVideo; +@property (nonatomic, getter=isVideoRenderingEnabled) BOOL videoRenderingEnabled; +@property (nonatomic, readonly) EAGLContext *context; +@property (nonatomic) CGRect presentationFrame; - (void)startVideoCapture; - (void)pauseVideoCapture; diff --git a/Source/PBJVision.m b/Source/PBJVision.m index 61722d5..cad48bd 100644 --- a/Source/PBJVision.m +++ b/Source/PBJVision.m @@ -11,6 +11,7 @@ #import #import #import +#import #define LOG_VISION 0 #if !defined(NDEBUG) && LOG_VISION @@ -39,6 +40,26 @@ NSString * const PBJVisionVideoPathKey = @"PBJVisionVideoPathKey"; NSString * const PBJVisionVideoThumbnailKey = @"PBJVisionVideoThumbnailKey"; +// buffer rendering shader uniforms and attributes +// TODO: create an abstraction for shaders + +enum +{ + PBJVisionUniformY, + PBJVisionUniformUV, + PBJVisionUniformCount +}; +GLint _uniforms[PBJVisionUniformCount]; + +enum +{ + PBJVisionAttributeVertex, + PBJVisionAttributeTextureCoord, + PBJVisionAttributeCount +}; + +/// + @interface PBJVision () < AVCaptureAudioDataOutputSampleBufferDelegate, AVCaptureVideoDataOutputSampleBufferDelegate> @@ -84,7 +105,21 @@ @interface PBJVision () < CMTime _timeOffset; CMTime _audioTimestamp; CMTime _videoTimestamp; - + + // sample buffer rendering + + PBJCameraDevice _bufferDevice; + PBJCameraOrientation _bufferOrientation; + size_t _bufferWidth; + size_t _bufferHeight; + CGRect _presentationFrame; + + EAGLContext *_context; + GLuint _program; + CVOpenGLESTextureRef _lumaTexture; + CVOpenGLESTextureRef _chromaTexture; + CVOpenGLESTextureCacheRef _videoTextureCache; + // flags struct { @@ -96,6 +131,7 @@ @interface PBJVision () < unsigned int paused:1; unsigned int interrupted:1; unsigned int videoWritten:1; + unsigned int videoRenderingEnabled:1; } __block _flags; } @@ -110,6 +146,8 @@ @implementation PBJVision @synthesize cameraMode = _cameraMode; @synthesize cameraOrientation = _cameraOrientation; @synthesize focusMode = _focusMode; +@synthesize context = _context; +@synthesize presentationFrame = _presentationFrame; #pragma mark - singleton @@ -139,6 +177,16 @@ - (BOOL)isRecording return isRecording; } +- (void)setVideoRenderingEnabled:(BOOL)videoRenderingEnabled +{ + _flags.videoRenderingEnabled = (unsigned int)videoRenderingEnabled; +} + +- (BOOL)isVideoRenderingEnabled +{ + return _flags.videoRenderingEnabled; +} + - (void)_setOrientationForConnection:(AVCaptureConnection *)connection { if (!connection) @@ -219,6 +267,12 @@ - (id)init { self = [super init]; if (self) { + _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; + if (!_context) { + DLog(@"failed to create GL context"); + } + [self _setupGL]; + _captureSessionDispatchQueue = dispatch_queue_create("PBJVisionSession", DISPATCH_QUEUE_SERIAL); // protects session _captureVideoDispatchQueue = dispatch_queue_create("PBJVisionVideo", DISPATCH_QUEUE_SERIAL); // protects capture _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:nil]; @@ -233,6 +287,15 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; _delegate = nil; + + [self _cleanUpTextures]; + + if (_videoTextureCache) { + CFRelease(_videoTextureCache); + _videoTextureCache = NULL; + } + + [self _destroyGL]; } #pragma mark - queue helper methods @@ -269,6 +332,15 @@ - (void)_setupCamera { if (_captureSession) return; + +#if COREVIDEO_USE_EAGLCONTEXT_CLASS_IN_API + CVReturn cvError = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_videoTextureCache); +#else + CVReturn cvError = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, (__bridge void *)_context, NULL, &_videoTextureCache); +#endif + if (cvError) { + NSLog(@"error CVOpenGLESTextureCacheCreate (%d)", cvError); + } _captureSession = [[AVCaptureSession alloc] init]; @@ -1220,10 +1292,118 @@ - (CMSampleBufferRef)_createOffsetSampleBuffer:(CMSampleBufferRef)sampleBuffer w return outputSampleBuffer; } -- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection +#pragma mark - sample buffer processing + +- (void)_cleanUpTextures { - // TODO: save the last frame for onion skinning + CVOpenGLESTextureCacheFlush(_videoTextureCache, 0); + + if (_lumaTexture) { + CFRelease(_lumaTexture); + _lumaTexture = NULL; + } + + if (_chromaTexture) { + CFRelease(_chromaTexture); + _chromaTexture = NULL; + } +} +// convert CoreVideo YUV pixel buffer (Y luminance and Cb Cr chroma) into RGB +// processing is done on the GPU, operation WAY more efficient than converting .on the CPU +- (void)_processSampleBuffer:(CMSampleBufferRef)sampleBuffer +{ + if (!_context) + return; + + if (!_videoTextureCache) + return; + + CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + + if (CVPixelBufferLockBaseAddress(imageBuffer, 0) != kCVReturnSuccess) + return; + + [EAGLContext setCurrentContext:_context]; + + [self _cleanUpTextures]; + + size_t width = CVPixelBufferGetWidth(imageBuffer); + size_t height = CVPixelBufferGetHeight(imageBuffer); + + // only bind the vertices once or if parameters change + + if (_bufferWidth != width || + _bufferHeight != height || + _bufferDevice != _cameraDevice || + _bufferOrientation != _cameraOrientation) { + + _bufferWidth = width; + _bufferHeight = height; + _bufferDevice = _cameraDevice; + _bufferOrientation = _cameraOrientation; + [self _setupBuffers]; + + } + + // always upload the texturs since the input may be changing + + CVReturn error = 0; + + // Y-plane + glActiveTexture(GL_TEXTURE0); + error = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, + _videoTextureCache, + imageBuffer, + NULL, + GL_TEXTURE_2D, + GL_RED_EXT, + (GLsizei)_bufferWidth, + (GLsizei)_bufferHeight, + GL_RED_EXT, + GL_UNSIGNED_BYTE, + 0, + &_lumaTexture); + if (error) { + DLog(@"error CVOpenGLESTextureCacheCreateTextureFromImage (%d)", error); + } + + glBindTexture(CVOpenGLESTextureGetTarget(_lumaTexture), CVOpenGLESTextureGetName(_lumaTexture)); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // UV-plane + glActiveTexture(GL_TEXTURE1); + error = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, + _videoTextureCache, + imageBuffer, + NULL, + GL_TEXTURE_2D, + GL_RG_EXT, + (GLsizei)(_bufferWidth * 0.5), + (GLsizei)(_bufferHeight * 0.5), + GL_RG_EXT, + GL_UNSIGNED_BYTE, + 1, + &_chromaTexture); + if (error) { + DLog(@"error CVOpenGLESTextureCacheCreateTextureFromImage (%d)", error); + } + + glBindTexture(CVOpenGLESTextureGetTarget(_chromaTexture), CVOpenGLESTextureGetName(_chromaTexture)); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + if (CVPixelBufferUnlockBaseAddress(imageBuffer, 0) != kCVReturnSuccess) + return; + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); +} + +#pragma mark - AVCaptureAudioDataOutputSampleBufferDelegate, AVCaptureVideoDataOutputSampleBufferDelegate + +- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection +{ CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer); CFRetain(sampleBuffer); CFRetain(formatDescription); @@ -1314,6 +1494,14 @@ - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CM _videoTimestamp = time; _flags.videoWritten = YES; } + + // process the sample buffer for rendering + if (_flags.videoRenderingEnabled && _flags.videoWritten) { + [self _executeBlockOnMainQueue:^{ + [self _processSampleBuffer:bufferToWrite]; + }]; + } + CFRelease(bufferToWrite); } @@ -1541,5 +1729,188 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N } } +#pragma mark - OpenGLES context support + +- (void)_setupBuffers +{ + +// unit square for testing +// static const GLfloat unitSquareVertices[] = { +// -1.0f, -1.0f, +// 1.0f, -1.0f, +// -1.0f, 1.0f, +// 1.0f, 1.0f, +// }; + + CGSize inputSize = CGSizeMake(_bufferWidth, _bufferHeight); + CGRect insetRect = AVMakeRectWithAspectRatioInsideRect(inputSize, _presentationFrame); + + CGFloat widthScale = CGRectGetHeight(_presentationFrame) / CGRectGetHeight(insetRect); + CGFloat heightScale = CGRectGetWidth(_presentationFrame) / CGRectGetWidth(insetRect); + + static GLfloat vertices[8]; + + vertices[0] = -widthScale; + vertices[1] = -heightScale; + vertices[2] = widthScale; + vertices[3] = -heightScale; + vertices[4] = -widthScale; + vertices[5] = heightScale; + vertices[6] = widthScale; + vertices[7] = heightScale; + + static const GLfloat textureCoordinates[] = { + 0.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f, + }; + + static const GLfloat textureCoordinatesVerticalFlip[] = { + 1.0f, 1.0f, + 0.0f, 1.0f, + 1.0f, 0.0f, + 0.0f, 0.0f, + }; + + glEnableVertexAttribArray(PBJVisionAttributeVertex); + glVertexAttribPointer(PBJVisionAttributeVertex, 2, GL_FLOAT, GL_FALSE, 0, vertices); + + if (_cameraDevice == PBJCameraDeviceFront) { + glEnableVertexAttribArray(PBJVisionAttributeTextureCoord); + glVertexAttribPointer(PBJVisionAttributeTextureCoord, 2, GL_FLOAT, GL_FALSE, 0, textureCoordinatesVerticalFlip); + } else { + glEnableVertexAttribArray(PBJVisionAttributeTextureCoord); + glVertexAttribPointer(PBJVisionAttributeTextureCoord, 2, GL_FLOAT, GL_FALSE, 0, textureCoordinates); + } +} + +- (void)_setupGL +{ + [EAGLContext setCurrentContext:_context]; + + [self _loadShaders]; + + glUseProgram(_program); + + glUniform1i(_uniforms[PBJVisionUniformY], 0); + glUniform1i(_uniforms[PBJVisionUniformUV], 1); +} + +- (void)_destroyGL +{ + [EAGLContext setCurrentContext:_context]; + + if (_program) { + glDeleteProgram(_program); + _program = 0; + } + + if ([EAGLContext currentContext] == _context) { + [EAGLContext setCurrentContext:nil]; + } +} + +#pragma mark - OpenGLES shader support +// TODO: abstract this in future + +- (BOOL)_loadShaders +{ + GLuint vertShader; + GLuint fragShader; + NSString *vertShaderName; + NSString *fragShaderName; + + _program = glCreateProgram(); + + vertShaderName = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"vsh"]; + if (![self _compileShader:&vertShader type:GL_VERTEX_SHADER file:vertShaderName]) { + DLog(@"failed to compile vertex shader"); + return NO; + } + + fragShaderName = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"fsh"]; + if (![self _compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragShaderName]) { + DLog(@"failed to compile fragment shader"); + return NO; + } + + glAttachShader(_program, vertShader); + glAttachShader(_program, fragShader); + + glBindAttribLocation(_program, PBJVisionAttributeVertex, "a_position"); + glBindAttribLocation(_program, PBJVisionAttributeTextureCoord, "a_texture"); + + if (![self _linkProgram:_program]) { + DLog(@"failed to link program, %d", _program); + + if (vertShader) { + glDeleteShader(vertShader); + vertShader = 0; + } + if (fragShader) { + glDeleteShader(fragShader); + fragShader = 0; + } + if (_program) { + glDeleteProgram(_program); + _program = 0; + } + + return NO; + } + + _uniforms[PBJVisionUniformY] = glGetUniformLocation(_program, "u_samplerY"); + _uniforms[PBJVisionUniformUV] = glGetUniformLocation(_program, "u_samplerUV"); + + if (vertShader) { + glDetachShader(_program, vertShader); + glDeleteShader(vertShader); + } + if (fragShader) { + glDetachShader(_program, fragShader); + glDeleteShader(fragShader); + } + + return YES; +} + +- (BOOL)_compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file +{ + GLint status; + const GLchar *source; + + source = (GLchar *)[[NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil] UTF8String]; + if (!source) { + DLog(@"failed to load vertex shader"); + return NO; + } + + *shader = glCreateShader(type); + glShaderSource(*shader, 1, &source, NULL); + glCompileShader(*shader); + + glGetShaderiv(*shader, GL_COMPILE_STATUS, &status); + if (status == 0) { + glDeleteShader(*shader); + return NO; + } + + return YES; +} + +- (BOOL)_linkProgram:(GLuint)prog +{ + GLint status; + glLinkProgram(prog); + + glGetProgramiv(prog, GL_LINK_STATUS, &status); + if (status == 0) { + return NO; + } + + return YES; +} + @end diff --git a/Source/Shaders/Shader.fsh b/Source/Shaders/Shader.fsh new file mode 100644 index 0000000..caacd4d --- /dev/null +++ b/Source/Shaders/Shader.fsh @@ -0,0 +1,22 @@ + +uniform sampler2D u_samplerY; +uniform sampler2D u_samplerUV; + +varying highp vec2 v_texture; + +void main() +{ + mediump vec3 yuv; + lowp vec3 rgb; + + yuv.x = texture2D(u_samplerY, v_texture).r; + yuv.yz = texture2D(u_samplerUV, v_texture).rg - vec2(0.5, 0.5); + + // BT.709, the standard for HDTV + rgb = mat3( 1, 1, 1, + 0, -.18732, 1.8556, + 1.57481, -.46813, 0) * yuv; + + gl_FragColor = vec4(rgb, 1); +} + diff --git a/Source/Shaders/Shader.vsh b/Source/Shaders/Shader.vsh new file mode 100644 index 0000000..547df06 --- /dev/null +++ b/Source/Shaders/Shader.vsh @@ -0,0 +1,11 @@ + +attribute vec4 a_position; +attribute vec2 a_texture; + +varying vec2 v_texture; + +void main() +{ + v_texture = a_texture; + gl_Position = a_position; +}