diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3709ecf --- /dev/null +++ b/.gitignore @@ -0,0 +1,97 @@ +.DS_Store +_local/* +Build/* +release.sh +Frameworks/* +!Frameworks/.gitkeep + +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ diff --git a/Frameworks/.gitkeep b/Frameworks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Imago.xcodeproj/project.pbxproj b/Imago.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0dd6a21 --- /dev/null +++ b/Imago.xcodeproj/project.pbxproj @@ -0,0 +1,1176 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + B111517624BD621D009CA476 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1C6F8A124B3F3D5009D98C0 /* Cocoa.framework */; }; + B111517724BD621D009CA476 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B177492524B0FAE4007103FB /* CoreMedia.framework */; }; + B111517824BD621D009CA476 /* CoreImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B10F711B24BD323C008276A7 /* CoreImage.framework */; }; + B111517924BD621D009CA476 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B177492324B0FAE4007103FB /* CoreGraphics.framework */; }; + B111517A24BD621D009CA476 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B177492124B0FAD1007103FB /* AVFoundation.framework */; }; + B111517B24BD621D009CA476 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B177492424B0FAE4007103FB /* CoreVideo.framework */; }; + B111517C24BD621D009CA476 /* CoreMediaIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B177492624B0FAE5007103FB /* CoreMediaIO.framework */; }; + B111519524BD6759009CA476 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = B111518B24BD6758009CA476 /* Device.swift */; }; + B111519624BD6759009CA476 /* Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = B111518D24BD6758009CA476 /* Stream.swift */; }; + B111519724BD6759009CA476 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = B111518E24BD6758009CA476 /* Main.swift */; }; + B111519924BD6759009CA476 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B111519024BD6758009CA476 /* Plugin.swift */; }; + B111519A24BD6759009CA476 /* PluginInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = B111519124BD6758009CA476 /* PluginInterface.swift */; }; + B111519B24BD6759009CA476 /* Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = B111519224BD6758009CA476 /* Property.swift */; }; + B111519D24BD6759009CA476 /* Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = B111519424BD6758009CA476 /* Object.swift */; }; + B1499C7724D1D9B300FC3557 /* Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1499C7624D1D9B300FC3557 /* Stream.swift */; }; + B15B113624CB61B6002703FF /* PubSub.swift in Sources */ = {isa = PBXBuildFile; fileRef = B15B113524CB61B6002703FF /* PubSub.swift */; }; + B15B113724CB61B6002703FF /* PubSub.swift in Sources */ = {isa = PBXBuildFile; fileRef = B15B113524CB61B6002703FF /* PubSub.swift */; }; + B17748B524B03497007103FB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17748B424B03497007103FB /* AppDelegate.swift */; }; + B17748B724B03497007103FB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17748B624B03497007103FB /* ContentView.swift */; }; + B17748B924B03498007103FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B17748B824B03498007103FB /* Assets.xcassets */; }; + B17748BC24B03498007103FB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B17748BB24B03498007103FB /* Preview Assets.xcassets */; }; + B17748CB24B03498007103FB /* ImagoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17748CA24B03498007103FB /* ImagoTests.swift */; }; + B17748D624B03498007103FB /* ImagoUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17748D524B03498007103FB /* ImagoUITests.swift */; }; + B17748E724B03542007103FB /* Camera.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17748E424B03542007103FB /* Camera.swift */; }; + B17748F224B039B2007103FB /* EDSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B17748F024B0390D007103FB /* EDSDK.framework */; }; + B17748F324B039B2007103FB /* EDSDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B17748F024B0390D007103FB /* EDSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B18149F224B61F86004CD8A8 /* CanonCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = B18149F124B61F86004CD8A8 /* CanonCamera.swift */; }; + B18149F924B67E6E004CD8A8 /* CanonCommandErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = B18149F824B67E6D004CD8A8 /* CanonCommandErrors.swift */; }; + B18149FB24B68094004CD8A8 /* CanonCommandProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = B18149FA24B68094004CD8A8 /* CanonCommandProperty.swift */; }; + B183883424DDC18E007981BB /* CameraFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183883324DDC18E007981BB /* CameraFilter.swift */; }; + B183884624E1A829007981BB /* CVPixelBufferPool+Imago.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183884524E1A829007981BB /* CVPixelBufferPool+Imago.swift */; }; + B183884724E1A82A007981BB /* CVPixelBufferPool+Imago.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183884524E1A829007981BB /* CVPixelBufferPool+Imago.swift */; }; + B183884924E1B806007981BB /* TitlebarAccesory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183884824E1B806007981BB /* TitlebarAccesory.swift */; }; + B183884C24E1B853007981BB /* CameraFilterMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183884B24E1B853007981BB /* CameraFilterMenu.swift */; }; + B183884E24E1B8FB007981BB /* LivePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183884D24E1B8FB007981BB /* LivePreview.swift */; }; + B183885324E1BC07007981BB /* CIImage+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183885224E1BC07007981BB /* CIImage+Filter.swift */; }; + B183885524E1BD63007981BB /* CIImage+Imago.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D0889224D1F75A0001B8E9 /* CIImage+Imago.swift */; }; + B183885724E1C340007981BB /* CameraRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B183885624E1C340007981BB /* CameraRow.swift */; }; + B183885F24E30615007981BB /* libturbojpeg.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = B183885E24E30614007981BB /* libturbojpeg.0.dylib */; }; + B183886024E30615007981BB /* libturbojpeg.0.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B183885E24E30614007981BB /* libturbojpeg.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + B1B8AD0824C61953009470CA /* Frame+Decompress.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C5577724C53AE7009355F1 /* Frame+Decompress.swift */; }; + B1B8AD0B24C61E9E009470CA /* Const.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B8AD0924C61E8F009470CA /* Const.swift */; }; + B1B8AD0F24C620C6009470CA /* DLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B8AD0E24C620C6009470CA /* DLog.swift */; }; + B1B8AD1024C620C6009470CA /* DLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B8AD0E24C620C6009470CA /* DLog.swift */; }; + B1B8AD1324C621A4009470CA /* ImagoStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B8AD1124C621A4009470CA /* ImagoStream.swift */; }; + B1B8AD1424C64416009470CA /* Const.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B8AD0924C61E8F009470CA /* Const.swift */; }; + B1B8AD4B24C756AE009470CA /* CallerID.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B8AD4A24C756AE009470CA /* CallerID.swift */; }; + B1B8AD4C24C756AE009470CA /* CallerID.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B8AD4A24C756AE009470CA /* CallerID.swift */; }; + B1C297A724CF870B0089C84D /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C297A624CF870B0089C84D /* RepeatingTimer.swift */; }; + B1C297A824CF870B0089C84D /* RepeatingTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C297A624CF870B0089C84D /* RepeatingTimer.swift */; }; + B1C5576F24C0B59A009355F1 /* Frame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C5576E24C0B59A009355F1 /* Frame.swift */; }; + B1C5577024C0B59A009355F1 /* Frame.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C5576E24C0B59A009355F1 /* Frame.swift */; }; + B1D0889024D1F1060001B8E9 /* CVPixelBuffer+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D0888F24D1F1060001B8E9 /* CVPixelBuffer+Data.swift */; }; + B1D0889124D1F1060001B8E9 /* CVPixelBuffer+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D0888F24D1F1060001B8E9 /* CVPixelBuffer+Data.swift */; }; + B1D0889324D1F75A0001B8E9 /* CIImage+Imago.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D0889224D1F75A0001B8E9 /* CIImage+Imago.swift */; }; + B1D0889624D221160001B8E9 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D0889524D221160001B8E9 /* ViewController.swift */; }; + B1DF8B7024BD7FDB007ED34C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B1DF8B6E24BD7FDB007ED34C /* Main.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + B17748C724B03498007103FB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B17748A924B03497007103FB /* Project object */; + proxyType = 1; + remoteGlobalIDString = B17748B024B03497007103FB; + remoteInfo = Imago; + }; + B17748D224B03498007103FB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B17748A924B03497007103FB /* Project object */; + proxyType = 1; + remoteGlobalIDString = B17748B024B03497007103FB; + remoteInfo = Imago; + }; + B183883624DF377A007981BB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B17748A924B03497007103FB /* Project object */; + proxyType = 1; + remoteGlobalIDString = B111516B24BD621D009CA476; + remoteInfo = ImagoPlugin; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + B17748EF24B035B5007103FB /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + B17748F324B039B2007103FB /* EDSDK.framework in Embed Frameworks */, + B183886024E30615007981BB /* libturbojpeg.0.dylib in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + B10F711B24BD323C008276A7 /* CoreImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreImage.framework; path = System/Library/Frameworks/CoreImage.framework; sourceTree = SDKROOT; }; + B111518324BD621D009CA476 /* ImagoPlugin.plugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImagoPlugin.plugin; sourceTree = BUILT_PRODUCTS_DIR; }; + B111518924BD630D009CA476 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B111518B24BD6758009CA476 /* Device.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Device.swift; path = ../Device.swift; sourceTree = ""; }; + B111518D24BD6758009CA476 /* Stream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Stream.swift; path = ../Stream.swift; sourceTree = ""; }; + B111518E24BD6758009CA476 /* Main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Main.swift; path = ../Main.swift; sourceTree = ""; }; + B111519024BD6758009CA476 /* Plugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Plugin.swift; path = ../Plugin.swift; sourceTree = ""; }; + B111519124BD6758009CA476 /* PluginInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PluginInterface.swift; path = ../PluginInterface.swift; sourceTree = ""; }; + B111519224BD6758009CA476 /* Property.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Property.swift; path = ../Property.swift; sourceTree = ""; }; + B111519424BD6758009CA476 /* Object.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Object.swift; path = ../Object.swift; sourceTree = ""; }; + B111519F24BD6B13009CA476 /* PrefixHeader.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrefixHeader.pch; sourceTree = ""; }; + B11151A024BD6B13009CA476 /* ImagoPlugin-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ImagoPlugin-Bridging-Header.h"; sourceTree = ""; }; + B142810F24B8C0CF00F50FCD /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + B1499C7624D1D9B300FC3557 /* Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = ""; }; + B15B113524CB61B6002703FF /* PubSub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubSub.swift; sourceTree = ""; }; + B17748B124B03497007103FB /* Imago.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Imago.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B17748B424B03497007103FB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + B17748B624B03497007103FB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + B17748B824B03498007103FB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B17748BB24B03498007103FB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + B17748C024B03498007103FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B17748C124B03498007103FB /* Imago.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Imago.entitlements; sourceTree = ""; }; + B17748C624B03498007103FB /* ImagoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImagoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B17748CA24B03498007103FB /* ImagoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagoTests.swift; sourceTree = ""; }; + B17748CC24B03498007103FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B17748D124B03498007103FB /* ImagoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImagoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B17748D524B03498007103FB /* ImagoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagoUITests.swift; sourceTree = ""; }; + B17748D724B03498007103FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B17748E424B03542007103FB /* Camera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Camera.swift; sourceTree = ""; }; + B17748E924B0358F007103FB /* Imago-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Imago-Bridging-Header.h"; sourceTree = ""; }; + B17748EA24B03590007103FB /* Imago-PrefixHeader.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Imago-PrefixHeader.pch"; sourceTree = ""; }; + B17748F024B0390D007103FB /* EDSDK.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = EDSDK.framework; path = Frameworks/EDSDK/EDSDK.framework; sourceTree = ""; }; + B177491E24B0F9AF007103FB /* libturbojpeg.0.2.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libturbojpeg.0.2.0.dylib; path = Frameworks/libturbojpeg.0.2.0.dylib; sourceTree = ""; }; + B177492124B0FAD1007103FB /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + B177492324B0FAE4007103FB /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + B177492424B0FAE4007103FB /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; + B177492524B0FAE4007103FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + B177492624B0FAE5007103FB /* CoreMediaIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMediaIO.framework; path = System/Library/Frameworks/CoreMediaIO.framework; sourceTree = SDKROOT; }; + B18149F124B61F86004CD8A8 /* CanonCamera.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanonCamera.swift; sourceTree = ""; }; + B18149F824B67E6D004CD8A8 /* CanonCommandErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanonCommandErrors.swift; sourceTree = ""; }; + B18149FA24B68094004CD8A8 /* CanonCommandProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanonCommandProperty.swift; sourceTree = ""; }; + B183883324DDC18E007981BB /* CameraFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraFilter.swift; sourceTree = ""; }; + B183884324E19A2B007981BB /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + B183884424E1A544007981BB /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + B183884524E1A829007981BB /* CVPixelBufferPool+Imago.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CVPixelBufferPool+Imago.swift"; sourceTree = ""; }; + B183884824E1B806007981BB /* TitlebarAccesory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarAccesory.swift; sourceTree = ""; }; + B183884B24E1B853007981BB /* CameraFilterMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraFilterMenu.swift; sourceTree = ""; }; + B183884D24E1B8FB007981BB /* LivePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePreview.swift; sourceTree = ""; }; + B183885224E1BC07007981BB /* CIImage+Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CIImage+Filter.swift"; sourceTree = ""; }; + B183885624E1C340007981BB /* CameraRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraRow.swift; sourceTree = ""; }; + B183885E24E30614007981BB /* libturbojpeg.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libturbojpeg.0.dylib; path = Frameworks/libturbojpeg.0.dylib; sourceTree = ""; }; + B1B8AD0924C61E8F009470CA /* Const.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Const.swift; sourceTree = ""; }; + B1B8AD0E24C620C6009470CA /* DLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DLog.swift; sourceTree = ""; }; + B1B8AD1124C621A4009470CA /* ImagoStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ImagoStream.swift; path = ../Shared/ImagoStream.swift; sourceTree = ""; }; + B1B8AD4A24C756AE009470CA /* CallerID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallerID.swift; sourceTree = ""; }; + B1C297A624CF870B0089C84D /* RepeatingTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatingTimer.swift; sourceTree = ""; }; + B1C5576E24C0B59A009355F1 /* Frame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Frame.swift; sourceTree = ""; }; + B1C5577724C53AE7009355F1 /* Frame+Decompress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Frame+Decompress.swift"; sourceTree = ""; }; + B1C6F8A124B3F3D5009D98C0 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + B1D0888F24D1F1060001B8E9 /* CVPixelBuffer+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CVPixelBuffer+Data.swift"; sourceTree = ""; }; + B1D0889224D1F75A0001B8E9 /* CIImage+Imago.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CIImage+Imago.swift"; sourceTree = ""; }; + B1D0889524D221160001B8E9 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + B1DF8B6F24BD7FDB007ED34C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B111517524BD621D009CA476 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B111517624BD621D009CA476 /* Cocoa.framework in Frameworks */, + B111517724BD621D009CA476 /* CoreMedia.framework in Frameworks */, + B111517824BD621D009CA476 /* CoreImage.framework in Frameworks */, + B111517924BD621D009CA476 /* CoreGraphics.framework in Frameworks */, + B111517A24BD621D009CA476 /* AVFoundation.framework in Frameworks */, + B111517B24BD621D009CA476 /* CoreVideo.framework in Frameworks */, + B111517C24BD621D009CA476 /* CoreMediaIO.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B17748AE24B03497007103FB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B17748F224B039B2007103FB /* EDSDK.framework in Frameworks */, + B183885F24E30615007981BB /* libturbojpeg.0.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B17748C324B03498007103FB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B17748CE24B03498007103FB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B111518524BD6240009CA476 /* Tests */ = { + isa = PBXGroup; + children = ( + B17748D424B03498007103FB /* ImagoUITests */, + B17748C924B03498007103FB /* ImagoTests */, + ); + path = Tests; + sourceTree = ""; + }; + B111518624BD6259009CA476 /* Plugin */ = { + isa = PBXGroup; + children = ( + B111519E24BD6B13009CA476 /* Headers */, + B111518924BD630D009CA476 /* Info.plist */, + B1BD496124E34951000BD5C1 /* Base */, + B1B8AD1124C621A4009470CA /* ImagoStream.swift */, + B183884424E1A544007981BB /* LICENSE */, + ); + path = Plugin; + sourceTree = ""; + }; + B111519E24BD6B13009CA476 /* Headers */ = { + isa = PBXGroup; + children = ( + B111519F24BD6B13009CA476 /* PrefixHeader.pch */, + B11151A024BD6B13009CA476 /* ImagoPlugin-Bridging-Header.h */, + ); + path = Headers; + sourceTree = ""; + }; + B17748A824B03497007103FB = { + isa = PBXGroup; + children = ( + B142810F24B8C0CF00F50FCD /* README.md */, + B183884324E19A2B007981BB /* LICENSE */, + B1B8AD0D24C620C6009470CA /* Shared */, + B17748B324B03497007103FB /* Imago */, + B111518624BD6259009CA476 /* Plugin */, + B17748B224B03497007103FB /* Products */, + B17748EB24B035B5007103FB /* Frameworks */, + ); + sourceTree = ""; + }; + B17748B224B03497007103FB /* Products */ = { + isa = PBXGroup; + children = ( + B17748B124B03497007103FB /* Imago.app */, + B17748C624B03498007103FB /* ImagoTests.xctest */, + B17748D124B03498007103FB /* ImagoUITests.xctest */, + B111518324BD621D009CA476 /* ImagoPlugin.plugin */, + ); + name = Products; + sourceTree = ""; + }; + B17748B324B03497007103FB /* Imago */ = { + isa = PBXGroup; + children = ( + B17748C024B03498007103FB /* Info.plist */, + B1F0B10524B919FF00D9C1C1 /* Views */, + B17748B424B03497007103FB /* AppDelegate.swift */, + B1D0889524D221160001B8E9 /* ViewController.swift */, + B1499C7624D1D9B300FC3557 /* Stream.swift */, + B183885424E1BC1B007981BB /* Extensions */, + B183884F24E1BB10007981BB /* Camera */, + B1DF8B6C24BD7C29007ED34C /* Headers */, + B1DF8B6D24BD7C40007ED34C /* Assets */, + B17748BA24B03498007103FB /* Preview Content */, + B111518524BD6240009CA476 /* Tests */, + ); + path = Imago; + sourceTree = ""; + }; + B17748BA24B03498007103FB /* Preview Content */ = { + isa = PBXGroup; + children = ( + B17748BB24B03498007103FB /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + B17748C924B03498007103FB /* ImagoTests */ = { + isa = PBXGroup; + children = ( + B17748CA24B03498007103FB /* ImagoTests.swift */, + B17748CC24B03498007103FB /* Info.plist */, + ); + path = ImagoTests; + sourceTree = ""; + }; + B17748D424B03498007103FB /* ImagoUITests */ = { + isa = PBXGroup; + children = ( + B17748D524B03498007103FB /* ImagoUITests.swift */, + B17748D724B03498007103FB /* Info.plist */, + ); + path = ImagoUITests; + sourceTree = ""; + }; + B17748EB24B035B5007103FB /* Frameworks */ = { + isa = PBXGroup; + children = ( + B183885E24E30614007981BB /* libturbojpeg.0.dylib */, + B10F711B24BD323C008276A7 /* CoreImage.framework */, + B1C6F8A124B3F3D5009D98C0 /* Cocoa.framework */, + B177492324B0FAE4007103FB /* CoreGraphics.framework */, + B177492524B0FAE4007103FB /* CoreMedia.framework */, + B177492624B0FAE5007103FB /* CoreMediaIO.framework */, + B177492424B0FAE4007103FB /* CoreVideo.framework */, + B177492124B0FAD1007103FB /* AVFoundation.framework */, + B177491E24B0F9AF007103FB /* libturbojpeg.0.2.0.dylib */, + B17748F024B0390D007103FB /* EDSDK.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + B18149F524B62003004CD8A8 /* Canon */ = { + isa = PBXGroup; + children = ( + B18149F124B61F86004CD8A8 /* CanonCamera.swift */, + B18149F824B67E6D004CD8A8 /* CanonCommandErrors.swift */, + B18149FA24B68094004CD8A8 /* CanonCommandProperty.swift */, + ); + path = Canon; + sourceTree = ""; + }; + B183884F24E1BB10007981BB /* Camera */ = { + isa = PBXGroup; + children = ( + B17748E424B03542007103FB /* Camera.swift */, + B183883324DDC18E007981BB /* CameraFilter.swift */, + B18149F524B62003004CD8A8 /* Canon */, + ); + path = Camera; + sourceTree = ""; + }; + B183885024E1BB67007981BB /* Extensions */ = { + isa = PBXGroup; + children = ( + B1D0888F24D1F1060001B8E9 /* CVPixelBuffer+Data.swift */, + B183884524E1A829007981BB /* CVPixelBufferPool+Imago.swift */, + B1D0889224D1F75A0001B8E9 /* CIImage+Imago.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + B183885124E1BB80007981BB /* IPC */ = { + isa = PBXGroup; + children = ( + B1B8AD4A24C756AE009470CA /* CallerID.swift */, + B15B113524CB61B6002703FF /* PubSub.swift */, + ); + path = IPC; + sourceTree = ""; + }; + B183885424E1BC1B007981BB /* Extensions */ = { + isa = PBXGroup; + children = ( + B1C5577724C53AE7009355F1 /* Frame+Decompress.swift */, + B183885224E1BC07007981BB /* CIImage+Filter.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + B1B8AD0D24C620C6009470CA /* Shared */ = { + isa = PBXGroup; + children = ( + B1B8AD0924C61E8F009470CA /* Const.swift */, + B1B8AD0E24C620C6009470CA /* DLog.swift */, + B1C5576E24C0B59A009355F1 /* Frame.swift */, + B1C297A624CF870B0089C84D /* RepeatingTimer.swift */, + B183885124E1BB80007981BB /* IPC */, + B183885024E1BB67007981BB /* Extensions */, + ); + path = Shared; + sourceTree = ""; + }; + B1BD496124E34951000BD5C1 /* Base */ = { + isa = PBXGroup; + children = ( + B111518E24BD6758009CA476 /* Main.swift */, + B111519024BD6758009CA476 /* Plugin.swift */, + B111519124BD6758009CA476 /* PluginInterface.swift */, + B111518B24BD6758009CA476 /* Device.swift */, + B111519424BD6758009CA476 /* Object.swift */, + B111519224BD6758009CA476 /* Property.swift */, + B111518D24BD6758009CA476 /* Stream.swift */, + ); + path = Base; + sourceTree = ""; + }; + B1DF8B6C24BD7C29007ED34C /* Headers */ = { + isa = PBXGroup; + children = ( + B17748E924B0358F007103FB /* Imago-Bridging-Header.h */, + B17748EA24B03590007103FB /* Imago-PrefixHeader.pch */, + ); + path = Headers; + sourceTree = ""; + }; + B1DF8B6D24BD7C40007ED34C /* Assets */ = { + isa = PBXGroup; + children = ( + B1DF8B6E24BD7FDB007ED34C /* Main.storyboard */, + B17748C124B03498007103FB /* Imago.entitlements */, + B17748B824B03498007103FB /* Assets.xcassets */, + ); + path = Assets; + sourceTree = ""; + }; + B1F0B10524B919FF00D9C1C1 /* Views */ = { + isa = PBXGroup; + children = ( + B17748B624B03497007103FB /* ContentView.swift */, + B183885624E1C340007981BB /* CameraRow.swift */, + B183884B24E1B853007981BB /* CameraFilterMenu.swift */, + B183884824E1B806007981BB /* TitlebarAccesory.swift */, + B183884D24E1B8FB007981BB /* LivePreview.swift */, + ); + path = Views; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B111516B24BD621D009CA476 /* ImagoPlugin */ = { + isa = PBXNativeTarget; + buildConfigurationList = B111518024BD621D009CA476 /* Build configuration list for PBXNativeTarget "ImagoPlugin" */; + buildPhases = ( + B111516C24BD621D009CA476 /* Sources */, + B111517524BD621D009CA476 /* Frameworks */, + B1DF8B6B24BD6E06007ED34C /* Copy Plugin to System */, + B145826924E31D010035723E /* Update Build Version */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ImagoPlugin; + productName = "Imago DAL"; + productReference = B111518324BD621D009CA476 /* ImagoPlugin.plugin */; + productType = "com.apple.product-type.bundle"; + }; + B17748B024B03497007103FB /* Imago */ = { + isa = PBXNativeTarget; + buildConfigurationList = B17748DA24B03498007103FB /* Build configuration list for PBXNativeTarget "Imago" */; + buildPhases = ( + B145826824E315360035723E /* Code Sign Nested Bundles in EDSDK.framework */, + B17748AD24B03497007103FB /* Sources */, + B17748AE24B03497007103FB /* Frameworks */, + B17748AF24B03497007103FB /* Resources */, + B17748EF24B035B5007103FB /* Embed Frameworks */, + B145826A24E31E180035723E /* Update Build Version */, + ); + buildRules = ( + ); + dependencies = ( + B183883724DF377A007981BB /* PBXTargetDependency */, + ); + name = Imago; + productName = Imago; + productReference = B17748B124B03497007103FB /* Imago.app */; + productType = "com.apple.product-type.application"; + }; + B17748C524B03498007103FB /* ImagoTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B17748DD24B03498007103FB /* Build configuration list for PBXNativeTarget "ImagoTests" */; + buildPhases = ( + B17748C224B03498007103FB /* Sources */, + B17748C324B03498007103FB /* Frameworks */, + B17748C424B03498007103FB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B17748C824B03498007103FB /* PBXTargetDependency */, + ); + name = ImagoTests; + productName = ImagoTests; + productReference = B17748C624B03498007103FB /* ImagoTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + B17748D024B03498007103FB /* ImagoUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B17748E024B03498007103FB /* Build configuration list for PBXNativeTarget "ImagoUITests" */; + buildPhases = ( + B17748CD24B03498007103FB /* Sources */, + B17748CE24B03498007103FB /* Frameworks */, + B17748CF24B03498007103FB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B17748D324B03498007103FB /* PBXTargetDependency */, + ); + name = ImagoUITests; + productName = ImagoUITests; + productReference = B17748D124B03498007103FB /* ImagoUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B17748A924B03497007103FB /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1160; + ORGANIZATIONNAME = "Nolan Brown"; + TargetAttributes = { + B17748B024B03497007103FB = { + CreatedOnToolsVersion = 11.3; + }; + B17748C524B03498007103FB = { + CreatedOnToolsVersion = 11.3; + TestTargetID = B17748B024B03497007103FB; + }; + B17748D024B03498007103FB = { + CreatedOnToolsVersion = 11.3; + TestTargetID = B17748B024B03497007103FB; + }; + }; + }; + buildConfigurationList = B17748AC24B03497007103FB /* Build configuration list for PBXProject "Imago" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B17748A824B03497007103FB; + productRefGroup = B17748B224B03497007103FB /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B17748B024B03497007103FB /* Imago */, + B111516B24BD621D009CA476 /* ImagoPlugin */, + B17748C524B03498007103FB /* ImagoTests */, + B17748D024B03498007103FB /* ImagoUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B17748AF24B03497007103FB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B17748BC24B03498007103FB /* Preview Assets.xcassets in Resources */, + B17748B924B03498007103FB /* Assets.xcassets in Resources */, + B1DF8B7024BD7FDB007ED34C /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B17748C424B03498007103FB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B17748CF24B03498007103FB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + B145826824E315360035723E /* Code Sign Nested Bundles in EDSDK.framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Code Sign Nested Bundles in EDSDK.framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nEDSDK_FRAMEWORK_PATH=$SRCROOT/Frameworks/EDSDK/EDSDK.framework\nEDSDK_CURRENT_VERSION_PATH=$EDSDK_FRAMEWORK_PATH/Versions/A\ncodesign -fs \"$EXPANDED_CODE_SIGN_IDENTITY_NAME\" $EDSDK_CURRENT_VERSION_PATH/CHHLLite.bundle\ncodesign -fs \"$EXPANDED_CODE_SIGN_IDENTITY_NAME\" $EDSDK_CURRENT_VERSION_PATH/EdsImage.bundle\n#codesign -fs \"$EXPANDED_CODE_SIGN_IDENTITY_NAME\" $EDSDK_CURRENT_VERSION_PATH/EDSDK\n#codesign -fs \"$EXPANDED_CODE_SIGN_IDENTITY_NAME\" $EDSDK_FRAMEWORK_PATH\n"; + }; + B145826924E31D010035723E /* Update Build Version */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Update Build Version"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Keep build version synced between App and Plugin\n\nAPP_PLIST=\"${PROJECT_DIR}/Imago/Info.plist\"\n\nSHORT_VERSION=$($PLB -c \"Print CFBundleShortVersionString\" \"$APP_PLIST\")\nBUILD_NUMBER=$($PLB -c \"Print CFBundleVersion\" \"$APP_PLIST\")\n\nPLUGIN_PLIST=\"${PROJECT_DIR}/${INFOPLIST_FILE}\"\nPLB=/usr/libexec/PlistBuddy\n\n$PLB -c \"Set :CFBundleShortVersionString $SHORT_VERSION\" \"$PLUGIN_PLIST\"\n$PLB -c \"Set :CFBundleVersion $BUILD_NUMBER\" \"$PLUGIN_PLIST\"\n"; + }; + B145826A24E31E180035723E /* Update Build Version */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Update Build Version"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\nAPP_PLIST=\"${PROJECT_DIR}/${INFOPLIST_FILE}\"\n\nPLB=/usr/libexec/PlistBuddy\n\nLAST_NUMBER=$($PLB -c \"Print CFBundleVersion\" \"$APP_PLIST\")\nNEW_VERSION=$(($LAST_NUMBER + 1))\n$PLB -c \"Set :CFBundleVersion $NEW_VERSION\" \"$APP_PLIST\"\n\n"; + }; + B1DF8B6B24BD6E06007ED34C /* Copy Plugin to System */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Plugin to System"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif [ $CONFIGURATION == \"Debug\" ];then\necho $(security find-generic-password -ga \"ImagoBuild\" -w) | sudo -S rm -R /Library/CoreMediaIO/Plug-Ins/DAL/$PRODUCT_NAME.plugin; \nsudo cp -R $SYMROOT/$CONFIGURATION/$PRODUCT_NAME.plugin /Library/CoreMediaIO/Plug-Ins/DAL/$PRODUCT_NAME.plugin\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B111516C24BD621D009CA476 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B183885524E1BD63007981BB /* CIImage+Imago.swift in Sources */, + B111519B24BD6759009CA476 /* Property.swift in Sources */, + B111519A24BD6759009CA476 /* PluginInterface.swift in Sources */, + B111519724BD6759009CA476 /* Main.swift in Sources */, + B111519524BD6759009CA476 /* Device.swift in Sources */, + B1C297A824CF870B0089C84D /* RepeatingTimer.swift in Sources */, + B1B8AD1024C620C6009470CA /* DLog.swift in Sources */, + B111519924BD6759009CA476 /* Plugin.swift in Sources */, + B1B8AD1324C621A4009470CA /* ImagoStream.swift in Sources */, + B1B8AD4C24C756AE009470CA /* CallerID.swift in Sources */, + B183884724E1A82A007981BB /* CVPixelBufferPool+Imago.swift in Sources */, + B1B8AD0B24C61E9E009470CA /* Const.swift in Sources */, + B111519D24BD6759009CA476 /* Object.swift in Sources */, + B1C5577024C0B59A009355F1 /* Frame.swift in Sources */, + B15B113724CB61B6002703FF /* PubSub.swift in Sources */, + B1D0889124D1F1060001B8E9 /* CVPixelBuffer+Data.swift in Sources */, + B111519624BD6759009CA476 /* Stream.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B17748AD24B03497007103FB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B1C5576F24C0B59A009355F1 /* Frame.swift in Sources */, + B1C297A724CF870B0089C84D /* RepeatingTimer.swift in Sources */, + B17748E724B03542007103FB /* Camera.swift in Sources */, + B183885724E1C340007981BB /* CameraRow.swift in Sources */, + B15B113624CB61B6002703FF /* PubSub.swift in Sources */, + B1D0889024D1F1060001B8E9 /* CVPixelBuffer+Data.swift in Sources */, + B1D0889624D221160001B8E9 /* ViewController.swift in Sources */, + B18149F924B67E6E004CD8A8 /* CanonCommandErrors.swift in Sources */, + B17748B724B03497007103FB /* ContentView.swift in Sources */, + B1B8AD1424C64416009470CA /* Const.swift in Sources */, + B183884C24E1B853007981BB /* CameraFilterMenu.swift in Sources */, + B183884924E1B806007981BB /* TitlebarAccesory.swift in Sources */, + B1B8AD0824C61953009470CA /* Frame+Decompress.swift in Sources */, + B1B8AD4B24C756AE009470CA /* CallerID.swift in Sources */, + B183883424DDC18E007981BB /* CameraFilter.swift in Sources */, + B18149F224B61F86004CD8A8 /* CanonCamera.swift in Sources */, + B1B8AD0F24C620C6009470CA /* DLog.swift in Sources */, + B17748B524B03497007103FB /* AppDelegate.swift in Sources */, + B18149FB24B68094004CD8A8 /* CanonCommandProperty.swift in Sources */, + B1499C7724D1D9B300FC3557 /* Stream.swift in Sources */, + B183884E24E1B8FB007981BB /* LivePreview.swift in Sources */, + B1D0889324D1F75A0001B8E9 /* CIImage+Imago.swift in Sources */, + B183884624E1A829007981BB /* CVPixelBufferPool+Imago.swift in Sources */, + B183885324E1BC07007981BB /* CIImage+Filter.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B17748C224B03498007103FB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B17748CB24B03498007103FB /* ImagoTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B17748CD24B03498007103FB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B17748D624B03498007103FB /* ImagoUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + B17748C824B03498007103FB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B17748B024B03497007103FB /* Imago */; + targetProxy = B17748C724B03498007103FB /* PBXContainerItemProxy */; + }; + B17748D324B03498007103FB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B17748B024B03497007103FB /* Imago */; + targetProxy = B17748D224B03498007103FB /* PBXContainerItemProxy */; + }; + B183883724DF377A007981BB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B111516B24BD621D009CA476 /* ImagoPlugin */; + targetProxy = B183883624DF377A007981BB /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + B1DF8B6E24BD7FDB007ED34C /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B1DF8B6F24BD7FDB007ED34C /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + B111518124BD621D009CA476 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9DCD34JNP6; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Frameworks/EDSDK", + ); + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREFIX_HEADER = Plugin/Headers/PrefixHeader.pch; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/Frameworks/EDSDK/Header", + "/opt/libjpeg-turbo/include", + ); + INFOPLIST_FILE = Plugin/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.nolanbrown.Imago.Plugin; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Plugin/Headers/ImagoPlugin-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + WRAPPER_EXTENSION = plugin; + }; + name = Debug; + }; + B111518224BD621D009CA476 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9DCD34JNP6; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Frameworks/EDSDK", + ); + GCC_PREFIX_HEADER = Plugin/Headers/PrefixHeader.pch; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/Frameworks/EDSDK/Header", + "/opt/libjpeg-turbo/include", + ); + INFOPLIST_FILE = Plugin/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Frameworks", + ); + OTHER_CODE_SIGN_FLAGS = "--timestamp"; + PRODUCT_BUNDLE_IDENTIFIER = com.nolanbrown.Imago.Plugin; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Plugin/Headers/ImagoPlugin-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + WRAPPER_EXTENSION = plugin; + }; + name = Release; + }; + B17748D824B03498007103FB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + B17748D924B03498007103FB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + B17748DB24B03498007103FB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Imago/Assets/Imago.entitlements; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_ASSET_PATHS = "\"Imago/Preview Content\""; + DEVELOPMENT_TEAM = 9DCD34JNP6; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Frameworks/EDSDK", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "IMAGO_APP=1", + "DEBUG=1", + ); + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/Frameworks/EDSDK/Header", + "/opt/libjpeg-turbo/include", + ); + INFOPLIST_FILE = Imago/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.nolanbrown.Imago; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Imago/Headers/Imago-Bridging-Header.h"; + SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + B17748DC24B03498007103FB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Imago/Assets/Imago.entitlements; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_ASSET_PATHS = "\"Imago/Preview Content\""; + DEVELOPMENT_TEAM = 9DCD34JNP6; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Frameworks/EDSDK", + ); + GCC_PREPROCESSOR_DEFINITIONS = "IMAGO_APP=1"; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/Frameworks/EDSDK/Header", + "/opt/libjpeg-turbo/include", + ); + INFOPLIST_FILE = Imago/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + OTHER_CODE_SIGN_FLAGS = "--timestamp"; + PRODUCT_BUNDLE_IDENTIFIER = com.nolanbrown.Imago; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Imago Mac App Provisioning Profile"; + SWIFT_OBJC_BRIDGING_HEADER = "Imago/Headers/Imago-Bridging-Header.h"; + SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + B17748DE24B03498007103FB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9DCD34JNP6; + INFOPLIST_FILE = ImagoTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.nolanbrown.ImagoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Imago.app/Contents/MacOS/Imago"; + }; + name = Debug; + }; + B17748DF24B03498007103FB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9DCD34JNP6; + INFOPLIST_FILE = ImagoTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.nolanbrown.ImagoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Imago.app/Contents/MacOS/Imago"; + }; + name = Release; + }; + B17748E124B03498007103FB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9DCD34JNP6; + INFOPLIST_FILE = ImagoUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.nolanbrown.ImagoUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Imago; + }; + name = Debug; + }; + B17748E224B03498007103FB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9DCD34JNP6; + INFOPLIST_FILE = ImagoUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.nolanbrown.ImagoUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Imago; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B111518024BD621D009CA476 /* Build configuration list for PBXNativeTarget "ImagoPlugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B111518124BD621D009CA476 /* Debug */, + B111518224BD621D009CA476 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B17748AC24B03497007103FB /* Build configuration list for PBXProject "Imago" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B17748D824B03498007103FB /* Debug */, + B17748D924B03498007103FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B17748DA24B03498007103FB /* Build configuration list for PBXNativeTarget "Imago" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B17748DB24B03498007103FB /* Debug */, + B17748DC24B03498007103FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B17748DD24B03498007103FB /* Build configuration list for PBXNativeTarget "ImagoTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B17748DE24B03498007103FB /* Debug */, + B17748DF24B03498007103FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B17748E024B03498007103FB /* Build configuration list for PBXNativeTarget "ImagoUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B17748E124B03498007103FB /* Debug */, + B17748E224B03498007103FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B17748A924B03497007103FB /* Project object */; +} diff --git a/Imago.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Imago.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Imago.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Imago.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Imago.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Imago.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Imago.xcodeproj/xcshareddata/xcschemes/Imago.xcscheme b/Imago.xcodeproj/xcshareddata/xcschemes/Imago.xcscheme new file mode 100644 index 0000000..e5f8552 --- /dev/null +++ b/Imago.xcodeproj/xcshareddata/xcschemes/Imago.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Imago/AppDelegate.swift b/Imago/AppDelegate.swift new file mode 100644 index 0000000..5bd6a6a --- /dev/null +++ b/Imago/AppDelegate.swift @@ -0,0 +1,64 @@ +// +// AppDelegate.swift +// Imago +// +// Created by Nolan Brown on 7/3/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Cocoa +import SwiftUI + + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSToolbarDelegate { + + var window: NSWindow! + var viewController: ViewController! + + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Create the SwiftUI view that provides the window contents. + viewController = ViewController() + + // Create the titlebar accessory + let titlebarAccessoryView = TitlebarAccessory().environmentObject(viewController).frame(minWidth: 480, minHeight: 40, alignment: .topLeading).padding([.top, .leading, .trailing], -15.0).padding([.bottom, .leading],15.0) + let accessoryHostingView = NSHostingView(rootView:titlebarAccessoryView) + accessoryHostingView.frame.size = accessoryHostingView.fittingSize + let titlebarAccessory = NSTitlebarAccessoryViewController() + titlebarAccessory.view = accessoryHostingView + + + // Create the window and set the content view. + window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), + styleMask: [.titled, .closable, .miniaturizable, .fullSizeContentView], + backing: .buffered, defer: false) + window.center() + window.title = "Imago" + window.setFrameAutosaveName("Main Window") + + window.delegate = self + window.addTitlebarAccessoryViewController(titlebarAccessory) + + // Create the SwiftUI view that provides the window contents. + let contentView = ContentView().environmentObject(viewController) + + window.contentView = NSHostingView(rootView: contentView) + window.makeKeyAndOrderFront(nil) + + } + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + print("applicationShouldTerminateAfterLastWindowClosed \(sender)") + return true + } + + + func applicationWillTerminate(_ aNotification: Notification) { + print("applicationWillTerminate") + viewController.teardown() + } + +} + + + diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@1024.png b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@1024.png new file mode 100644 index 0000000..2c1acde Binary files /dev/null and b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@1024.png differ diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@128.png b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@128.png new file mode 100644 index 0000000..bf40073 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@128.png differ diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@16.png b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@16.png new file mode 100644 index 0000000..184a436 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@16.png differ diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@256-1.png b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@256-1.png new file mode 100644 index 0000000..8603519 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@256-1.png differ diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@256.png b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@256.png new file mode 100644 index 0000000..8603519 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@256.png differ diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@32-1.png b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@32-1.png new file mode 100644 index 0000000..b47b223 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@32-1.png differ diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@32.png b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@32.png new file mode 100644 index 0000000..b47b223 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@32.png differ diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@512-1.png b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@512-1.png new file mode 100644 index 0000000..9391c14 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@512-1.png differ diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@512.png b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@512.png new file mode 100644 index 0000000..9391c14 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@512.png differ diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@64.png b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@64.png new file mode 100644 index 0000000..a889ec7 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Asset 9@64.png differ diff --git a/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..b90134b --- /dev/null +++ b/Imago/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "Asset 9@16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "Asset 9@32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "Asset 9@32-1.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "Asset 9@64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "Asset 9@128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "Asset 9@256-1.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "Asset 9@256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "Asset 9@512-1.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "Asset 9@512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "Asset 9@1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Asset 7.png b/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Asset 7.png new file mode 100644 index 0000000..803a804 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Asset 7.png differ diff --git a/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Asset 7@2x.png b/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Asset 7@2x.png new file mode 100644 index 0000000..f634bba Binary files /dev/null and b/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Asset 7@2x.png differ diff --git a/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Asset 7@3x.png b/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Asset 7@3x.png new file mode 100644 index 0000000..e220710 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Asset 7@3x.png differ diff --git a/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Contents.json b/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Contents.json new file mode 100644 index 0000000..c0b1e39 --- /dev/null +++ b/Imago/Assets/Assets.xcassets/CameraIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Asset 7.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Asset 7@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Asset 7@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Imago/Assets/Assets.xcassets/Contents.json b/Imago/Assets/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Imago/Assets/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Bold-L.png b/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Bold-L.png new file mode 100644 index 0000000..e6da0f8 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Bold-L.png differ diff --git a/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Bold-L@2x.png b/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Bold-L@2x.png new file mode 100644 index 0000000..8ad75ac Binary files /dev/null and b/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Bold-L@2x.png differ diff --git a/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Bold-L@3x.png b/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Bold-L@3x.png new file mode 100644 index 0000000..7a989c7 Binary files /dev/null and b/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Bold-L@3x.png differ diff --git a/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Contents.json b/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Contents.json new file mode 100644 index 0000000..d774646 --- /dev/null +++ b/Imago/Assets/Assets.xcassets/ReloadIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Bold-L.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Bold-L@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Bold-L@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Imago/Assets/Base.lproj/Main.storyboard b/Imago/Assets/Base.lproj/Main.storyboard new file mode 100644 index 0000000..230cdf5 --- /dev/null +++ b/Imago/Assets/Base.lproj/Main.storyboard @@ -0,0 +1,683 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Imago/Assets/Imago.entitlements b/Imago/Assets/Imago.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Imago/Assets/Imago.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/Imago/Camera/Camera.swift b/Imago/Camera/Camera.swift new file mode 100644 index 0000000..b91eed5 --- /dev/null +++ b/Imago/Camera/Camera.swift @@ -0,0 +1,83 @@ +// +// Camera.swift +// Imago +// +// Created by Nolan Brown on 7/3/20. +// + +import SwiftUI + +import Foundation +import Cocoa +/* + + The Camera class is expected to be subclassed to interface with a particular brand or type of camera and return frames + */ + +extension Notification.Name { + static let didLoadCameras = Notification.Name("didLoadCameras") + static let cameraAddedEvent = Notification.Name("cameraAddedEvent") +} + +class Camera : Hashable, Identifiable, ObservableObject, CustomDebugStringConvertible { + var currentFrame: Frame? + + // For furture use when we can save previously used Cameras + // Stores if a camera is connected to the applications + @Published var connected: Bool = true + + // Is the camera sending frames? + @Published var streaming: Bool = false + + var name: String = "Camera" + var model: String = "Model" + var identifier: String = "Serial Number" + + var isActive: Bool = false + + var filter: CameraFilter = CameraFilter.None + + + var debugDescription: String { + return "\(String(describing: type(of: self)))(id:\(identifier) name:'\(name)')" + } + + var didReceiveFrame: ((Frame, Camera)->Void)? + + + //var model: String + static func == (lhs: Camera, rhs: Camera) -> Bool { + return lhs.identifier == rhs.identifier + } + public func hash(into hasher: inout Hasher) { + return hasher.combine(ObjectIdentifier(self).hashValue) + } + + + func setActive(active: Bool) { + if active == true { + streaming = true + } + else if active == false { + streaming = false + } + isActive = active + } + + + func setFrame(_ frame: Frame) { + if self.isActive { + + DispatchQueue.main.async { [unowned self, frame] in + self.currentFrame = frame + //self.previewImage = image + + self.didReceiveFrame?(frame, self) + } + + } + } + +} + + diff --git a/Imago/Camera/CameraFilter.swift b/Imago/Camera/CameraFilter.swift new file mode 100644 index 0000000..dcf0bec --- /dev/null +++ b/Imago/Camera/CameraFilter.swift @@ -0,0 +1,77 @@ +// +// CameraFilter.swift +// Imago +// +// Created by Nolan Brown on 8/7/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import CoreImage + +struct CameraFilter: OptionSet, Hashable, CustomStringConvertible { + static let Options: [CameraFilter] = [.Mirror, .Pixellate, .Thermalize] + + static let Mirror = CameraFilter(rawValue: 1 << 0) + static let Pixellate = CameraFilter(rawValue: 1 << 1) + static let Thermalize = CameraFilter(rawValue: 1 << 2) + + static let None = CameraFilter([]) + + let rawValue: Int + + + var hashValue: Int { + return self.rawValue + } + + + + static var debugDescriptions: [CameraFilter:String] = { + var descriptions = [CameraFilter:String]() + descriptions[.None] = "--" + descriptions[.Mirror] = "Mirror" + descriptions[.Pixellate] = "Pixellate" + descriptions[.Thermalize] = "Thermalize" + return descriptions + }() + + public var keyDescriptions: String { + var result = [String]() + for key in CameraFilter.debugDescriptions.keys { + guard self.contains(key), + let description = CameraFilter.debugDescriptions[key] + else { continue } + result.append(description) + } + return "\(result)" + } + + public var description: String { + return "CameraFilter(rawValue: \(self.rawValue)) \(self.keyDescriptions)" + } + + public func processImage(_ image: CIImage) -> CIImage { + var processedImage: CIImage = image + + if self.contains(.Pixellate) { + if let newImage = processedImage.pixellate(scale: 15) { + processedImage = newImage + } + } + if self.contains(.Thermalize) { + if let newImage = processedImage.thermalize() { + processedImage = newImage + } + } + if self.contains(.Mirror) { + if let newImage = processedImage.mirror() { + processedImage = newImage + } + + } + return processedImage + } + + +} diff --git a/Imago/Camera/Canon/CanonCamera.swift b/Imago/Camera/Canon/CanonCamera.swift new file mode 100644 index 0000000..97d8987 --- /dev/null +++ b/Imago/Camera/Canon/CanonCamera.swift @@ -0,0 +1,556 @@ +// +// CanonCamera.swift +// Imago +// +// Created by Nolan Brown on 7/8/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation + +struct CommandResult : CustomDebugStringConvertible { + let rawResult: EdsError + let property: CanonProperty? + + init() { + self.init(EdsError(EDS_ERR_OK), nil) + } + + init(_ rawResult: EdsError) { + self.init(rawResult, nil) + } + + init(_ rawResult: EdsError, _ property: CanonProperty?) { + self.rawResult = rawResult + self.property = property + + } + init(_ property: CanonProperty) { + self.init(EdsError(EDS_ERR_OK), property) + } + + func withProperty(_ property: CanonProperty) -> CommandResult { + return CommandResult(self.rawResult, property) + } + + + func isOK() -> Bool { + let isOK = (rawResult == EdsError(EDS_ERR_OK)) + return isOK + } + + // For readability in Command.perform() functions + func isError() -> Bool { + return !isOK() + } + + func getResultDescription() -> String { + return DescriptionForEdsError(self.rawResult) + } + + var debugDescription : String { + return self.description + } + + var description: String { + if self.property != nil { + return "CommandResult(result:\(self.getResultDescription()) property:\(self.property!))" + } + + return "CommandResult(\(self.getResultDescription()))" + } + +} + +class CanonCamera : Camera { + fileprivate var _deviceInfo: EdsDeviceInfo? = nil + fileprivate var _cameraRef: EdsCameraRef? = nil + fileprivate var _serialNumber: String? = nil + + fileprivate var _deviceDescription: String + fileprivate var _rawPort: String + + fileprivate var _fps: Int = 30 + fileprivate var _sequence: UInt64 = 0 + + fileprivate var _commandQueue: DispatchQueue = DispatchQueue(label: "com.nolanbrown.imago.CanonCamera.commands") //, qos: .userInteractive) + + private lazy var _signal: DispatchSourceUserDataAdd = { + let signal = DispatchSource.makeUserDataAddSource(queue: self._commandQueue) + signal.setEventHandler{ + // set frame here + + } + return signal + }() + + private lazy var _newFrameTimer: DispatchSourceTimer = { + let interval: DispatchTimeInterval = DispatchTimeInterval.nanoseconds(Int(NSEC_PER_SEC / UInt64(Double(self._fps)))) + let timer = DispatchSource.makeTimerSource(queue: self._commandQueue) + timer.setEventHandler(handler: self.newFrameTimerHandler) + timer.setCancelHandler(handler: self.timerCancelledHandler) + timer.schedule(deadline: .now(), repeating: interval) + return timer + }() + + private lazy var _stayAliveTimer: DispatchSourceTimer = { + let interval: DispatchTimeInterval = DispatchTimeInterval.seconds(60) + let timer = DispatchSource.makeTimerSource(queue: self._commandQueue) + timer.setEventHandler(handler: self.stayAliveTimerHandler) + timer.schedule(deadline: .now() + interval , repeating: interval) + return timer + }() + + + fileprivate var _id: String? + deinit { + print("deinit CanonCamera") + EdsRelease(_cameraRef) + } + +// required init(from decoder: Decoder) throws { +// super.init(from: decoder) +// let values = try decoder.container(keyedBy: CodingKeys.self) +// name = try values.decode(String.self, forKey: .name) +// model = try values.decode(String.self, forKey: .model) +// identifier = try values.decode(String.self, forKey: .identifier) +// +// } + + init(cameraRef:EdsCameraRef, deviceInfo: EdsDeviceInfo){ + _cameraRef = cameraRef + _deviceInfo = deviceInfo + + EdsRetain(_cameraRef) + + _deviceDescription = withUnsafePointer(to: _deviceInfo!.szDeviceDescription) { + String(cString: UnsafeRawPointer($0).assumingMemoryBound(to: CChar.self)) + } + _rawPort = withUnsafePointer(to: _deviceInfo!.szPortName) { + String(cString: UnsafeRawPointer($0).assumingMemoryBound(to: CChar.self)) + } + super.init() + + name = _deviceDescription + identifier = _rawPort + if let serial = self.serialNumber() { + identifier = serial + } + + } + + internal func getCameraRef()->EdsCameraRef?{ + return _cameraRef + } + + func serialNumber() -> String? { + if _serialNumber != nil { + return _serialNumber + } + if isActive { + let prop = CanonProperty.forID(EdsPropertyID(kEdsPropID_BodyIDEx)) + let propValue = getEdsPropertyValue(prop) + _serialNumber = propValue?.stringValue + return _serialNumber + } + return nil + } + +// override func identifier() -> String { +// return _rawPort +// } +// +// override func name() -> String { +// return _deviceDescription +// +// } + + + + override func setActive(active: Bool) { + var becameActive = active + + if active { + _commandQueue.sync { [unowned self] in + if self.registerEdsEventHandlers() { + if self.openSession() { + dlog("open session") + + if self.setEvfMode() && self.setOutputDevice(toPC: true) { + self._newFrameTimer.resume() + self._stayAliveTimer.resume() + becameActive = true + return + + } + } + } + becameActive = false + } + } + else { + _commandQueue.sync { [unowned self] in + self._newFrameTimer.suspend() + self._stayAliveTimer.suspend() + + self.removeEdsEventHandlers() + if self.setOutputDevice(toPC: false) { + if self.closeSession() { + dlog("Closed Session") + } + else { + dlog("Session didn't close") + } + } + else { + dlog("Output device wasn't reset") + } + } + } + + super.setActive(active: becameActive) + + } + + + func stayAliveTimerHandler() { + if self.setOutputDevice(toPC: true) { + dlog("Stay alive timer set output device") + } + else { + dlog("Stay alive timer failed to output device") + } + } + + func newFrameTimerHandler() { + guard let frame = loadFrame() else { + return + } + self.setFrame(frame) + flog(frame, "set frame for next request") + + } + + func timerCancelledHandler() { + dlog("Timer Cancelled") + } + + + +} + +extension CanonCamera { + private func openSession() -> Bool { + let result = CommandResult(EdsOpenSession(_cameraRef)) + if result.isError() { + if result.rawResult == EDS_ERR_SESSION_ALREADY_OPEN { + return true + } + return false + } + return true + } + + private func closeSession() -> Bool { + let result = CommandResult(EdsCloseSession(_cameraRef)) + if result.isError() { + if result.rawResult == EDS_ERR_SESSION_NOT_OPEN { + return true + } + return false + } + return true + } + + private func setEvfMode() -> Bool { + var evfMode: EdsUInt32 = 0 + + let result = CommandResult(EdsGetPropertyData(_cameraRef, EdsPropertyID(kEdsPropID_Evf_Mode), 0, EdsUInt32(MemoryLayout.size), &evfMode)) + if result.isError() { + return false + } + + if(evfMode == 0) + { + evfMode = 1 + // Set to the camera. + let setResult = CommandResult(EdsSetPropertyData(_cameraRef, EdsPropertyID(kEdsPropID_Evf_Mode), 0, EdsUInt32(MemoryLayout.size), &evfMode)) + if setResult.isError() { + return false + } + } + return true + } + + private func setOutputDevice(toPC: Bool = true) -> Bool { + var device: EdsUInt32 = 0 + + // Get Output Device + let outputDevicePropertyID = EdsPropertyID(kEdsPropID_Evf_OutputDevice) + let result = CommandResult(EdsGetPropertyData(_cameraRef, outputDevicePropertyID, 0, EdsUInt32(MemoryLayout.size), &device)) + if result.isError() { + dlog("Get Output Device Failed: \(result)") + + return false + } + + // Set the current output device. + if toPC { + device |= kEdsEvfOutputDevice_PC.rawValue + } + else { + device &= ~kEdsEvfOutputDevice_PC.rawValue + } + + let setResult = CommandResult(EdsSetPropertyData(_cameraRef, outputDevicePropertyID, 0, EdsUInt32(MemoryLayout.size), &device)) + if setResult.isError() { + dlog("Set Output Device Failed: \(setResult)") + return false + } + else { + dlog("Set Output Device == \(device)") + } + return true + } + + func loadFrame() -> Frame? { + let starts = mach_absolute_time() + + var streamRef: EdsStreamRef? = nil + var imageRef: EdsEvfImageRef? = nil + let bufferSize:EdsUInt64 = 0 + + // Create memory stream. + let createMemoryStreamResult = CommandResult(EdsCreateMemoryStream(bufferSize, &streamRef)) + if createMemoryStreamResult.isError() { + return nil + } + + // Create EvfImageRef. + let createImageRefResult = CommandResult(EdsCreateEvfImageRef(streamRef, &imageRef)) + if createImageRefResult.isError() { + return nil + } + + let downloadImageResult = CommandResult(EdsDownloadEvfImage(_cameraRef, imageRef)) + if downloadImageResult.isError() { + return nil + } + + // Get Image Details + var imageSize: EdsUInt64 = 0 + var pImage: UnsafeMutableRawPointer? = nil + + EdsGetPointer(streamRef, &pImage) + EdsGetLength(streamRef, &imageSize) + + if(imageRef != nil) + { + EdsRelease(imageRef) + } + + if(streamRef != nil) + { + EdsRelease(streamRef) + } + + let endget = mach_absolute_time() + + if imageSize > 0 { + let startdcom = mach_absolute_time() + if var imgData = Frame.decompress(UnsafeMutableRawPointer(pImage)!, length: UInt(imageSize)) { + let endcom = mach_absolute_time() + //imgData.id = UUID().uuidString + imgData.timestamp = starts + imgData.sequence = _sequence + + _sequence += 1 + + flog(imgData, "received sdk response", timestamp: endget) + flog(imgData, "decompress start", timestamp: startdcom) + flog(imgData, "decompress end", timestamp: endcom) + return imgData + } + + } + + + return nil + } + + + @discardableResult + private func registerEdsEventHandlers() -> Bool { + let camera: UnsafeMutableRawPointer = unsafeBitCast(self, to: UnsafeMutableRawPointer.self) + let result = CommandResult(EdsSetCameraStateEventHandler(_cameraRef, EdsStateEvent(kEdsStateEvent_All), edsHandleStateEvent, camera)) + if result.isError() { + return false + } + return true + } + + @discardableResult + private func removeEdsEventHandlers() -> Bool { + let result = CommandResult(EdsSetCameraStateEventHandler(_cameraRef, EdsStateEvent(kEdsStateEvent_All), nil, nil)) + if result.isError() { + return false + } + return true + } + + + +} + + +let edsHandleStateEvent : EdsStateEventHandler = { ( inEvent, inParam, inContext)->EdsError in + + var error = EdsError(EDS_ERR_OK) + + dlog("handleStateEvent \(inEvent)") + switch(inEvent) + { + case EdsStateEvent(kEdsStateEvent_Shutdown): + dlog("kEdsStateEvent_Shutdown") + break; + + case EdsStateEvent(kEdsStateEvent_WillSoonShutDown): + dlog("kEdsStateEvent_WillSoonShutDown") + break; + + case EdsStateEvent(kEdsStateEvent_ShutDownTimerUpdate): + dlog("kEdsStateEvent_ShutDownTimerUpdate") + break; + + default: + break; + } + return error; +} + +extension CanonCamera { + private func getEdsPropertyValue(_ property: CanonProperty) -> CanonProperty? { + guard let cameraRef = _cameraRef else { + return nil + } + var propertyWithValue: CanonProperty? + let propertyID = property.id + + var propType: EdsDataType = kEdsDataType_Unknown + var propSize: EdsUInt32 = 0 + + let propertySizeResult = CommandResult(EdsGetPropertySize(cameraRef, propertyID, 0, &propType, &propSize)) + if propertySizeResult.isOK() { + if(propType == kEdsDataType_UInt32 || propType == kEdsDataType_Int32 || propertyID == EdsPropertyID(kEdsPropID_Evf_OutputDevice)) + { + var uintData: EdsUInt32 = 0 + let getPropertyResult = CommandResult(EdsGetPropertyData(cameraRef, propertyID, 0, EdsUInt32(MemoryLayout.size), &uintData)) + + if getPropertyResult.isOK() { + propertyWithValue = property.withValue(uintData) + } + } + else if(propType == kEdsDataType_String) + { + var stringData:[EdsChar] = Array(repeating: 0, count: 256) + + let getPropertyResult = CommandResult(EdsGetPropertyData(cameraRef, propertyID, 0, EdsUInt32(MemoryLayout.size*256), &stringData)) + if getPropertyResult.isOK() { + let propertyStringValue : String = String(cString: stringData,encoding: String.Encoding.ascii)! + propertyWithValue = property.withValue(propertyStringValue) + } + } + } + return propertyWithValue + } +} + +extension CanonCamera { + static func setup() { + var result = CommandResult(EdsInitializeSDK()) + if result.isError() { + dlog("Error creating SDK connection: \(result)") + } + + result = CommandResult(EdsSetCameraAddedHandler(edsHandleCameraAddEvent, nil)) + if result.isError() { + dlog("Error adding Camera Added Handler: \(result)") + } + } + static func teardown() { + + var result = CommandResult(EdsSetCameraAddedHandler(nil, nil)) + if result.isError() { + dlog("Error removing Camera Added Handler: \(result)") + } + + result = CommandResult(EdsTerminateSDK()) + if result.isError() { + dlog("Error terminating SDK connection: \(result)") + } + } + + static func loadCameras() { + DispatchQueue.main.async { + var cameraList = nil as EdsCameraListRef? + var result = CommandResult() + result = CommandResult(EdsGetCameraList(&cameraList)) + if result.isError() { + return //nil + } + var cameraCount = 0 as EdsUInt32 + result = CommandResult(EdsGetChildCount(cameraList, &cameraCount)) + if result.isError() { + if cameraList != nil { + EdsRelease(cameraList) + } + return //nil + } + + var availableCameras: [CanonCamera] = [] + for i in 0.. EdsError in + + var error = EdsError(EDS_ERR_OK) + + dlog("Camera added..") + Thread.sleep(forTimeInterval:0.1) + CanonCamera.loadCameras() + + return error; +} + diff --git a/Imago/Camera/Canon/CanonCommandErrors.swift b/Imago/Camera/Canon/CanonCommandErrors.swift new file mode 100644 index 0000000..49e64a9 --- /dev/null +++ b/Imago/Camera/Canon/CanonCommandErrors.swift @@ -0,0 +1,146 @@ +// +// CanonCommandErrors.swift +// Imago +// +// Created by Nolan Brown on 7/8/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation + +func DescriptionForEdsError(_ error: EdsError) -> String { + switch error { + case EdsError(EDS_ERR_OK): return "EDS_ERR_OK" + case EdsError(EDS_ERR_UNIMPLEMENTED): return "EDS_ERR_UNIMPLEMENTED" + case EdsError(EDS_ERR_INTERNAL_ERROR): return "EDS_ERR_INTERNAL_ERROR" + case EdsError(EDS_ERR_MEM_ALLOC_FAILED): return "EDS_ERR_MEM_ALLOC_FAILED" + case EdsError(EDS_ERR_MEM_FREE_FAILED): return "EDS_ERR_MEM_FREE_FAILED" + case EdsError(EDS_ERR_OPERATION_CANCELLED): return "EDS_ERR_OPERATION_CANCELLED" + case EdsError(EDS_ERR_INCOMPATIBLE_VERSION): return "EDS_ERR_INCOMPATIBLE_VERSION" + case EdsError(EDS_ERR_NOT_SUPPORTED): return "EDS_ERR_NOT_SUPPORTED" + case EdsError(EDS_ERR_UNEXPECTED_EXCEPTION): return "EDS_ERR_UNEXPECTED_EXCEPTION" + case EdsError(EDS_ERR_PROTECTION_VIOLATION): return "EDS_ERR_PROTECTION_VIOLATION" + case EdsError(EDS_ERR_MISSING_SUBCOMPONENT): return "EDS_ERR_MISSING_SUBCOMPONENT" + case EdsError(EDS_ERR_SELECTION_UNAVAILABLE): return "EDS_ERR_SELECTION_UNAVAILABLE" + case EdsError(EDS_ERR_FILE_IO_ERROR): return "EDS_ERR_FILE_IO_ERROR" + case EdsError(EDS_ERR_FILE_TOO_MANY_OPEN): return "EDS_ERR_FILE_TOO_MANY_OPEN" + case EdsError(EDS_ERR_FILE_NOT_FOUND): return "EDS_ERR_FILE_NOT_FOUND" + case EdsError(EDS_ERR_FILE_OPEN_ERROR): return "EDS_ERR_FILE_OPEN_ERROR" + case EdsError(EDS_ERR_FILE_CLOSE_ERROR): return "EDS_ERR_FILE_CLOSE_ERROR" + case EdsError(EDS_ERR_FILE_SEEK_ERROR): return "EDS_ERR_FILE_SEEK_ERROR" + case EdsError(EDS_ERR_FILE_TELL_ERROR): return "EDS_ERR_FILE_TELL_ERROR" + case EdsError(EDS_ERR_FILE_READ_ERROR): return "EDS_ERR_FILE_READ_ERROR" + case EdsError(EDS_ERR_FILE_WRITE_ERROR): return "EDS_ERR_FILE_WRITE_ERROR" + case EdsError(EDS_ERR_FILE_PERMISSION_ERROR): return "EDS_ERR_FILE_PERMISSION_ERROR" + case EdsError(EDS_ERR_FILE_DISK_FULL_ERROR): return "EDS_ERR_FILE_DISK_FULL_ERROR" + case EdsError(EDS_ERR_FILE_ALREADY_EXISTS): return "EDS_ERR_FILE_ALREADY_EXISTS" + case EdsError(EDS_ERR_FILE_FORMAT_UNRECOGNIZED): return "EDS_ERR_FILE_FORMAT_UNRECOGNIZED" + case EdsError(EDS_ERR_FILE_DATA_CORRUPT): return "EDS_ERR_FILE_DATA_CORRUPT" + case EdsError(EDS_ERR_FILE_NAMING_NA): return "EDS_ERR_FILE_NAMING_NA" + case EdsError(EDS_ERR_DIR_NOT_FOUND): return "EDS_ERR_DIR_NOT_FOUND" + case EdsError(EDS_ERR_DIR_IO_ERROR): return "EDS_ERR_DIR_IO_ERROR" + case EdsError(EDS_ERR_DIR_ENTRY_NOT_FOUND): return "EDS_ERR_DIR_ENTRY_NOT_FOUND" + case EdsError(EDS_ERR_DIR_ENTRY_EXISTS): return "EDS_ERR_DIR_ENTRY_EXISTS" + case EdsError(EDS_ERR_DIR_NOT_EMPTY): return "EDS_ERR_DIR_NOT_EMPTY" + case EdsError(EDS_ERR_PROPERTIES_UNAVAILABLE): return "EDS_ERR_PROPERTIES_UNAVAILABLE" + case EdsError(EDS_ERR_PROPERTIES_MISMATCH): return "EDS_ERR_PROPERTIES_MISMATCH" + case EdsError(EDS_ERR_PROPERTIES_NOT_LOADED): return "EDS_ERR_PROPERTIES_NOT_LOADED" + case EdsError(EDS_ERR_INVALID_PARAMETER): return "EDS_ERR_INVALID_PARAMETER" + case EdsError(EDS_ERR_INVALID_HANDLE): return "EDS_ERR_INVALID_HANDLE" + case EdsError(EDS_ERR_INVALID_POINTER): return "EDS_ERR_INVALID_POINTER" + case EdsError(EDS_ERR_INVALID_INDEX): return "EDS_ERR_INVALID_INDEX" + case EdsError(EDS_ERR_INVALID_LENGTH): return "EDS_ERR_INVALID_LENGTH" + case EdsError(EDS_ERR_INVALID_FN_POINTER): return "EDS_ERR_INVALID_FN_POINTER" + case EdsError(EDS_ERR_INVALID_SORT_FN): return "EDS_ERR_INVALID_SORT_FN" + case EdsError(EDS_ERR_DEVICE_NOT_FOUND): return "EDS_ERR_DEVICE_NOT_FOUND" + case EdsError(EDS_ERR_DEVICE_BUSY): return "EDS_ERR_DEVICE_BUSY" + case EdsError(EDS_ERR_DEVICE_INVALID): return "EDS_ERR_DEVICE_INVALID" + case EdsError(EDS_ERR_DEVICE_EMERGENCY): return "EDS_ERR_DEVICE_EMERGENCY" + case EdsError(EDS_ERR_DEVICE_MEMORY_FULL): return "EDS_ERR_DEVICE_MEMORY_FULL" + case EdsError(EDS_ERR_DEVICE_INTERNAL_ERROR): return "EDS_ERR_DEVICE_INTERNAL_ERROR" + case EdsError(EDS_ERR_DEVICE_INVALID_PARAMETER): return "EDS_ERR_DEVICE_INVALID_PARAMETER" + case EdsError(EDS_ERR_DEVICE_NO_DISK): return "EDS_ERR_DEVICE_NO_DISK" + case EdsError(EDS_ERR_DEVICE_DISK_ERROR): return "EDS_ERR_DEVICE_DISK_ERROR" + case EdsError(EDS_ERR_DEVICE_CF_GATE_CHANGED): return "EDS_ERR_DEVICE_CF_GATE_CHANGED" + case EdsError(EDS_ERR_DEVICE_DIAL_CHANGED): return "EDS_ERR_DEVICE_DIAL_CHANGED" + case EdsError(EDS_ERR_DEVICE_NOT_INSTALLED): return "EDS_ERR_DEVICE_NOT_INSTALLED" + case EdsError(EDS_ERR_DEVICE_STAY_AWAKE): return "EDS_ERR_DEVICE_STAY_AWAKE" + case EdsError(EDS_ERR_DEVICE_NOT_RELEASED): return "EDS_ERR_DEVICE_NOT_RELEASED" + case EdsError(EDS_ERR_STREAM_IO_ERROR): return "EDS_ERR_STREAM_IO_ERROR" + case EdsError(EDS_ERR_STREAM_NOT_OPEN): return "EDS_ERR_STREAM_NOT_OPEN" + case EdsError(EDS_ERR_STREAM_ALREADY_OPEN): return "EDS_ERR_STREAM_ALREADY_OPEN" + case EdsError(EDS_ERR_STREAM_OPEN_ERROR): return "EDS_ERR_STREAM_OPEN_ERROR" + case EdsError(EDS_ERR_STREAM_CLOSE_ERROR): return "EDS_ERR_STREAM_CLOSE_ERROR" + case EdsError(EDS_ERR_STREAM_SEEK_ERROR): return "EDS_ERR_STREAM_SEEK_ERROR" + case EdsError(EDS_ERR_STREAM_TELL_ERROR): return "EDS_ERR_STREAM_TELL_ERROR" + case EdsError(EDS_ERR_STREAM_READ_ERROR): return "EDS_ERR_STREAM_READ_ERROR" + case EdsError(EDS_ERR_STREAM_WRITE_ERROR): return "EDS_ERR_STREAM_WRITE_ERROR" + case EdsError(EDS_ERR_STREAM_PERMISSION_ERROR): return "EDS_ERR_STREAM_PERMISSION_ERROR" + case EdsError(EDS_ERR_STREAM_COULDNT_BEGIN_THREAD): return "EDS_ERR_STREAM_COULDNT_BEGIN_THREAD" + case EdsError(EDS_ERR_STREAM_BAD_OPTIONS): return "EDS_ERR_STREAM_BAD_OPTIONS" + case EdsError(EDS_ERR_STREAM_END_OF_STREAM): return "EDS_ERR_STREAM_END_OF_STREAM" + case EdsError(EDS_ERR_COMM_PORT_IS_IN_USE): return "EDS_ERR_COMM_PORT_IS_IN_USE" + case EdsError(EDS_ERR_COMM_DISCONNECTED): return "EDS_ERR_COMM_DISCONNECTED" + case EdsError(EDS_ERR_COMM_DEVICE_INCOMPATIBLE): return "EDS_ERR_COMM_DEVICE_INCOMPATIBLE" + case EdsError(EDS_ERR_COMM_BUFFER_FULL): return "EDS_ERR_COMM_BUFFER_FULL" + case EdsError(EDS_ERR_COMM_USB_BUS_ERR): return "EDS_ERR_COMM_USB_BUS_ERR" + case EdsError(EDS_ERR_USB_DEVICE_LOCK_ERROR): return "EDS_ERR_USB_DEVICE_LOCK_ERROR" + case EdsError(EDS_ERR_USB_DEVICE_UNLOCK_ERROR): return "EDS_ERR_USB_DEVICE_UNLOCK_ERROR" + case EdsError(EDS_ERR_STI_UNKNOWN_ERROR): return "EDS_ERR_STI_UNKNOWN_ERROR" + case EdsError(EDS_ERR_STI_INTERNAL_ERROR): return "EDS_ERR_STI_INTERNAL_ERROR" + case EdsError(EDS_ERR_STI_DEVICE_CREATE_ERROR): return "EDS_ERR_STI_DEVICE_CREATE_ERROR" + case EdsError(EDS_ERR_STI_DEVICE_RELEASE_ERROR): return "EDS_ERR_STI_DEVICE_RELEASE_ERROR" + case EdsError(EDS_ERR_DEVICE_NOT_LAUNCHED): return "EDS_ERR_DEVICE_NOT_LAUNCHED" + case EdsError(EDS_ERR_ENUM_NA): return "EDS_ERR_ENUM_NA" + case EdsError(EDS_ERR_INVALID_FN_CALL): return "EDS_ERR_INVALID_FN_CALL" + case EdsError(EDS_ERR_HANDLE_NOT_FOUND): return "EDS_ERR_HANDLE_NOT_FOUND" + case EdsError(EDS_ERR_INVALID_ID): return "EDS_ERR_INVALID_ID" + case EdsError(EDS_ERR_WAIT_TIMEOUT_ERROR): return "EDS_ERR_WAIT_TIMEOUT_ERROR" + case EdsError(EDS_ERR_SESSION_NOT_OPEN): return "EDS_ERR_SESSION_NOT_OPEN" + case EdsError(EDS_ERR_INVALID_TRANSACTIONID): return "EDS_ERR_INVALID_TRANSACTIONID" + case EdsError(EDS_ERR_INCOMPLETE_TRANSFER): return "EDS_ERR_INCOMPLETE_TRANSFER" + case EdsError(EDS_ERR_INVALID_STRAGEID): return "EDS_ERR_INVALID_STRAGEID" + case EdsError(EDS_ERR_DEVICEPROP_NOT_SUPPORTED): return "EDS_ERR_DEVICEPROP_NOT_SUPPORTED" + case EdsError(EDS_ERR_INVALID_OBJECTFORMATCODE): return "EDS_ERR_INVALID_OBJECTFORMATCODE" + case EdsError(EDS_ERR_SELF_TEST_FAILED): return "EDS_ERR_SELF_TEST_FAILED" + case EdsError(EDS_ERR_PARTIAL_DELETION): return "EDS_ERR_PARTIAL_DELETION" + case EdsError(EDS_ERR_SPECIFICATION_BY_FORMAT_UNSUPPORTED): return "EDS_ERR_SPECIFICATION_BY_FORMAT_UNSUPPORTED" + case EdsError(EDS_ERR_NO_VALID_OBJECTINFO): return "EDS_ERR_NO_VALID_OBJECTINFO" + case EdsError(EDS_ERR_INVALID_CODE_FORMAT): return "EDS_ERR_INVALID_CODE_FORMAT" + case EdsError(EDS_ERR_UNKNOWN_VENDOR_CODE): return "EDS_ERR_UNKNOWN_VENDOR_CODE" + case EdsError(EDS_ERR_CAPTURE_ALREADY_TERMINATED): return "EDS_ERR_CAPTURE_ALREADY_TERMINATED" + case EdsError(EDS_ERR_PTP_DEVICE_BUSY): return "EDS_ERR_PTP_DEVICE_BUSY" + case EdsError(EDS_ERR_INVALID_PARENTOBJECT): return "EDS_ERR_INVALID_PARENTOBJECT" + case EdsError(EDS_ERR_INVALID_DEVICEPROP_FORMAT): return "EDS_ERR_INVALID_DEVICEPROP_FORMAT" + case EdsError(EDS_ERR_INVALID_DEVICEPROP_VALUE): return "EDS_ERR_INVALID_DEVICEPROP_VALUE" + case EdsError(EDS_ERR_SESSION_ALREADY_OPEN): return "EDS_ERR_SESSION_ALREADY_OPEN" + case EdsError(EDS_ERR_TRANSACTION_CANCELLED): return "EDS_ERR_TRANSACTION_CANCELLED" + case EdsError(EDS_ERR_SPECIFICATION_OF_DESTINATION_UNSUPPORTED): return "EDS_ERR_SPECIFICATION_OF_DESTINATION_UNSUPPORTED" + case EdsError(EDS_ERR_NOT_CAMERA_SUPPORT_SDK_VERSION): return "EDS_ERR_NOT_CAMERA_SUPPORT_SDK_VERSION" + case EdsError(EDS_ERR_UNKNOWN_COMMAND): return "EDS_ERR_UNKNOWN_COMMAND" + case EdsError(EDS_ERR_OPERATION_REFUSED): return "EDS_ERR_OPERATION_REFUSED" + case EdsError(EDS_ERR_LENS_COVER_CLOSE): return "EDS_ERR_LENS_COVER_CLOSE" + case EdsError(EDS_ERR_LOW_BATTERY): return "EDS_ERR_LOW_BATTERY" + case EdsError(EDS_ERR_OBJECT_NOTREADY): return "EDS_ERR_OBJECT_NOTREADY" + case EdsError(EDS_ERR_CANNOT_MAKE_OBJECT): return "EDS_ERR_CANNOT_MAKE_OBJECT" + case EdsError(EDS_ERR_MEMORYSTATUS_NOTREADY): return "EDS_ERR_MEMORYSTATUS_NOTREADY" + case EdsError(EDS_ERR_TAKE_PICTURE_AF_NG): return "EDS_ERR_TAKE_PICTURE_AF_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_RESERVED): return "EDS_ERR_TAKE_PICTURE_RESERVED" + case EdsError(EDS_ERR_TAKE_PICTURE_MIRROR_UP_NG): return "EDS_ERR_TAKE_PICTURE_MIRROR_UP_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_SENSOR_CLEANING_NG): return "EDS_ERR_TAKE_PICTURE_SENSOR_CLEANING_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_SILENCE_NG): return "EDS_ERR_TAKE_PICTURE_SILENCE_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_NO_CARD_NG): return "EDS_ERR_TAKE_PICTURE_NO_CARD_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_CARD_NG): return "EDS_ERR_TAKE_PICTURE_CARD_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_CARD_PROTECT_NG): return "EDS_ERR_TAKE_PICTURE_CARD_PROTECT_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_MOVIE_CROP_NG): return "EDS_ERR_TAKE_PICTURE_MOVIE_CROP_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_STROBO_CHARGE_NG): return "EDS_ERR_TAKE_PICTURE_STROBO_CHARGE_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_NO_LENS_NG): return "EDS_ERR_TAKE_PICTURE_NO_LENS_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_SPECIAL_MOVIE_MODE_NG): return "EDS_ERR_TAKE_PICTURE_SPECIAL_MOVIE_MODE_NG" + case EdsError(EDS_ERR_TAKE_PICTURE_LV_REL_PROHIBIT_MODE_NG): return "EDS_ERR_TAKE_PICTURE_LV_REL_PROHIBIT_MODE_NG" + case EdsError(EDS_ERR_LAST_GENERIC_ERROR_PLUS_ONE): return "EDS_ERR_LAST_GENERIC_ERROR_PLUS_ONE" + + default: + return "UNKNOWN" + + } +} diff --git a/Imago/Camera/Canon/CanonCommandProperty.swift b/Imago/Camera/Canon/CanonCommandProperty.swift new file mode 100644 index 0000000..51e8518 --- /dev/null +++ b/Imago/Camera/Canon/CanonCommandProperty.swift @@ -0,0 +1,176 @@ +// +// CanonCommandProperty.swift +// Imago +// +// Created by Nolan Brown on 7/8/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation + +// The majority of these aren't used but are available here in case they're set as part of the EDSDK Property Change Handler +struct CanonProperty : Hashable, Identifiable, CustomDebugStringConvertible { + var id: EdsPropertyID + var name: String + var stringValue: String? + var dataValue: Data? + var dataType: EdsDataType? + var intValue: UInt32? + + init(_ name: String, _ edsPropertyID: EdsPropertyID) { + self.name = name + self.id = edsPropertyID + } + + init(id:EdsPropertyID, name: String, stringValue: String?, dataValue: Data?, dataType: EdsDataType?, intValue: UInt32?) { + self.id = id + self.name = name + self.stringValue = stringValue + self.dataValue = dataValue + self.dataType = dataType + self.intValue = intValue + + } + + static func forID(_ propertyID: EdsPropertyID) -> CanonProperty { + return GetProperyForEdsPropertyID(propertyID) + } + + func withValue(_ data: Data, _ type: EdsDataType) -> CanonProperty { + return CanonProperty(id: self.id, name: self.name, stringValue: nil, dataValue: data, dataType: type, intValue: nil) + } + func withValue(_ str: String) -> CanonProperty { + return CanonProperty(id: self.id, name: self.name, stringValue: str, dataValue: nil, dataType: nil, intValue: nil) + } + func withValue(_ i: UInt32) -> CanonProperty { + return CanonProperty(id: self.id, name: self.name, stringValue: nil, dataValue: nil, dataType: nil, intValue: i) + } + + static func == (lhs: CanonProperty, rhs: CanonProperty) -> Bool { + return lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(stringValue) + hasher.combine(dataValue) + hasher.combine(intValue) + } + var debugDescription: String { + if self.stringValue != nil { + return "CanonProperty(id: \(id) name: \(name) value: \(self.stringValue!))" + } + else if self.dataValue != nil { + return "CanonProperty(id: \(id) name: \(name) value: \(self.dataValue!))" + } + else if self.intValue != nil { + return "CanonProperty(id: \(id) name: \(name) value: \(self.intValue!))" + } + return "CanonProperty(id: \(id) name: \(name) value: nil)" + } +} + +func GetProperyForEdsPropertyID(_ propertyID: EdsPropertyID) -> CanonProperty { + switch propertyID { + case EdsPropertyID(kEdsPropID_Unknown): return CanonProperty("Unknown", EdsPropertyID(kEdsPropID_Unknown)) + case EdsPropertyID(kEdsPropID_ProductName): return CanonProperty("ProductName", EdsPropertyID(kEdsPropID_ProductName)) + case EdsPropertyID(kEdsPropID_OwnerName): return CanonProperty("OwnerName", EdsPropertyID(kEdsPropID_OwnerName)) + case EdsPropertyID(kEdsPropID_MakerName): return CanonProperty("MakerName", EdsPropertyID(kEdsPropID_MakerName)) + case EdsPropertyID(kEdsPropID_DateTime): return CanonProperty("DateTime", EdsPropertyID(kEdsPropID_DateTime)) + case EdsPropertyID(kEdsPropID_FirmwareVersion): return CanonProperty("FirmwareVersion", EdsPropertyID(kEdsPropID_FirmwareVersion)) + case EdsPropertyID(kEdsPropID_BatteryLevel): return CanonProperty("BatteryLevel", EdsPropertyID(kEdsPropID_BatteryLevel)) + case EdsPropertyID(kEdsPropID_CFn): return CanonProperty("CFn", EdsPropertyID(kEdsPropID_CFn)) + case EdsPropertyID(kEdsPropID_SaveTo): return CanonProperty("SaveTo", EdsPropertyID(kEdsPropID_SaveTo)) + case EdsPropertyID(kEdsPropID_CurrentStorage): return CanonProperty("CurrentStorage", EdsPropertyID(kEdsPropID_CurrentStorage)) + case EdsPropertyID(kEdsPropID_CurrentFolder): return CanonProperty("CurrentFolder", EdsPropertyID(kEdsPropID_CurrentFolder)) + case EdsPropertyID(kEdsPropID_BatteryQuality): return CanonProperty("BatteryQuality", EdsPropertyID(kEdsPropID_BatteryQuality)) + case EdsPropertyID(kEdsPropID_BodyIDEx): return CanonProperty("BodyIDEx", EdsPropertyID(kEdsPropID_BodyIDEx)) + case EdsPropertyID(kEdsPropID_HDDirectoryStructure): return CanonProperty("HDDirectoryStructure", EdsPropertyID(kEdsPropID_HDDirectoryStructure)) + case EdsPropertyID(kEdsPropID_ImageQuality): return CanonProperty("ImageQuality", EdsPropertyID(kEdsPropID_ImageQuality)) + case EdsPropertyID(kEdsPropID_Orientation): return CanonProperty("Orientation", EdsPropertyID(kEdsPropID_Orientation)) + case EdsPropertyID(kEdsPropID_ICCProfile): return CanonProperty("ICCProfile", EdsPropertyID(kEdsPropID_ICCProfile)) + case EdsPropertyID(kEdsPropID_FocusInfo): return CanonProperty("FocusInfo", EdsPropertyID(kEdsPropID_FocusInfo)) + case EdsPropertyID(kEdsPropID_WhiteBalance): return CanonProperty("WhiteBalance", EdsPropertyID(kEdsPropID_WhiteBalance)) + case EdsPropertyID(kEdsPropID_ColorTemperature): return CanonProperty("ColorTemperature", EdsPropertyID(kEdsPropID_ColorTemperature)) + case EdsPropertyID(kEdsPropID_WhiteBalanceShift): return CanonProperty("WhiteBalanceShift", EdsPropertyID(kEdsPropID_WhiteBalanceShift)) + case EdsPropertyID(kEdsPropID_ColorSpace): return CanonProperty("ColorSpace", EdsPropertyID(kEdsPropID_ColorSpace)) + case EdsPropertyID(kEdsPropID_PictureStyle): return CanonProperty("PictureStyle", EdsPropertyID(kEdsPropID_PictureStyle)) + case EdsPropertyID(kEdsPropID_PictureStyleDesc): return CanonProperty("PictureStyleDesc", EdsPropertyID(kEdsPropID_PictureStyleDesc)) + case EdsPropertyID(kEdsPropID_PictureStyleCaption): return CanonProperty("PictureStyleCaption", EdsPropertyID(kEdsPropID_PictureStyleCaption)) + case EdsPropertyID(kEdsPropID_GPSVersionID): return CanonProperty("GPSVersionID", EdsPropertyID(kEdsPropID_GPSVersionID)) + case EdsPropertyID(kEdsPropID_GPSLatitudeRef): return CanonProperty("GPSLatitudeRef", EdsPropertyID(kEdsPropID_GPSLatitudeRef)) + case EdsPropertyID(kEdsPropID_GPSLatitude): return CanonProperty("GPSLatitude", EdsPropertyID(kEdsPropID_GPSLatitude)) + case EdsPropertyID(kEdsPropID_GPSLongitudeRef): return CanonProperty("GPSLongitudeRef", EdsPropertyID(kEdsPropID_GPSLongitudeRef)) + case EdsPropertyID(kEdsPropID_GPSLongitude): return CanonProperty("GPSLongitude", EdsPropertyID(kEdsPropID_GPSLongitude)) + case EdsPropertyID(kEdsPropID_GPSAltitudeRef): return CanonProperty("GPSAltitudeRef", EdsPropertyID(kEdsPropID_GPSAltitudeRef)) + case EdsPropertyID(kEdsPropID_GPSAltitude): return CanonProperty("GPSAltitude", EdsPropertyID(kEdsPropID_GPSAltitude)) + case EdsPropertyID(kEdsPropID_GPSTimeStamp): return CanonProperty("GPSTimeStamp", EdsPropertyID(kEdsPropID_GPSTimeStamp)) + case EdsPropertyID(kEdsPropID_GPSSatellites): return CanonProperty("GPSSatellites", EdsPropertyID(kEdsPropID_GPSSatellites)) + case EdsPropertyID(kEdsPropID_GPSStatus): return CanonProperty("GPSStatus", EdsPropertyID(kEdsPropID_GPSStatus)) + case EdsPropertyID(kEdsPropID_GPSMapDatum): return CanonProperty("GPSMapDatum", EdsPropertyID(kEdsPropID_GPSMapDatum)) + case EdsPropertyID(kEdsPropID_GPSDateStamp): return CanonProperty("GPSDateStamp", EdsPropertyID(kEdsPropID_GPSDateStamp)) + case EdsPropertyID(kEdsPropID_AEMode): return CanonProperty("AEMode", EdsPropertyID(kEdsPropID_AEMode)) + case EdsPropertyID(kEdsPropID_DriveMode): return CanonProperty("DriveMode", EdsPropertyID(kEdsPropID_DriveMode)) + case EdsPropertyID(kEdsPropID_ISOSpeed): return CanonProperty("ISOSpeed", EdsPropertyID(kEdsPropID_ISOSpeed)) + case EdsPropertyID(kEdsPropID_MeteringMode): return CanonProperty("MeteringMode", EdsPropertyID(kEdsPropID_MeteringMode)) + case EdsPropertyID(kEdsPropID_AFMode): return CanonProperty("AFMode", EdsPropertyID(kEdsPropID_AFMode)) + case EdsPropertyID(kEdsPropID_Av): return CanonProperty("Av", EdsPropertyID(kEdsPropID_Av)) + case EdsPropertyID(kEdsPropID_Tv): return CanonProperty("Tv", EdsPropertyID(kEdsPropID_Tv)) + case EdsPropertyID(kEdsPropID_ExposureCompensation): return CanonProperty("ExposureCompensation", EdsPropertyID(kEdsPropID_ExposureCompensation)) + case EdsPropertyID(kEdsPropID_FocalLength): return CanonProperty("FocalLength", EdsPropertyID(kEdsPropID_FocalLength)) + case EdsPropertyID(kEdsPropID_AvailableShots): return CanonProperty("AvailableShots", EdsPropertyID(kEdsPropID_AvailableShots)) + case EdsPropertyID(kEdsPropID_Bracket): return CanonProperty("Bracket", EdsPropertyID(kEdsPropID_Bracket)) + case EdsPropertyID(kEdsPropID_WhiteBalanceBracket): return CanonProperty("WhiteBalanceBracket", EdsPropertyID(kEdsPropID_WhiteBalanceBracket)) + case EdsPropertyID(kEdsPropID_LensName): return CanonProperty("LensName", EdsPropertyID(kEdsPropID_LensName)) + case EdsPropertyID(kEdsPropID_AEBracket): return CanonProperty("AEBracket", EdsPropertyID(kEdsPropID_AEBracket)) + case EdsPropertyID(kEdsPropID_FEBracket): return CanonProperty("FEBracket", EdsPropertyID(kEdsPropID_FEBracket)) + case EdsPropertyID(kEdsPropID_ISOBracket): return CanonProperty("ISOBracket", EdsPropertyID(kEdsPropID_ISOBracket)) + case EdsPropertyID(kEdsPropID_NoiseReduction): return CanonProperty("NoiseReduction", EdsPropertyID(kEdsPropID_NoiseReduction)) + case EdsPropertyID(kEdsPropID_FlashOn): return CanonProperty("FlashOn", EdsPropertyID(kEdsPropID_FlashOn)) + case EdsPropertyID(kEdsPropID_RedEye): return CanonProperty("RedEye", EdsPropertyID(kEdsPropID_RedEye)) + case EdsPropertyID(kEdsPropID_FlashMode): return CanonProperty("FlashMode", EdsPropertyID(kEdsPropID_FlashMode)) + case EdsPropertyID(kEdsPropID_LensStatus): return CanonProperty("LensStatus", EdsPropertyID(kEdsPropID_LensStatus)) + case EdsPropertyID(kEdsPropID_Artist): return CanonProperty("Artist", EdsPropertyID(kEdsPropID_Artist)) + case EdsPropertyID(kEdsPropID_Copyright): return CanonProperty("Copyright", EdsPropertyID(kEdsPropID_Copyright)) + case EdsPropertyID(kEdsPropID_AEModeSelect): return CanonProperty("AEModeSelect", EdsPropertyID(kEdsPropID_AEModeSelect)) + case EdsPropertyID(kEdsPropID_PowerZoom_Speed): return CanonProperty("PowerZoom_Speed", EdsPropertyID(kEdsPropID_PowerZoom_Speed)) + case EdsPropertyID(kEdsPropID_Evf_OutputDevice): return CanonProperty("Evf_OutputDevice", EdsPropertyID(kEdsPropID_Evf_OutputDevice)) + case EdsPropertyID(kEdsPropID_Evf_Mode): return CanonProperty("Evf_Mode", EdsPropertyID(kEdsPropID_Evf_Mode)) + case EdsPropertyID(kEdsPropID_Evf_WhiteBalance): return CanonProperty("Evf_WhiteBalance", EdsPropertyID(kEdsPropID_Evf_WhiteBalance)) + case EdsPropertyID(kEdsPropID_Evf_ColorTemperature): return CanonProperty("Evf_ColorTemperature", EdsPropertyID(kEdsPropID_Evf_ColorTemperature)) + case EdsPropertyID(kEdsPropID_Evf_DepthOfFieldPreview): return CanonProperty("Evf_DepthOfFieldPreview", EdsPropertyID(kEdsPropID_Evf_DepthOfFieldPreview)) + case EdsPropertyID(kEdsPropID_Evf_Zoom): return CanonProperty("Evf_Zoom", EdsPropertyID(kEdsPropID_Evf_Zoom)) + case EdsPropertyID(kEdsPropID_Evf_ZoomPosition): return CanonProperty("Evf_ZoomPosition", EdsPropertyID(kEdsPropID_Evf_ZoomPosition)) + case EdsPropertyID(kEdsPropID_Evf_Histogram): return CanonProperty("Evf_Histogram", EdsPropertyID(kEdsPropID_Evf_Histogram)) + case EdsPropertyID(kEdsPropID_Evf_ImagePosition): return CanonProperty("Evf_ImagePosition", EdsPropertyID(kEdsPropID_Evf_ImagePosition)) + case EdsPropertyID(kEdsPropID_Evf_HistogramStatus): return CanonProperty("Evf_HistogramStatus", EdsPropertyID(kEdsPropID_Evf_HistogramStatus)) + case EdsPropertyID(kEdsPropID_Evf_AFMode): return CanonProperty("Evf_AFMode", EdsPropertyID(kEdsPropID_Evf_AFMode)) + case EdsPropertyID(kEdsPropID_Record): return CanonProperty("Record", EdsPropertyID(kEdsPropID_Record)) + case EdsPropertyID(kEdsPropID_Evf_HistogramY): return CanonProperty("Evf_HistogramY", EdsPropertyID(kEdsPropID_Evf_HistogramY)) + case EdsPropertyID(kEdsPropID_Evf_HistogramR): return CanonProperty("Evf_HistogramR", EdsPropertyID(kEdsPropID_Evf_HistogramR)) + case EdsPropertyID(kEdsPropID_Evf_HistogramG): return CanonProperty("Evf_HistogramG", EdsPropertyID(kEdsPropID_Evf_HistogramG)) + case EdsPropertyID(kEdsPropID_Evf_HistogramB): return CanonProperty("Evf_HistogramB", EdsPropertyID(kEdsPropID_Evf_HistogramB)) + case EdsPropertyID(kEdsPropID_Evf_CoordinateSystem): return CanonProperty("Evf_CoordinateSystem", EdsPropertyID(kEdsPropID_Evf_CoordinateSystem)) + case EdsPropertyID(kEdsPropID_Evf_ZoomRect): return CanonProperty("Evf_ZoomRect", EdsPropertyID(kEdsPropID_Evf_ZoomRect)) + case EdsPropertyID(kEdsPropID_Evf_ImageClipRect): return CanonProperty("Evf_ImageClipRect", EdsPropertyID(kEdsPropID_Evf_ImageClipRect)) + case EdsPropertyID(kEdsPropID_Evf_PowerZoom_CurPosition): return CanonProperty("Evf_PowerZoom_CurPosition", EdsPropertyID(kEdsPropID_Evf_PowerZoom_CurPosition)) + case EdsPropertyID(kEdsPropID_Evf_PowerZoom_MaxPosition): return CanonProperty("Evf_PowerZoom_MaxPosition", EdsPropertyID(kEdsPropID_Evf_PowerZoom_MaxPosition)) + case EdsPropertyID(kEdsPropID_Evf_PowerZoom_MinPosition): return CanonProperty("Evf_PowerZoom_MinPosition", EdsPropertyID(kEdsPropID_Evf_PowerZoom_MinPosition)) + case EdsPropertyID(kEdsPropID_TempStatus): return CanonProperty("TempStatus", EdsPropertyID(kEdsPropID_TempStatus)) + case EdsPropertyID(kEdsPropID_EVF_RollingPitching): return CanonProperty("EVF_RollingPitching", EdsPropertyID(kEdsPropID_EVF_RollingPitching)) + case EdsPropertyID(kEdsPropID_FixedMovie): return CanonProperty("FixedMovie", EdsPropertyID(kEdsPropID_FixedMovie)) + case EdsPropertyID(kEdsPropID_MovieParam): return CanonProperty("MovieParam", EdsPropertyID(kEdsPropID_MovieParam)) + case EdsPropertyID(kEdsPropID_Evf_ClickWBCoeffs): return CanonProperty("Evf_ClickWBCoeffs", EdsPropertyID(kEdsPropID_Evf_ClickWBCoeffs)) + case EdsPropertyID(kEdsPropID_ManualWhiteBalanceData): return CanonProperty("ManualWhiteBalanceData", EdsPropertyID(kEdsPropID_ManualWhiteBalanceData)) + case EdsPropertyID(kEdsPropID_MirrorUpSetting): return CanonProperty("MirrorUpSetting", EdsPropertyID(kEdsPropID_MirrorUpSetting)) + case EdsPropertyID(kEdsPropID_MirrorLockUpState): return CanonProperty("MirrorLockUpState", EdsPropertyID(kEdsPropID_MirrorLockUpState)) + case EdsPropertyID(kEdsPropID_UTCTime): return CanonProperty("UTCTime", EdsPropertyID(kEdsPropID_UTCTime)) + case EdsPropertyID(kEdsPropID_TimeZone): return CanonProperty("TimeZone", EdsPropertyID(kEdsPropID_TimeZone)) + case EdsPropertyID(kEdsPropID_SummerTimeSetting): return CanonProperty("SummerTimeSetting", EdsPropertyID(kEdsPropID_SummerTimeSetting)) + case EdsPropertyID(kEdsPropID_DC_Zoom): return CanonProperty("DC_Zoom", EdsPropertyID(kEdsPropID_DC_Zoom)) + case EdsPropertyID(kEdsPropID_DC_Strobe): return CanonProperty("DC_Strobe", EdsPropertyID(kEdsPropID_DC_Strobe)) + case EdsPropertyID(kEdsPropID_LensBarrelStatus): return CanonProperty("LensBarrelStatus", EdsPropertyID(kEdsPropID_LensBarrelStatus)) + default: + return CanonProperty("Unknown", EdsPropertyID(kEdsPropID_Unknown)) + } +} diff --git a/Imago/Client.swift b/Imago/Client.swift new file mode 100644 index 0000000..9e49405 --- /dev/null +++ b/Imago/Client.swift @@ -0,0 +1,141 @@ +// +// Client.swift +// Imago +// +// Created by Nolan Brown on 7/21/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation + +class Client : Conductor2 { + + private lazy var _registerRetryTimer: DispatchSourceTimer = { + let interval: DispatchTimeInterval = DispatchTimeInterval.seconds(1) + let timer = DispatchSource.makeTimerSource() + timer.setEventHandler(handler: register) + timer.schedule(deadline: .now() + interval, repeating: interval) + return timer + }() + private var _retryingRegistration: Bool = false + private var _messagePort: CFMessagePort? + var _identifier: String + var _server: Server + var onRegistration : ((Client) -> Void)? = nil + var receivedNewFrame : ((Frame) -> Void)? = nil + + + override init(serverName: String? = nil) { + _identifier = UUID().uuidString + _server = Server(serverName: "\(serverName!).\(_identifier)") + super.init(serverName: serverName) + _server.receivedNewFrameData = receivedNewFrameDataHandler + _server.start() + } + + override func isClient() -> Bool { + return true + } + + func invalidate() { + _registerRetryTimer.suspend() + _messagePort = nil + } + + public func register() { + if _messagePort == nil { + if let port = CFMessagePortCreateRemote(nil, serverName() as CFString) { + dlog("CONNECTED \(port)") + _messagePort = port + _registerRetryTimer.suspend() + _retryingRegistration = false + } + else { + dlog("NOT CONNECTED \(self)") + + // We couldn't connect to the remote port. Maybe it hasn't been opened yet?? + if _retryingRegistration == false { + _retryingRegistration = true + _registerRetryTimer.resume() + } + + return + } + + } + + let request = Request(port: _messagePort!, callerID: .Register, str: _identifier) + let response = sendRequest(request) + if response.isSuccess() { + let id = response.asString()! + didRegister(id) + } + else { + dlog("Error during registration: \(response)") + // We should retry here.. + } + } + + private func didRegister(_ identifier: String) { + if identifier != _identifier { + dlog("Invalid registration identifier found \(identifier) != \(_identifier)") + } + else if onRegistration != nil { + DispatchQueue.main.async { [unowned self] in + self.onRegistration!(self) + } + } + } + + func receivedNewFrameDataHandler(_ data: Data) { + let pre_deserailize = mach_absolute_time() + + let frame = Frame.deserialize(fromData: data) + let post_deserailize = mach_absolute_time() + flog(frame!, "deserailize began", timestamp: pre_deserailize) + flog(frame!, "deserailize ended", timestamp: post_deserailize) + + if receivedNewFrame != nil { + receivedNewFrame!(frame!) + } + } + + func getFrame() -> Frame? { + guard let _messagePort = _messagePort else { + dlog("_messagePort is nil") + self.register() + return nil + } + let prerequest = mach_absolute_time() + let request = Request(port: _messagePort, callerID: .GetFrame, str: _identifier) + let response = sendRequest(request) + let postrequest = mach_absolute_time() + + if response.isSuccess() { + let data = response.data + if data!.count > 0 { + let pre_deserailize = mach_absolute_time() + + let frame = Frame.deserialize(fromData: data!) + let post_deserailize = mach_absolute_time() + + flog(frame!, "request began", timestamp: prerequest) + flog(frame!, "request completed", timestamp: postrequest) + flog(frame!, "deserailize began", timestamp: pre_deserailize) + flog(frame!, "deserailize ended", timestamp: post_deserailize) + + return frame + } + } + else { + dlog("Server error response: \(response) ") + + } + return nil + } + + func _registerRetryHandler() { + self.register() + } + +} diff --git a/Imago/Conductor.swift b/Imago/Conductor.swift new file mode 100644 index 0000000..a85239d --- /dev/null +++ b/Imago/Conductor.swift @@ -0,0 +1,348 @@ +// +// Conductor.swift +// Imago +// +// Created by Nolan Brown on 7/3/20. +// + + + +class Conductor { + private var _serverName: String + + private let _timeout: Double = 10.0 + + internal let _runLoopMode: CFRunLoopMode = CFRunLoopMode.defaultMode //CFRunLoopMode.commonModes + + private lazy var _registerRetryTimer: DispatchSourceTimer = { + let interval: DispatchTimeInterval = DispatchTimeInterval.seconds(1) + let timer = DispatchSource.makeTimerSource() + timer.setEventHandler(handler: register) + timer.schedule(deadline: .now() + interval, repeating: interval) + return timer + }() + + private var _serverPort: CFMessagePort? + private var _registeredClients: Set = [] + + private var _retryingRegistration: Bool = false + + var _identifier: String + + var onRegistration : ((Client) -> Void)? = nil + var receivedNewFrame : ((Frame) -> Void)? = nil + + public init(serverName: String? = nil) { + + if serverName == nil { + _serverName = Bundle.main.bundleIdentifier! + } + else { + _serverName = serverName! + } + dlog("Using \(_serverName) for Conductor Server") + + } + + func serverName() -> String { + return _serverName + } + + + + func sendRequest(_ request: Request) -> Response { + let messageID = request.messageID() + let port = request.port + + let requestData = request.asCFData() + + + + var responseRawData: Unmanaged? + + let status = CFMessagePortSendRequest(port, + messageID, + requestData, + _timeout, + _timeout, + _runLoopMode.rawValue, + &responseRawData) + + var responseData: Data? + if responseRawData != nil { + responseData = responseRawData!.takeRetainedValue() as Data + } + + return Response(callerID: request.callerID, status: status, data: responseData) + } + + // PUBLIC METHODS + func start() { + // Don't try to start the publisher again + if _messagePort != nil { + return + } + + let (createdPort, createdRunLoopSource) = createLocalPort(serverName()) + _messagePort = createdPort + _runLoopSource = createdRunLoopSource + dlog("Started Server at \(serverName()): \(_messagePort)") + } + + func stop() { + if _messagePort != nil { + CFMessagePortInvalidate(_messagePort) + _messagePort = nil + } + if _runLoopSource != nil { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), _runLoopSource, _runLoopMode) + _runLoopSource = nil + } + } + func sendFrameToClients(_ frame: Frame) { + guard let data = frame.serialize() else { + print("no frame data \(frame)") + return + } + for client in _registeredClients { + let request = Request(port: client, callerID: .ReceiveFrame, data: data) + let response = sendRequest(request) + if response.isSuccess() { + print("response \(response)") + } + } + } + + private func addClient(_ identifier: String) { + _registeredClientIDs.insert(identifier) + + let clientName = "\(serverName()).\(identifier)" + + if let port = CFMessagePortCreateRemote(nil, clientName as CFString) { + if !_registeredClients.contains(port) { + _registeredClients.insert(port) + } + } + } + + fileprivate func receivedRequest(_ data: Data?, _ callerID: CallerID) -> Data? { + + // let receivedData: Data? + // var identifier: String? + // // We will always expected publisher data to be an indentifier + // if data != nil && CFDataGetLength(data!) > 0 { + // let receivedData = data! as Data + // identifier = String(data: receivedData, encoding: .utf8)! + // } + + switch callerID { + case .Register: + guard let id_data = data else { + return nil + } + let identifier = String(data: id_data, encoding: .utf8)! + + dlog("Recieved \(callerID) request from client \(identifier)") + + addClient(identifier) + return identifier.data(using: .utf8) + + case .GetFrame: + + if getFrameData != nil { + return getFrameData!() + } + case .ReceiveFrame: + guard let frame_data = data else { + return nil + } + receivedNewFrameData?(frame_data) + return "OK".data(using: .utf8) + default: + break + } + return nil + } + + private func createLocalPort(_ portName: String) -> (CFMessagePort?, CFRunLoopSource?) { + var context = CFMessagePortContext(version: 0, info: Unmanaged.passUnretained(self).toOpaque(), retain: nil, release: nil, copyDescription: nil) + if let port = CFMessagePortCreateLocal(nil, portName as CFString, PortRequestCallback, &context, nil) { + //CFMessagePortSetInvalidationCallBack(port, PortInvalidationCallback) + + let runLoopSource = CFMessagePortCreateRunLoopSource(nil, port, 0); + + CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, _runLoopMode); + + CFMessagePortSetDispatchQueue(port, _queue) + + return (port, runLoopSource) + } + return (nil, nil) + } + + + + func invalidate() { + _registerRetryTimer.suspend() + _messagePort = nil + } + + // + + public func register() { + if _messagePort == nil { + if let port = CFMessagePortCreateRemote(nil, serverName() as CFString) { + dlog("CONNECTED \(port)") + _messagePort = port + _registerRetryTimer.suspend() + _retryingRegistration = false + } + else { + dlog("NOT CONNECTED \(self)") + + // We couldn't connect to the remote port. Maybe it hasn't been opened yet?? + if _retryingRegistration == false { + _retryingRegistration = true + _registerRetryTimer.resume() + } + + return + } + + } + + let request = Request(port: _messagePort!, callerID: .Register, str: _identifier) + let response = sendRequest(request) + if response.isSuccess() { + let id = response.asString()! + didRegister(id) + } + else { + dlog("Error during registration: \(response)") + // We should retry here.. + } + } + + private func didRegister(_ identifier: String) { + if identifier != _identifier { + dlog("Invalid registration identifier found \(identifier) != \(_identifier)") + } + else if onRegistration != nil { + DispatchQueue.main.async { [unowned self] in + self.onRegistration!(self) + } + } + } + + func receivedNewFrameDataHandler(_ data: Data) { + let pre_deserailize = mach_absolute_time() + + let frame = Frame.deserialize(fromData: data) + let post_deserailize = mach_absolute_time() + flog(frame!, "deserailize began", timestamp: pre_deserailize) + flog(frame!, "deserailize ended", timestamp: post_deserailize) + + if receivedNewFrame != nil { + receivedNewFrame!(frame!) + } + } + + func getFrame() -> Frame? { + guard let _messagePort = _messagePort else { + dlog("_messagePort is nil") + self.register() + return nil + } + let prerequest = mach_absolute_time() + let request = Request(port: _messagePort, callerID: .GetFrame, str: _identifier) + let response = sendRequest(request) + let postrequest = mach_absolute_time() + + if response.isSuccess() { + let data = response.data + if data!.count > 0 { + let pre_deserailize = mach_absolute_time() + + let frame = Frame.deserialize(fromData: data!) + let post_deserailize = mach_absolute_time() + + flog(frame!, "request began", timestamp: prerequest) + flog(frame!, "request completed", timestamp: postrequest) + flog(frame!, "deserailize began", timestamp: pre_deserailize) + flog(frame!, "deserailize ended", timestamp: post_deserailize) + + return frame + } + } + else { + dlog("Server error response: \(response) ") + + } + return nil + } + + func _registerRetryHandler() { + self.register() + } +} + + +class Conductor2 { + private var _serverName: String + + private let _timeout: Double = 10.0 + + internal let _runLoopMode: CFRunLoopMode = CFRunLoopMode.defaultMode //CFRunLoopMode.commonModes + + public init(serverName: String? = nil) { + + if serverName == nil { + _serverName = Bundle.main.bundleIdentifier! + } + else { + _serverName = serverName! + } + dlog("Using \(_serverName) for Conductor Server") + + } + + func serverName() -> String { + return _serverName + } + + func isServer() -> Bool { + return false + } + + func isClient() -> Bool { + return !isServer() + } + + + func sendRequest(_ request: Request) -> Response { + let messageID = request.messageID() + let port = request.port + + let requestData = request.asCFData() + + + + var responseRawData: Unmanaged? + + let status = CFMessagePortSendRequest(port, + messageID, + requestData, + _timeout, + _timeout, + _runLoopMode.rawValue, + &responseRawData) + + var responseData: Data? + if responseRawData != nil { + responseData = responseRawData!.takeRetainedValue() as Data + } + + return Response(callerID: request.callerID, status: status, data: responseData) + } + +} diff --git a/Imago/Extensions/CIImage+Filter.swift b/Imago/Extensions/CIImage+Filter.swift new file mode 100644 index 0000000..d0f43c7 --- /dev/null +++ b/Imago/Extensions/CIImage+Filter.swift @@ -0,0 +1,62 @@ +// +// CIImage+Filter.swift +// Imago +// +// Created by Nolan Brown on 8/10/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import CoreImage + +// Filter conviences +extension CIImage { + func mirror() -> CIImage? { + return self.oriented(.upMirrored) + } + + func pixellate(scale: Int = 15 ) -> CIImage? { + if let currentFilter = CIFilter(name: "CIPixellate") { + currentFilter.setValue(self, forKey: kCIInputImageKey) + currentFilter.setValue(scale, forKey: kCIInputScaleKey) + + let v = self.centerVector() + currentFilter.setValue(v, forKey: kCIInputCenterKey) + + return currentFilter.outputImage + } + return nil + } + + func thermalize() -> CIImage? { + if let currentFilter = CIFilter(name: "CIThermal") { + currentFilter.setValue(self, forKey: kCIInputImageKey) + return currentFilter.outputImage + } + return nil + } + + + func findFaceRects(_ context: CIContext, padding: Float = 30 ) -> [CGRect] { + let detector: CIDetector! = CIDetector(ofType: CIDetectorTypeFace, + context: context, + options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) + let features = detector.features(in: self, options: nil) + + var faces : [CGRect] = [] + for faceFeature in features { + var faceRect = faceFeature.bounds + faceRect = faceRect.insetBy(dx: CGFloat(padding) , dy: CGFloat(padding)) + faces.append(faceRect) + + } + + return faces + } + + func centerVector() -> CIVector { + return CIVector(cgPoint: CGPoint(x: self.extent.width/2, y: self.extent.height / 2) ) + + } +} + diff --git a/Imago/Extensions/Frame+Decompress.swift b/Imago/Extensions/Frame+Decompress.swift new file mode 100644 index 0000000..d47e694 --- /dev/null +++ b/Imago/Extensions/Frame+Decompress.swift @@ -0,0 +1,65 @@ +// +// Frame+Decompress.swift +// Imago +// +// Created by Nolan Brown on 7/19/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation + +func TJPAD(_ width: Int32) -> Int32 { + return (((width) + 3) & (~3)) +} + +extension Frame { + + static func decompress(_ bytes: UnsafeMutableRawPointer?, length: UInt) -> Frame? { + let _decompressor: tjhandle = tjInitDecompress() + var width: Int32 = 0 + var height: Int32 = 0 + + let rawBytes: UnsafeMutablePointer = bytes!.assumingMemoryBound(to: UInt8.self) + let result = tjDecompressHeader(_decompressor, + rawBytes, + length, + &width, &height) + if result == 0 { + + let encoding = TJPF_BGRA.rawValue + + // We can't access tjPixelSize directly because it lacks a type in turbojpeg and becomes a tuple in Swift + var tjPixelSizes = tjPixelSize + let pixelSize = withUnsafeBytes(of: &tjPixelSizes) { (rawPtr) -> Int32 in + let ptr = rawPtr.baseAddress!.assumingMemoryBound(to: Int32.self) + return ptr[Int(encoding)] + } + let wanted_bpr = TJPAD(pixelSize * width) + + let bufferSize = Int(wanted_bpr * height) + let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) + let result = tjDecompress2(_decompressor, rawBytes, length, buffer, width, wanted_bpr, height, encoding, 0); + + // Use TJFLAG_BOTTOMUP when rendering in PreviewMetalView + //let result = tjDecompress2(_decompressor, rawBytes, length, buffer, width, wanted_bpr, height, TJPF_BGRA.rawValue, TJFLAG_BOTTOMUP); + + if (result == 0) + { + let data = Data(bytes: buffer, count: bufferSize) + + let imgData = Frame(data:data, + height: Int(height), + width: Int(width), + bytesPerRow: Int(wanted_bpr), + pixelSize: Int(pixelSize)) + + free(buffer) + tjDestroy(_decompressor) + return imgData + } + } + tjDestroy(_decompressor) + + return nil + } +} diff --git a/Imago/Headers/Imago-Bridging-Header.h b/Imago/Headers/Imago-Bridging-Header.h new file mode 100644 index 0000000..3aa27bb --- /dev/null +++ b/Imago/Headers/Imago-Bridging-Header.h @@ -0,0 +1,23 @@ +// +// Imago-Bridging-Header.h +// Imago +// +// Created by Nolan Brown on 7/3/20. +// + +#ifndef Imago_Bridging_Header_h +#define Imago_Bridging_Header_h + +#ifndef __MACOS__ +#define __MACOS__ +#endif + +#import "EDSDK.h" +#import + +#import +#import +#import + + +#endif /* Imago_Bridging_Header_h */ diff --git a/Imago/Headers/Imago-PrefixHeader.pch b/Imago/Headers/Imago-PrefixHeader.pch new file mode 100644 index 0000000..dd750d7 --- /dev/null +++ b/Imago/Headers/Imago-PrefixHeader.pch @@ -0,0 +1,17 @@ +// +// PrefixHeader.pch +// Imago +// +// Created by Nolan Brown on 7/2/20. +// + +#ifndef PrefixHeader_pch +#define PrefixHeader_pch + +// Include any system framework and library headers here that should be included in all compilation units. +// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file. +#ifndef __MACOS__ +#define __MACOS__ +#endif + +#endif /* PrefixHeader_pch */ diff --git a/Imago/Info.plist b/Imago/Info.plist new file mode 100644 index 0000000..d6c4c14 --- /dev/null +++ b/Imago/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 0.1 + CFBundleSignature + ???? + CFBundleVersion + 385 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2020 Nolan Brown. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + NSSupportsAutomaticTermination + + NSSupportsSuddenTermination + + + diff --git a/Imago/Preview Content/Preview Assets.xcassets/Contents.json b/Imago/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Imago/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Imago/Server.swift b/Imago/Server.swift new file mode 100644 index 0000000..7929e7c --- /dev/null +++ b/Imago/Server.swift @@ -0,0 +1,184 @@ +// +// Publisher.swift +// Imago +// +// Created by Nolan Brown on 7/16/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation + +class Server : Conductor2 { + + fileprivate var _messagePort: CFMessagePort? + fileprivate var _runLoopSource: CFRunLoopSource? + fileprivate var _queue: DispatchQueue = DispatchQueue(label: "com.nolanbrown.Imago.Server", qos: .userInteractive) + + //@objc weak var delegate: ConductorProtocol? + fileprivate var _registeredClientIDs: Set = [] + fileprivate var _registeredClients: Set = [] + + var subscribedHandler : (() -> Void)? = nil + var getFrameData : (() -> Data?)? = nil + + var receivedNewFrameData : ((Data) -> Void)? = nil + + deinit { + stop() + } + + override func isServer() -> Bool { + return true + } + + + // PUBLIC METHODS + func start() { + // Don't try to start the publisher again + if _messagePort != nil { + return + } + + let (createdPort, createdRunLoopSource) = createLocalPort(serverName()) + _messagePort = createdPort + _runLoopSource = createdRunLoopSource + dlog("Started Server at \(serverName()): \(_messagePort)") + } + + func stop() { + if _messagePort != nil { + CFMessagePortInvalidate(_messagePort) + _messagePort = nil + } + if _runLoopSource != nil { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), _runLoopSource, _runLoopMode) + _runLoopSource = nil + } + } + func sendFrameToClients(_ frame: Frame) { + guard let data = frame.serialize() else { + print("no frame data \(frame)") + return + } + for client in _registeredClients { + let request = Request(port: client, callerID: .ReceiveFrame, data: data) + let response = sendRequest(request) + if response.isSuccess() { + print("response \(response)") + } + } + } + + private func addClient(_ identifier: String) { + _registeredClientIDs.insert(identifier) + + let clientName = "\(serverName()).\(identifier)" + + if let port = CFMessagePortCreateRemote(nil, clientName as CFString) { + if !_registeredClients.contains(port) { + _registeredClients.insert(port) + } + } + } + + fileprivate func receivedRequest(_ data: Data?, _ callerID: CallerID) -> Data? { + +// let receivedData: Data? +// var identifier: String? +// // We will always expected publisher data to be an indentifier +// if data != nil && CFDataGetLength(data!) > 0 { +// let receivedData = data! as Data +// identifier = String(data: receivedData, encoding: .utf8)! +// } + + switch callerID { + case .Register: + guard let id_data = data else { + return nil + } + let identifier = String(data: id_data, encoding: .utf8)! + + dlog("Recieved \(callerID) request from client \(identifier)") + + addClient(identifier) + return identifier.data(using: .utf8) + + case .GetFrame: + + if getFrameData != nil { + return getFrameData!() + } + case .ReceiveFrame: + guard let frame_data = data else { + return nil + } + receivedNewFrameData?(frame_data) + return "OK".data(using: .utf8) + default: + break + } + return nil + } + + private func createLocalPort(_ portName: String) -> (CFMessagePort?, CFRunLoopSource?) { + var context = CFMessagePortContext(version: 0, info: Unmanaged.passUnretained(self).toOpaque(), retain: nil, release: nil, copyDescription: nil) + if let port = CFMessagePortCreateLocal(nil, portName as CFString, PortRequestCallback, &context, nil) { + //CFMessagePortSetInvalidationCallBack(port, PortInvalidationCallback) + + let runLoopSource = CFMessagePortCreateRunLoopSource(nil, port, 0); + + CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, _runLoopMode); + + CFMessagePortSetDispatchQueue(port, _queue) + + return (port, runLoopSource) + } + return (nil, nil) + } +} + +let PortInvalidationCallback : CFMessagePortInvalidationCallBack = { + (port: CFMessagePort?, info: UnsafeMutableRawPointer?) -> Void in + print("PortInvalidationCallback \(port)") +} + +/* + CFMessagePortCallBack + + All CFMessagePortCreateLocal methods are provided PortRequestCallback as the callback function. It's here we do: + 1. Determine what type the request is (ie CallerID) + 2. Load the related initialized Conductor object + 3. Process the request + + */ +let PortRequestCallback : CFMessagePortCallBack = { + (port: CFMessagePort?, messageID: Int32, data: CFData?, info: UnsafeMutableRawPointer?) -> Unmanaged? in + + let callbackID = CallerID(rawValue: messageID) + + var conductorInstance : Server? + if info != nil { + conductorInstance = unsafeBitCast(info, to: Server.self) + //conductorInstance = info!.load(as: Conductor.self) + } + if conductorInstance == nil { + print("ERROR: Conductor Instance Unavailable") + return nil + } + + var receivedData: Data? + if data != nil && CFDataGetLength(data!) > 0 { + receivedData = data! as Data + } + + var portName = conductorInstance!.serverName() + + guard let callerID = callbackID else { + return nil + } + + guard let responseData = conductorInstance!.receivedRequest(receivedData, callerID) else { + return nil + } + return Unmanaged.passRetained(responseData as CFData) +} diff --git a/Imago/Stream.swift b/Imago/Stream.swift new file mode 100644 index 0000000..367a28d --- /dev/null +++ b/Imago/Stream.swift @@ -0,0 +1,138 @@ +// +// Stream.swift +// Imago +// +// Created by Nolan Brown on 7/29/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import CoreImage +import AppKit + +enum StreamSetting { + case Pixellate + case Thermalize + +} + +// Coordinates frames to a registered plugin and any rendering that's required +class Stream : Hashable, Identifiable, ObservableObject { + + + var name: String? + var id: String = UUID().uuidString + var settings: [StreamSetting] = [] + + + private var _pubsub: PubSub = PubSub(publisherPortName:IMAGO_SERVER_NAME) + + private var _activeCamera: Camera? + + private var _context: CIContext = CIContext() + private var _bufferPool: CVPixelBufferPool? + + private var _fps: Int = 30 + + private var _frameQueue: [Frame] = [] + + + fileprivate var _processingQueue: DispatchQueue = DispatchQueue(label: "com.Imago.Camera", qos: .userInteractive) + + + static func == (lhs: Stream, rhs: Stream) -> Bool { + return lhs.id == rhs.id + } + public func hash(into hasher: inout Hasher) { + return hasher.combine(ObjectIdentifier(self).hashValue) + } + + + func getPubSubMetrics() -> Dictionary { + + var metrics: [String: Any] = _pubsub.getSubscriberMetrics() + metrics["IsRunning"] = _pubsub.isRunning() + return metrics + + } + + func start() { + _pubsub.start() + } + + func stop() { + _pubsub.stop() + if _bufferPool != nil { + CVPixelBufferPoolFlush(_bufferPool!, []) + } + } + + // Use frames from this camera to publish + func makeCameraActive(_ camera: Camera?) { + if camera != nil { + _activeCamera?.setActive(active: false) + camera!.didReceiveFrame = self.didReceiveFrame + camera!.setActive(active: true) + _activeCamera = camera + } + else { + _activeCamera?.setActive(active: false) + _activeCamera = nil + } + + } + + /* + Callback from the active Camera containing the latest Frame + We process the frame before publishing to any registered subscribers + */ + func didReceiveFrame(_ frame: Frame, _ camera: Camera) { + _processingQueue.async { [unowned self, frame, camera] in + if let newFrame = self.prepareFrameForPublication(frame, camera) { + if let data = newFrame.serialize() { + self._pubsub.publishFrameData(data) + } + } + } + } + + func getCachedPixelBufferPool(_ frame: Frame) -> CVPixelBufferPool? { + if _bufferPool != nil { + return _bufferPool + } + _bufferPool = CVPixelBufferPool.create(size: frame.getSize()) + return _bufferPool + } + + + /* + We will perform any neccessary processing to prepare the frame for publication to subscribers + Frame's provided as input haven't had their data converted to a CVPixelBuffer yet + + */ + func prepareFrameForPublication(_ frame: Frame, _ camera: Camera) -> Frame? { + var createdFrame: Frame? + let pixelBufferPool = getCachedPixelBufferPool(frame) + + autoreleasepool { + if let ciImage = frame.buildCIImage() { + + let processedImage = camera.filter.processImage(ciImage) + + + if let pixelBuffer = processedImage.toPixelBuffer(withContext: _context, fromPool: pixelBufferPool) { + + if let data = pixelBuffer.toData() { + var newFrame = frame.copy() + newFrame.data = data + newFrame.isDataPixelBuffer = true + createdFrame = newFrame + } + } + } + + } + return createdFrame + } + +} diff --git a/Imago/Tests/ImagoTests/ImagoTests.swift b/Imago/Tests/ImagoTests/ImagoTests.swift new file mode 100644 index 0000000..058881e --- /dev/null +++ b/Imago/Tests/ImagoTests/ImagoTests.swift @@ -0,0 +1,34 @@ +// +// ImagoTests.swift +// ImagoTests +// +// Created by Nolan Brown on 7/3/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import XCTest +@testable import Imago + +class ImagoTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Imago/Tests/ImagoTests/Info.plist b/Imago/Tests/ImagoTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/Imago/Tests/ImagoTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Imago/Tests/ImagoUITests/ImagoUITests.swift b/Imago/Tests/ImagoUITests/ImagoUITests.swift new file mode 100644 index 0000000..a48893d --- /dev/null +++ b/Imago/Tests/ImagoUITests/ImagoUITests.swift @@ -0,0 +1,43 @@ +// +// ImagoUITests.swift +// ImagoUITests +// +// Created by Nolan Brown on 7/3/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import XCTest + +class ImagoUITests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { + XCUIApplication().launch() + } + } + } +} diff --git a/Imago/Tests/ImagoUITests/Info.plist b/Imago/Tests/ImagoUITests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/Imago/Tests/ImagoUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Imago/ViewController.swift b/Imago/ViewController.swift new file mode 100644 index 0000000..8e715bd --- /dev/null +++ b/Imago/ViewController.swift @@ -0,0 +1,179 @@ +// +// ViewController.swift +// Imago +// +// Created by Nolan Brown on 7/29/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import Combine +import SwiftUI +import IOKit.pwr_mgt + +/* +Manages available cameras and links them to a Stream. +*/ +class ViewController: NSObject, ObservableObject, NSWindowDelegate { + @Published var cameras: [Camera] = [] + + private var _activeStream: Stream + + var newFrameHandler : ((Frame) -> Void)? = nil + + override init() { + _activeStream = Stream() + super.init() + + _activeStream.start() + + NotificationCenter.default.addObserver(self, selector: #selector(onCameraAddedEvent(_:)), name: .cameraAddedEvent, object: nil) + _disableSleep() + } + + + deinit { + self.teardown() + } + + /* + Action for UI + */ + func loadCameras() -> Void { + CanonCamera.loadCameras() + } + + + /* + Called from the UI onAppear to do initial setup + */ + func setup() { + CanonCamera.setup() + Thread.sleep(forTimeInterval: 0.5) + self.loadCameras() + } + + /* + Called when closing the application + */ + func teardown() { + setActiveCamera(nil) + CanonCamera.teardown() + } + + func setActiveCamera(_ camera: Camera?) { + if camera != nil { + _activeStream.makeCameraActive(camera!) + } + else { + _activeStream.makeCameraActive(nil) + } + } + + + private func _addNewCameras(_ newCameras: [Camera]) { + var cameraSet = Set(self.cameras) + for cam in newCameras { + if !cameraSet.contains(cam) { + cameraSet.insert(cam) + } + } + self.cameras = Array(cameraSet) + self.cameras.sort { (c1, c2) -> Bool in + if c1.name > c2.name { + return true + } + return false + } + + } + + + private func _disableSleep() { + var noSleepAssertion: IOPMAssertionID = IOPMAssertionID(0) + if (noSleepAssertion == 0) + { + let reasonForActivity = "Imago maintaining connection to camera" as CFString + + _ = IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleSystemSleep as CFString?, + IOPMAssertionLevel(kIOPMAssertionLevelOn), + reasonForActivity, &noSleepAssertion) + } + + } + + @objc func onCameraAddedEvent(_ notification: Notification) + { + if let cameras = notification.userInfo?["Cameras"] as? [Camera] { + self._addNewCameras(cameras) + } + } +} + +/* +extension ViewController { + + fileprivate var _isLiveViewActive = false + fileprivate var _liveViewWindow : NSWindow? + + func showLiveViewWindow(_ camera: Camera) { + + if self.activeCamera != nil && self._liveViewWindow == nil { + + self._liveViewWindow = NSWindow( + contentRect: NSRect(x: 300, y: 300, width: 300, height: 300), + styleMask: [.titled, .closable, .miniaturizable], // .resizable, .fullSizeContentView + backing: .buffered, defer: false) + let view = LiveImageView(containerWindow: _liveViewWindow, camera:self.activeCamera!) + let hostingView = NSHostingView(rootView: view) + self._liveViewWindow!.center() + self._liveViewWindow!.contentView = hostingView + self._liveViewWindow!.delegate = self + self._liveViewWindow!.makeKeyAndOrderFront(nil) + self._isLiveViewActive = true + self.startLiveStream() + } + } + func windowWillClose(_ notification: Notification) { + self._isLiveViewActive = false + //self.activeCamera!.stopLiveStream() + } +} +*/ + +/* + For future use to manage application settings + */ +struct Settings : Codable { + var showPreviewFrame: Bool + var autoConnectToLastUsedCamera: Bool + + private enum CodingKeys: String, CodingKey { + case showPreviewFrame + case autoConnectToLastUsedCamera + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + showPreviewFrame = try values.decode(Bool.self, forKey: .showPreviewFrame) + autoConnectToLastUsedCamera = try values.decode(Bool.self, forKey: .autoConnectToLastUsedCamera) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(showPreviewFrame, forKey: .showPreviewFrame) + try container.encode(autoConnectToLastUsedCamera, forKey: .autoConnectToLastUsedCamera) + } + +// func save() { +// let encoder = PropertyListEncoder() +// encoder.outputFormat = .xml +// do { +// let data = try encoder.encode(someSettings) +// try data.write(to: settingsURL) +// } catch { +// // Handle error +// print(error) +// } +// } +} diff --git a/Imago/Views/CameraFilterMenu.swift b/Imago/Views/CameraFilterMenu.swift new file mode 100644 index 0000000..c6bd0fc --- /dev/null +++ b/Imago/Views/CameraFilterMenu.swift @@ -0,0 +1,65 @@ +// +// CameraFilterMenu.swift +// Imago +// +// Created by Nolan Brown on 7/3/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import SwiftUI + +struct CameraFilterMenu: View { + var camera: Camera + @State var filter: CameraFilter = .None + + func toggleFilter(_ newFilter: CameraFilter) { + if newFilter == .None { + self.filter = .None + } + else { + if self.filter.contains(newFilter) { + self.filter.remove(newFilter) + } + else { + self.filter.insert(newFilter) + } + } + + self.camera.filter = self.filter + } + + func filterName(_ filter: CameraFilter) -> String { + var filterName = CameraFilter.debugDescriptions[filter] ?? "--" + let checkbox = "✓" + if self.filter.contains(filter) { + filterName = "\(checkbox) \(filterName)" + } + return filterName + } + + + var body: some View { + MenuButton("Filter") { + ForEach(CameraFilter.Options, id: \.hashValue) { f in + Button(self.filterName(f)) { self.toggleFilter(f) } + } + }.buttonStyle(DetectHover()) + } +} + +/* + From https://stackoverflow.com/questions/59837991/how-to-get-accentcolor-background-for-menu-items-in-swiftui-with-reduced-transpa + */ +struct DetectHover: ButtonStyle { + @State private var hovering: Bool = false + + public func makeBody(configuration: DetectHover.Configuration) -> some View { + configuration.label + .foregroundColor(self.hovering ? Color.white : Color.primary) + .background(self.hovering ? Color.blue : Color.clear) + .onHover { hover in + self.hovering = hover + } + } +} diff --git a/Imago/Views/CameraRow.swift b/Imago/Views/CameraRow.swift new file mode 100644 index 0000000..a8c65af --- /dev/null +++ b/Imago/Views/CameraRow.swift @@ -0,0 +1,61 @@ +// +// CameraRow.swift +// Imago +// +// Created by Nolan Brown on 7/3/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import SwiftUI + +struct CameraRow: View { + @EnvironmentObject var viewController: ViewController + + @ObservedObject var camera: Camera + @State var filter: CameraFilter = .None + + var body: some View { + VStack { + HStack(alignment: .center, spacing: 6) { + Circle() + .fill(camera.streaming ? Color.green : Color.red) + .frame(width: 9, height: 9).padding([.leading], 6.0) + Spacer() + VStack { + + Text(camera.name) + + HStack { + if self.camera.streaming { + CameraFilterMenu(camera: camera) + } else { + Button(action: { + if self.camera.streaming { + self.viewController.setActiveCamera(nil) + } + else { + self.viewController.setActiveCamera(self.camera) + } + }) { + Text(camera.streaming ? "Stop" : "Start") + } + } + } + + } + Spacer() + Image("CameraIcon").frame(width: 50, height: 50).padding([.trailing], 6.0) + + }.frame(maxWidth: .infinity).padding([.all], 6.0) + + }.background(RoundedRectangle(cornerRadius: 5, style: .continuous).fill(Color.white)).padding([.all], 6.0).onTapGesture { + if self.camera.streaming { + self.viewController.setActiveCamera(nil) + } + else { + self.viewController.setActiveCamera(self.camera) + } + } + } +} diff --git a/Imago/Views/ContentView.swift b/Imago/Views/ContentView.swift new file mode 100644 index 0000000..ab8301d --- /dev/null +++ b/Imago/Views/ContentView.swift @@ -0,0 +1,30 @@ +// +// ContentView.swift +// Imago +// +// Created by Nolan Brown on 7/3/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import SwiftUI + +struct ContentView: View { + @EnvironmentObject var viewController: ViewController + + var body: some View { + GeometryReader { geometry in + NavigationView { + ScrollView() { + ForEach(self.viewController.cameras, id: \.identifier) { camera in + CameraRow(camera: camera).tag(camera.identifier).padding([.all], 6.0) + } + }.frame(width:geometry.size.width) + + }.onAppear { + self.viewController.setup() + } + } + } +} + + diff --git a/Imago/Views/LivePreview.swift b/Imago/Views/LivePreview.swift new file mode 100644 index 0000000..d1b5405 --- /dev/null +++ b/Imago/Views/LivePreview.swift @@ -0,0 +1,45 @@ +// +// LivePreview.swift +// Imago +// +// Created by Nolan Brown on 8/10/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import SwiftUI + +struct LivePreview: View { + @ObservedObject var camera: Camera + @State var expanded: Bool = false + @State var size: NSSize = NSSize(width: 150, height: 100) + + var isPreviewAvailable: Bool { + return false + //return camera.streaming && camera.previewPixelBuffer != nil + } + + func getPreviewImage() -> NSImage? { +// if isPreviewAvailable { +// return NSImage(cgImage: camera.previewPixelBuffer!.toImage()!, size: camera.previewPixelBuffer!.size) +// } + return nil + } + + var body: some View { + VStack { + if isPreviewAvailable { + Image(nsImage: self.getPreviewImage()!) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width:size.width, height:size.height) + .scaledToFit() + .padding([.trailing], 6.0) + } + else { + Image("CameraIcon").frame(width: 50, height: 50).padding([.trailing], 6.0) + } + } + } +} + diff --git a/Imago/Views/TitlebarAccesory.swift b/Imago/Views/TitlebarAccesory.swift new file mode 100644 index 0000000..78bc0cb --- /dev/null +++ b/Imago/Views/TitlebarAccesory.swift @@ -0,0 +1,27 @@ +// +// TitlebarAccesory.swift +// Imago +// +// Created by Nolan Brown on 8/10/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import SwiftUI + +struct TitlebarAccessory: View { + @EnvironmentObject private var viewController: ViewController + + var body: some View { + HStack(alignment: .top) { + Button(action: { + self.viewController.loadCameras() + }) { + Text("Load Cameras") + } + + + }.frame(minHeight: 30, maxHeight: 30) + + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2e41e00 --- /dev/null +++ b/LICENSE @@ -0,0 +1,47 @@ +MIT License + +Copyright (c) 2020 Nolan Brown + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +# SimpleDALPlugin +MIT License + +Copyright (c) 2020 John Boiles +Copyright (c) 2020 Ryohei Ikegami + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Plugin/Device.swift b/Plugin/Device.swift new file mode 100644 index 0000000..c6d157c --- /dev/null +++ b/Plugin/Device.swift @@ -0,0 +1,45 @@ +// +// Device.swift +// SimpleDALPlugin +// +// Created by 池上涼平 on 2020/04/25. +// Copyright © 2020 com.seanchas116. All rights reserved. +// + +import Foundation +import IOKit + +class Device: Object { + var objectID: CMIOObjectID = 0 + var streamID: CMIOStreamID = 0 + let name = PLUGIN_DEVICE_NAME + let manufacturer = PLUGIN_DEVICE_MANUFACTURER + let deviceUID = PLUGIN_DEVICE_UID + let modelUID = PLUGIN_DEVICE_MODEL_UID + var excludeNonDALAccess: Bool = false + var deviceMaster: Int32 = -1 + + lazy var properties: [Int : Property] = [ + kCMIOObjectPropertyName: Property(name), + kCMIOObjectPropertyManufacturer: Property(manufacturer), + kCMIODevicePropertyDeviceUID: Property(deviceUID), + kCMIODevicePropertyModelUID: Property(modelUID), + kCMIODevicePropertyTransportType: Property(UInt32(kIOAudioDeviceTransportTypeBuiltIn)), + kCMIODevicePropertyDeviceIsAlive: Property(UInt32(1)), + kCMIODevicePropertyDeviceIsRunning: Property(UInt32(1)), + kCMIODevicePropertyDeviceIsRunningSomewhere: Property(UInt32(1)), + kCMIODevicePropertyDeviceCanBeDefaultDevice: Property(UInt32(1)), + kCMIODevicePropertyCanProcessAVCCommand: Property(UInt32(0)), + kCMIODevicePropertyCanProcessRS422Command: Property(UInt32(0)), + kCMIODevicePropertyHogMode: Property(Int32(-1)), + kCMIODevicePropertyStreams: Property { [unowned self] in self.streamID }, + kCMIODevicePropertyExcludeNonDALAccess: Property( + getter: { [unowned self] () -> UInt32 in self.excludeNonDALAccess ? 1 : 0 }, + setter: { [unowned self] (value: UInt32) -> Void in self.excludeNonDALAccess = value != 0 } + ), + kCMIODevicePropertyDeviceMaster: Property( + getter: { [unowned self] () -> Int32 in self.deviceMaster }, + setter: { [unowned self] (value: Int32) -> Void in self.deviceMaster = value } + ), + ] +} diff --git a/Plugin/Headers/ImagoPlugin-Bridging-Header.h b/Plugin/Headers/ImagoPlugin-Bridging-Header.h new file mode 100644 index 0000000..80bfa17 --- /dev/null +++ b/Plugin/Headers/ImagoPlugin-Bridging-Header.h @@ -0,0 +1,14 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + + +#ifndef Imago_Plugin_Bridging_Header_h +#define Imago_Plugin_Bridging_Header_h + +#import +#import +#import + + +#endif diff --git a/Plugin/Headers/PrefixHeader.pch b/Plugin/Headers/PrefixHeader.pch new file mode 100644 index 0000000..cf54a07 --- /dev/null +++ b/Plugin/Headers/PrefixHeader.pch @@ -0,0 +1,22 @@ +// +// PrefixHeader.pch +// Imago +// +// Created by Nolan Brown on 7/2/20. +// + +#ifndef PrefixHeader_pch +#define PrefixHeader_pch + +// Include any system framework and library headers here that should be included in all compilation units. +// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file. +#ifndef __MACOS__ +#define __MACOS__ +#endif + +#ifdef __OBJC__ + #import +#endif +//#include "bootstrap_priv.h" + +#endif /* PrefixHeader_pch */ diff --git a/Plugin/Info.plist b/Plugin/Info.plist new file mode 100644 index 0000000..4e24119 --- /dev/null +++ b/Plugin/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + + CFBundleVersion + + CFPlugInFactories + + 1E367E04-F5D5-40AE-AB2F-39E96ADDAEF8 + ImagoPluginMain + + CFPlugInTypes + + 30010C1C-93BF-11D8-8B5B-000A95AF9C6A + + 1E367E04-F5D5-40AE-AB2F-39E96ADDAEF8 + + + CMIOHardwareAssistantServiceNames + + com.nolanbrown.Imago + + NSHumanReadableCopyright + Copyright © 2020 Nolan Brown. All rights reserved. + NSPrincipalClass + + + diff --git a/Plugin/LICENSE b/Plugin/LICENSE new file mode 100644 index 0000000..8191a47 --- /dev/null +++ b/Plugin/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020 John Boiles +Copyright (c) 2020 Ryohei Ikegami + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Plugin/Main.swift b/Plugin/Main.swift new file mode 100644 index 0000000..7b4fbba --- /dev/null +++ b/Plugin/Main.swift @@ -0,0 +1,14 @@ +// +// Main.swift +// Imago +// +// Created by Nolan Brown on 7/3/20. +// + +import Foundation +import CoreMediaIO + +@_cdecl("ImagoPluginMain") +public func ImagoPluginMain(allocator: CFAllocator, requestedTypeUUID: CFUUID) -> CMIOHardwarePlugInRef { + return pluginRef +} diff --git a/Plugin/Object.swift b/Plugin/Object.swift new file mode 100644 index 0000000..af531c5 --- /dev/null +++ b/Plugin/Object.swift @@ -0,0 +1,55 @@ +// +// Object.swift +// SimpleDALPlugin +// +// Created by 池上涼平 on 2020/04/25. +// Copyright © 2020 com.seanchas116. All rights reserved. +// + +import Foundation + +protocol Object: class { + var objectID: CMIOObjectID { get } + var properties: [Int: Property] { get } +} + +extension Object { + func hasProperty(address: CMIOObjectPropertyAddress) -> Bool { + return properties[Int(address.mSelector)] != nil + } + + func isPropertySettable(address: CMIOObjectPropertyAddress) -> Bool { + guard let property = properties[Int(address.mSelector)] else { + return false + } + return property.isSettable + } + + func getPropertyDataSize(address: CMIOObjectPropertyAddress) -> UInt32 { + guard let property = properties[Int(address.mSelector)] else { + return 0 + } + return property.dataSize + } + + func getPropertyData(address: CMIOObjectPropertyAddress, dataSize: inout UInt32, data: UnsafeMutableRawPointer) { + guard let property = properties[Int(address.mSelector)] else { + return + } + dataSize = property.dataSize + property.getData(data: data) + } + + func setPropertyData(address: CMIOObjectPropertyAddress, data: UnsafeRawPointer) { + guard let property = properties[Int(address.mSelector)] else { + return + } + property.setData(data: data) + } +} + +var objects = [CMIOObjectID: Object]() + +func addObject(object: Object) { + objects[object.objectID] = object +} diff --git a/Plugin/Plugin.swift b/Plugin/Plugin.swift new file mode 100644 index 0000000..64aafb9 --- /dev/null +++ b/Plugin/Plugin.swift @@ -0,0 +1,18 @@ +// +// Plugin.swift +// SimpleDALPlugin +// +// Created by 池上涼平 on 2020/04/25. +// Copyright © 2020 com.seanchas116. All rights reserved. +// + +import Foundation + +class Plugin: Object { + var objectID: CMIOObjectID = 0 + let name = PLUGIN_NAME + + lazy var properties: [Int : Property] = [ + kCMIOObjectPropertyName: Property(name), + ] +} diff --git a/Plugin/PluginInterface.swift b/Plugin/PluginInterface.swift new file mode 100644 index 0000000..8658846 --- /dev/null +++ b/Plugin/PluginInterface.swift @@ -0,0 +1,279 @@ +// +// PluginInterface.swift +// SimpleDALPlugin +// +// Created by 池上涼平 on 2020/04/25. +// Copyright © 2020 com.seanchas116. All rights reserved. +// + +import Foundation + +private func QueryInterface(plugin: UnsafeMutableRawPointer?, uuid: REFIID, interface: UnsafeMutablePointer?) -> HRESULT { + //dlog() + let pluginRefPtr = UnsafeMutablePointer(OpaquePointer(interface)) + pluginRefPtr?.pointee = pluginRef + return HRESULT(noErr) +} + +private func AddRef(plugin: UnsafeMutableRawPointer?) -> ULONG { + //dlog() + return 0 +} + +private func Release(plugin: UnsafeMutableRawPointer?) -> ULONG { + //dlog() + return 0 +} + +private func Initialize(plugin: CMIOHardwarePlugInRef?) -> OSStatus { + //dlog() + return OSStatus(kCMIOHardwareIllegalOperationError) +} + +private func InitializeWithObjectID(plugin: CMIOHardwarePlugInRef?, objectID: CMIOObjectID) -> OSStatus { + //dlog() + guard let plugin = plugin else { + return OSStatus(kCMIOHardwareIllegalOperationError) + } + + var error = noErr + + let pluginObject = Plugin() + pluginObject.objectID = objectID + addObject(object: pluginObject) + + let device = Device() + error = CMIOObjectCreate(plugin, CMIOObjectID(kCMIOObjectSystemObject), CMIOClassID(kCMIODeviceClassID), &device.objectID) + guard error == noErr else { + dlog("error: \(error)") + return error + } + addObject(object: device) + + let stream = Stream() + error = CMIOObjectCreate(plugin, device.objectID, CMIOClassID(kCMIOStreamClassID), &stream.objectID) + guard error == noErr else { + dlog("error: \(error)") + return error + } + addObject(object: stream) + + device.streamID = stream.objectID + + error = CMIOObjectsPublishedAndDied(plugin, CMIOObjectID(kCMIOObjectSystemObject), 1, &device.objectID, 0, nil) + guard error == noErr else { + dlog("error: \(error)") + return error + } + + error = CMIOObjectsPublishedAndDied(plugin, device.objectID, 1, &stream.objectID, 0, nil) + guard error == noErr else { + dlog("error: \(error)") + return error + } + + return noErr +} +private func Teardown(plugin: CMIOHardwarePlugInRef?) -> OSStatus { + //dlog() + return noErr +} +private func ObjectShow(plugin: CMIOHardwarePlugInRef?, objectID: CMIOObjectID) { + //dlog() +} + +private func ObjectHasProperty(plugin: CMIOHardwarePlugInRef?, objectID: CMIOObjectID, address: UnsafePointer?) -> DarwinBoolean { + //dlog(address?.pointee.mSelector) + guard let address = address?.pointee else { + dlog("Address is nil") + return false + } + guard let object = objects[objectID] else { + dlog("Object not found") + return false + } + return DarwinBoolean(object.hasProperty(address: address)) +} + +private func ObjectIsPropertySettable(plugin: CMIOHardwarePlugInRef?, objectID: CMIOObjectID, address: UnsafePointer?, isSettable: UnsafeMutablePointer?) -> OSStatus { + //dlog(address?.pointee.mSelector) + guard let address = address?.pointee else { + dlog("Address is nil") + return OSStatus(kCMIOHardwareBadObjectError) + } + guard let object = objects[objectID] else { + dlog("Object not found") + return OSStatus(kCMIOHardwareBadObjectError) + } + let settable = object.isPropertySettable(address: address) + isSettable?.pointee = DarwinBoolean(settable) + return noErr +} + +private func ObjectGetPropertyDataSize(plugin: CMIOHardwarePlugInRef?, objectID: CMIOObjectID, address: UnsafePointer?, qualifiedDataSize: UInt32, qualifiedData: UnsafeRawPointer?, dataSize: UnsafeMutablePointer?) -> OSStatus { + //dlog(address?.pointee.mSelector) + guard let address = address?.pointee else { + dlog("Address is nil") + return OSStatus(kCMIOHardwareBadObjectError) + } + guard let object = objects[objectID] else { + dlog("Object not found") + return OSStatus(kCMIOHardwareBadObjectError) + } + dataSize?.pointee = object.getPropertyDataSize(address: address) + return noErr +} + +private func ObjectGetPropertyData(plugin: CMIOHardwarePlugInRef?, objectID: CMIOObjectID, address: UnsafePointer?, qualifiedDataSize: UInt32, qualifiedData: UnsafeRawPointer?, dataSize: UInt32, dataUsed: UnsafeMutablePointer?, data: UnsafeMutableRawPointer?) -> OSStatus { + //dlog(address?.pointee.mSelector) + guard let address = address?.pointee else { + dlog("Address is nil") + return OSStatus(kCMIOHardwareBadObjectError) + } + guard let object = objects[objectID] else { + dlog("Object not found") + return OSStatus(kCMIOHardwareBadObjectError) + } + guard let data = data else { + dlog("data is nil") + return OSStatus(kCMIOHardwareBadObjectError) + } + var dataUsed_: UInt32 = 0 + object.getPropertyData(address: address, dataSize: &dataUsed_, data: data) + dataUsed?.pointee = dataUsed_ + return noErr +} + +private func ObjectSetPropertyData(plugin: CMIOHardwarePlugInRef?, objectID: CMIOObjectID, address: UnsafePointer?, qualifiedDataSize: UInt32, qualifiedData: UnsafeRawPointer?, dataSize: UInt32, data: UnsafeRawPointer?) -> OSStatus { + //dlog() + + guard let address = address?.pointee else { + dlog("Address is nil") + return OSStatus(kCMIOHardwareBadObjectError) + } + guard let object = objects[objectID] else { + dlog("Object not found") + return OSStatus(kCMIOHardwareBadObjectError) + } + guard let data = data else { + dlog("data is nil") + return OSStatus(kCMIOHardwareBadObjectError) + } + object.setPropertyData(address: address, data: data) + return noErr +} + +private func DeviceSuspend(plugin: CMIOHardwarePlugInRef?, deviceID: CMIODeviceID) -> OSStatus { + //dlog() + return noErr +} + +private func DeviceResume(plugin: CMIOHardwarePlugInRef?, deviceID: CMIODeviceID) -> OSStatus { + //dlog() + return noErr +} + +private func DeviceStartStream(plugin: CMIOHardwarePlugInRef?, deviceID: CMIODeviceID, streamID: CMIOStreamID) -> OSStatus { + //dlog() + guard let stream = objects[streamID] as? Stream else { + dlog("no stream") + return OSStatus(kCMIOHardwareBadObjectError) + } + stream.start() + return noErr +} + +private func DeviceStopStream(plugin: CMIOHardwarePlugInRef?, deviceID: CMIODeviceID, streamID: CMIOStreamID) -> OSStatus { + //dlog() + guard let stream = objects[streamID] as? Stream else { + dlog("no stream") + return OSStatus(kCMIOHardwareBadObjectError) + } + stream.stop() + return noErr +} + +private func DeviceProcessAVCCommand(plugin: CMIOHardwarePlugInRef?, deviceID: CMIODeviceID, avcCommand: UnsafeMutablePointer?) -> OSStatus { + //dlog() + return OSStatus(kCMIOHardwareIllegalOperationError) +} + +private func DeviceProcessRS422Command(plugin: CMIOHardwarePlugInRef?, deviceID: CMIODeviceID, rs422Command: UnsafeMutablePointer?) -> OSStatus { + //dlog() + return OSStatus(kCMIOHardwareIllegalOperationError) +} + +private func StreamCopyBufferQueue(plugin: CMIOHardwarePlugInRef?, streamID: CMIOStreamID, queueAlteredProc: CMIODeviceStreamQueueAlteredProc?, queueAlteredRefCon: UnsafeMutableRawPointer?, queueOut: UnsafeMutablePointer?>?) -> OSStatus { + //dlog() + guard let queueOut = queueOut else { + dlog("no queueOut") + return OSStatus(kCMIOHardwareBadObjectError) + } + guard let stream = objects[streamID] as? Stream else { + dlog("no stream") + return OSStatus(kCMIOHardwareBadObjectError) + } + guard let queue = stream.copyBufferQueue(queueAlteredProc: queueAlteredProc, queueAlteredRefCon: queueAlteredRefCon) else { + dlog("no queue") + return OSStatus(kCMIOHardwareBadObjectError) + } + queueOut.pointee = Unmanaged.passRetained(queue) + return noErr +} + +private func StreamDeckPlay(plugin: CMIOHardwarePlugInRef?, streamID: CMIOStreamID) -> OSStatus { + //dlog() + return OSStatus(kCMIOHardwareIllegalOperationError) +} + +private func StreamDeckStop(plugin: CMIOHardwarePlugInRef?, streamID: CMIOStreamID) -> OSStatus { + //dlog() + return OSStatus(kCMIOHardwareIllegalOperationError) +} + +private func StreamDeckJog(plugin: CMIOHardwarePlugInRef?, streamID: CMIOStreamID, speed: Int32) -> OSStatus { + //dlog() + return OSStatus(kCMIOHardwareIllegalOperationError) +} + +private func StreamDeckCueTo(plugin: CMIOHardwarePlugInRef?, streamID: CMIOStreamID, requestedTimecode: Float64, playOnCue: DarwinBoolean) -> OSStatus { + //dlog() + return OSStatus(kCMIOHardwareIllegalOperationError) +} + +private func createPluginInterface() -> CMIOHardwarePlugInInterface { + return CMIOHardwarePlugInInterface( + _reserved: nil, + QueryInterface: QueryInterface, + AddRef: AddRef, + Release: Release, + Initialize: Initialize, + InitializeWithObjectID: InitializeWithObjectID, + Teardown: Teardown, + ObjectShow: ObjectShow, + ObjectHasProperty: ObjectHasProperty, + ObjectIsPropertySettable: ObjectIsPropertySettable, + ObjectGetPropertyDataSize: ObjectGetPropertyDataSize, + ObjectGetPropertyData: ObjectGetPropertyData, + ObjectSetPropertyData: ObjectSetPropertyData, + DeviceSuspend: DeviceSuspend, + DeviceResume: DeviceResume, + DeviceStartStream: DeviceStartStream, + DeviceStopStream: DeviceStopStream, + DeviceProcessAVCCommand: DeviceProcessAVCCommand, + DeviceProcessRS422Command: DeviceProcessRS422Command, + StreamCopyBufferQueue: StreamCopyBufferQueue, + StreamDeckPlay: StreamDeckPlay, + StreamDeckStop: StreamDeckStop, + StreamDeckJog: StreamDeckJog, + StreamDeckCueTo: StreamDeckCueTo) +} + +let pluginRef: CMIOHardwarePlugInRef = { + let interfacePtr = UnsafeMutablePointer.allocate(capacity: 1) + interfacePtr.pointee = createPluginInterface() + + let pluginRef = CMIOHardwarePlugInRef.allocate(capacity: 1) + pluginRef.pointee = interfacePtr + return pluginRef +}() diff --git a/Plugin/Property.swift b/Plugin/Property.swift new file mode 100644 index 0000000..1401df6 --- /dev/null +++ b/Plugin/Property.swift @@ -0,0 +1,155 @@ +// +// Property.swift +// SimpleDALPlugin +// +// Created by 池上涼平 on 2020/04/25. +// Copyright © 2020 com.seanchas116. All rights reserved. +// + +import Foundation + +protocol PropertyValue { + var dataSize: UInt32 { get } + func toData(data: UnsafeMutableRawPointer) + static func fromData(data: UnsafeRawPointer) -> Self +} + +extension String: PropertyValue { + var dataSize: UInt32 { + return UInt32(MemoryLayout.size) + } + func toData(data: UnsafeMutableRawPointer) { + let cfString = self as CFString + let unmanagedCFString = Unmanaged.passRetained(cfString) + UnsafeMutablePointer>(OpaquePointer(data)).pointee = unmanagedCFString + } + static func fromData(data: UnsafeRawPointer) -> Self { + fatalError("not implemented") + } +} + +extension CMFormatDescription: PropertyValue { + var dataSize: UInt32 { + return UInt32(MemoryLayout.size) + } + func toData(data: UnsafeMutableRawPointer) { + let unmanaged = Unmanaged.passRetained(self as! Self) + UnsafeMutablePointer>(OpaquePointer(data)).pointee = unmanaged + } + static func fromData(data: UnsafeRawPointer) -> Self { + fatalError("not implemented") + } +} + +extension CFArray: PropertyValue { + var dataSize: UInt32 { + return UInt32(MemoryLayout.size) + } + func toData(data: UnsafeMutableRawPointer) { + let unmanaged = Unmanaged.passRetained(self as! Self) + UnsafeMutablePointer>(OpaquePointer(data)).pointee = unmanaged + } + static func fromData(data: UnsafeRawPointer) -> Self { + fatalError("not implemented") + } +} + +struct CFTypeRefWrapper { + let ref: CFTypeRef +} + +extension CFTypeRefWrapper: PropertyValue { + var dataSize: UInt32 { + return UInt32(MemoryLayout.size) + } + func toData(data: UnsafeMutableRawPointer) { + let unmanaged = Unmanaged.passRetained(ref) + UnsafeMutablePointer>(OpaquePointer(data)).pointee = unmanaged + } + static func fromData(data: UnsafeRawPointer) -> Self { + fatalError("not implemented") + } +} + +extension UInt32: PropertyValue { + var dataSize: UInt32 { + return UInt32(MemoryLayout.size) + } + func toData(data: UnsafeMutableRawPointer) { + UnsafeMutablePointer(OpaquePointer(data)).pointee = self + } + static func fromData(data: UnsafeRawPointer) -> Self { + return UnsafePointer(OpaquePointer(data)).pointee + } +} + +extension Int32: PropertyValue { + var dataSize: UInt32 { + return UInt32(MemoryLayout.size) + } + func toData(data: UnsafeMutableRawPointer) { + UnsafeMutablePointer(OpaquePointer(data)).pointee = self + } + static func fromData(data: UnsafeRawPointer) -> Self { + return UnsafePointer(OpaquePointer(data)).pointee + } +} + +extension Float64: PropertyValue { + var dataSize: UInt32 { + return UInt32(MemoryLayout.size) + } + func toData(data: UnsafeMutableRawPointer) { + UnsafeMutablePointer(OpaquePointer(data)).pointee = self + } + static func fromData(data: UnsafeRawPointer) -> Self { + return UnsafePointer(OpaquePointer(data)).pointee + } +} + +extension AudioValueRange: PropertyValue { + var dataSize: UInt32 { + return UInt32(MemoryLayout.size) + } + func toData(data: UnsafeMutableRawPointer) { + UnsafeMutablePointer(OpaquePointer(data)).pointee = self + } + static func fromData(data: UnsafeRawPointer) -> Self { + return UnsafePointer(OpaquePointer(data)).pointee + } +} + +class Property { + let getter: () -> PropertyValue + let setter: ((UnsafeRawPointer) -> Void)? + + var isSettable: Bool { + return setter != nil + } + + var dataSize: UInt32 { + getter().dataSize + } + + convenience init(_ value: Element) { + self.init(getter: { value }) + } + + convenience init(getter: @escaping () -> Element) { + self.init(getter: getter, setter: nil) + } + + init(getter: @escaping () -> Element, setter: ((Element) -> Void)?) { + self.getter = getter + self.setter = (setter != nil) ? { data in setter?(Element.fromData(data: data)) } : nil + } + + func getData(data: UnsafeMutableRawPointer) { + let value = getter() + value.toData(data: data) + } + + func setData(data: UnsafeRawPointer) { + setter?(data) + } +} diff --git a/Plugin/Stream.swift b/Plugin/Stream.swift new file mode 100644 index 0000000..0f09560 --- /dev/null +++ b/Plugin/Stream.swift @@ -0,0 +1,179 @@ +// +// Stream.swift +// SimpleDALPlugin +// +// Created by 池上涼平 on 2020/04/25. +// Copyright © 2020 com.seanchas116. All rights reserved. +// + +import Foundation +import CoreImage + + +class Stream: Object { + var objectID: CMIOObjectID = 0 + let name = PLUGIN_STREAM_NAME + let width = 1280 + let height = 720 + private var _sequenceNumber: UInt64 = 0 + + private var frameRate = 30 + + private var queueAlteredProc: CMIODeviceStreamQueueAlteredProc? + private var queueAlteredRefCon: UnsafeMutableRawPointer? + + private var _imagoStream: ImagoStream + private lazy var _context: CIContext = { + return CIContext() + }() + + private lazy var formatDescription: CMVideoFormatDescription? = { + var formatDescription: CMVideoFormatDescription? + let error = CMVideoFormatDescriptionCreate( + allocator: kCFAllocatorDefault, + codecType: kCVPixelFormatType_32ARGB, + width: Int32(width), height: Int32(height), + extensions: nil, + formatDescriptionOut: &formatDescription) + guard error == noErr else { + dlog("CMVideoFormatDescriptionCreate Error: \(error)") + return nil + } + return formatDescription + }() + + private lazy var clock: CFTypeRef? = { + var clock: Unmanaged? = nil + + let error = CMIOStreamClockCreate( + kCFAllocatorDefault, + "\(name) clock" as CFString, + Unmanaged.passUnretained(self).toOpaque(), + CMTimeMake(value: 1, timescale: 10), + 100, 10, + &clock); + guard error == noErr else { + dlog("CMIOStreamClockCreate Error: \(error)") + return nil + } + return clock?.takeUnretainedValue() + }() + + private lazy var queue: CMSimpleQueue? = { + var queue: CMSimpleQueue? + let error = CMSimpleQueueCreate( + allocator: kCFAllocatorDefault, + capacity: 30, + queueOut: &queue) + guard error == noErr else { + dlog("CMSimpleQueueCreate Error: \(error)") + return nil + } + return queue + }() + + lazy var properties: [Int : Property] = [ + kCMIOObjectPropertyName: Property(name), + kCMIOStreamPropertyFormatDescription: Property(formatDescription!), + kCMIOStreamPropertyFormatDescriptions: Property([formatDescription!] as CFArray), + kCMIOStreamPropertyDirection: Property(UInt32(0)), + kCMIOStreamPropertyFrameRate: Property(Float64(frameRate)), + kCMIOStreamPropertyFrameRates: Property(Float64(frameRate)), + kCMIOStreamPropertyMinimumFrameRate: Property(Float64(frameRate)), + kCMIOStreamPropertyFrameRateRanges: Property(AudioValueRange(mMinimum: Float64(frameRate), mMaximum: Float64(frameRate))), + kCMIOStreamPropertyClock: Property(CFTypeRefWrapper(ref: clock!)), + ] + + init() { + _imagoStream = ImagoStream() + frameRate = _imagoStream.framesPerSecond() + } + + func start() { + dlog("START STREAM") + + _imagoStream.receivedFrame = self.enqueueFrame + _imagoStream.start() + + } + + func stop() { + dlog("STOP STREAM") + _imagoStream.stop() + _imagoStream.receivedFrame = nil + } + + func enqueueFrame(frame: Frame) { + guard let queue = queue else { + dlog("queue is nil") + return + } + + guard CMSimpleQueueGetCount(queue) < CMSimpleQueueGetCapacity(queue) else { + dlog("queue is full") + return + } + + guard let pixelBuffer: CVPixelBuffer = frame.buildPixelBuffer(image: nil, inContext: _context) else { + dlog("pixel buffer couldn't be created") + return + } + + let fps = frameRate + let sequence = _sequenceNumber + let scale = UInt64(fps) * 100 + let duration = CMTime(value: CMTimeValue(scale / UInt64(fps)), timescale: CMTimeScale(scale)) + let timestamp = CMTime(value: duration.value * CMTimeValue(sequence), timescale: CMTimeScale(scale)) + + let timing: CMSampleTimingInfo = CMSampleTimingInfo( + duration: duration, + presentationTimeStamp: timestamp, + decodeTimeStamp: timestamp + ) + + + var error = noErr + + var formatDescription: CMFormatDescription? + error = CMVideoFormatDescriptionCreateForImageBuffer( + allocator: kCFAllocatorDefault, + imageBuffer: pixelBuffer, + formatDescriptionOut: &formatDescription) + guard error == noErr else { + dlog("CMVideoFormatDescriptionCreateForImageBuffer Error: \(error)") + return + } + + error = CMIOStreamClockPostTimingEvent(timing.decodeTimeStamp, mach_absolute_time(), true, clock) + guard error == noErr else { + dlog("CMSimpleQueueCreate Error: \(error)") + return + } + var frameTiming = timing + var sampleBufferUnmanaged: Unmanaged? = nil + error = CMIOSampleBufferCreateForImageBuffer( + kCFAllocatorDefault, + pixelBuffer, + formatDescription, + &frameTiming, + sequence, + UInt32(kCMIOSampleBufferNoDiscontinuities), + &sampleBufferUnmanaged + ) + guard error == noErr else { + dlog("CMIOSampleBufferCreateForImageBuffer Error: \(error)") + return + } + + CMSimpleQueueEnqueue(queue, element: sampleBufferUnmanaged!.toOpaque()) + queueAlteredProc?(objectID, sampleBufferUnmanaged!.toOpaque(), queueAlteredRefCon) + _sequenceNumber += 1 + } + + func copyBufferQueue(queueAlteredProc: CMIODeviceStreamQueueAlteredProc?, queueAlteredRefCon: UnsafeMutableRawPointer?) -> CMSimpleQueue? { + self.queueAlteredProc = queueAlteredProc + self.queueAlteredRefCon = queueAlteredRefCon + return self.queue + } + +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4aa2c5 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# Imago + +## Introduction + +This project is named after the [Imago Camera](https://en.wikipedia.org/wiki/Imago_camera) created by Werner Kraus, which was created to capture images at the life size of a subject. + +I created Imago to replace multiple apps that allowed me to use my [Canon M50](https://www.amazon.com/Canon-Mirrorless-Camera-EF-M15-45mm-Video/dp/B079Y45KTJ/) as a Virtual Camera for apps like Zoom. I was using the wonderful [Camera Live](https://github.com/v002/v002-Camera-Live) by [Tom Butterworth (@bangnoise)](https://github.com/bangnoise) and [CamTwist](http://camtwiststudio.com/), both of which have worked extraordinarily well. I encourage you to take a look at both to decide which is a better solution for you. + +The DAL plugin is based off of [SimpleDALPlugin](https://github.com/seanchas116/SimpleDALPlugin) by [Ryohei Ikegami (@seanchas116)](https://github.com/seanchas116), who migrated [CoreMediaIO Device Abstraction Layer Minimal Example](https://github.com/johnboiles/coremediaio-dal-minimal-example) by [John Boiles (@johnboiles)](https://github.com/johnboiles) into Swift. + +The UI is written in SwiftUI for macOS 10.15, so it is missing some of the nicities that were added for macOS 11. + +## System Requirements + +- macOS 10.15 +- Compatible Canon Camera + +## Architecture + +**Caveat:** _This is my first Swift app, so if there are better ways to do something please let me know._ + +The project is split into two Targets, _Imago_ is the macOS app and _ImagoPlugin_ is the Virtual Camera plugin (aka Device Abstraction Layer). + +Beyond the base implementation of the CMIO Plugin, _ImagoPlugin_ is only responsible for communicating with _Imago_ and reconstituing the raw data back into a `CVPixelBuffer`. This is accomplished by setting up dedicated inter-process communications using [CFMessagePort](https://developer.apple.com/documentation/corefoundation/cfmessageport-rs2). + +The _Imago_ app is responsible for all communications and data management between the Camera (via the EDSDK.framework), image decompression, and coordinates all the inter-process communications to active instances of _ImagoPlugin_. + +### Camera Communications +Communications between _Imago_ and the EDSDK can be summarized as: +1. Initialize EDSDK +2. Get the available list of Cameras +3. Connect to a selected Camera +4. Get required properties about the Camera +5. Set the Camera Output mode +5. Get Image Data + +All EDSDK calls are in the `CanonCamera` subclass of `Camera` where they're coordinated using a dedicated DispatchQueue. + +### Imago <=> Imago Plugin Communcations + +For a primer on inter-process communications, check out this [NSHipster post](https://nshipster.com/inter-process-communication/) by [@Mattt](https://nshipster.com/authors/mattt/). + +_Imago_ uses CFMessagePort, which in turn uses Mach Ports under the hood. These provide fast, dedicated communication channels between processes that allow _Imago_ to push image data directly into _ImagoPlugin_. The main limitation of them is they only provide only 1:1 communications. Given that there can be multiple instances of an _ImagoPlugin_ running if a user has multiple client programs running (for example Zoom and Google Meet in Chrome), _Imago_ implements a Publisher/Subscriber architecture to allow for dedicated 1:1 communication between multiple processes. There are other ways of acheving the same result, either by using lower level Mach Ports or higher level XPC, and passing an IOSurface object between processess; however both those ways are more complicated then using CFMessagePort. + + +1. Start Publisher at known port address (ie. `com.nolanbrown.Imago.conductor.publisher`) + - If a Publisher isn't available, the Subscriber will periodically attempt to connect until it's able to establish a connection +2. When a Subscriber is ready to connect to the Publisher, it will create a UUID and open a local port address using that ID (ie `com.nolanbrown.Imago.conductor.e02d59bd-16fa-41a0-bf70-70adfc02e877`) +3. When Publisher receives a `Register` message from a subscriber process , it will open a remote port the Subcriber at that ID +4. Publisher sends data to all confirmed Subscribers +5. A Subscriber will periodically `Register` with the Publisher to confirm that both connections + + + +## Building + + +To get started building _Imago_, first the required libraries must be downloaded. To simplify this process, you only need to run `setup.sh` that's in the root directory of this project. The script will install and setup `libturbo-jpeg` and the `Canon EOS SDK`, once that's completed you can build the project. + +_ImagoPlugin_ is set as a dependecy for _Imago_, so the plugin will be automatically built every time _Imago_ is. + +### Imago Plugin Installation +To use the _ImagoPlugin_, it needs to be installed in `/Library/CoreMediaIO/Plug-Ins/DAL/`. It can be tedious to manually copy the plugin so there is a Run Script build phase that will automatically move the newly built `ImagoPlugin.plugin` to that directory. + +The Run Script requires a sudoer password to be provided which is done via a query to the Keychain. To set this up for yourself, create a new entry in Keychain Access with both Item Name and Account Name set as `ImagoBuild` and enter a password for a Sudo user, or create a new Keychain Item from the command line with the command `security add-generic-password -a ImagoBuild -s ImagoBuild -w` and enter a password for Sudo user. + +Below is the executed Run Script to automatically install the built plugin. +``` +if [ $CONFIGURATION == "Debug" ];then + echo $(security find-generic-password -ga "ImagoBuild" -w) | sudo -S rm -R /Library/CoreMediaIO/Plug-Ins/DAL/$PRODUCT_NAME.plugin; + sudo cp -R $SYMROOT/$CONFIGURATION/$PRODUCT_NAME.plugin /Library/CoreMediaIO/Plug-Ins/DAL/$PRODUCT_NAME.plugin +fi +``` + +### Required Libaries + +#### EDSDK (Canon EOS SDK) +To download the EDSDK.framework, register for a developer account with Canon. For US developers, you can do that [here](https://developercommunity.usa.canon.com/canon) and then [download the SDK](https://developercommunity.usa.canon.com/canon?id=sdk_download). + +Once downloaded, unzip the file and open `Macintosh.dmg`. From the mounted disk image `Macintosh` copy `EDSDK/Framework/EDSDK.framework` to `/Frameworks/EDSDK/EDSDK.framework` and the `Header` directory to `/Frameworks/EDSDK/Header/` + + + +#### libjpeg-turbo + +You can download the latest version of the libjpeg-turbo binary [here](https://sourceforge.net/projects/libjpeg-turbo/files/) or visit their [website](https://libjpeg-turbo.org/Documentation/OfficialBinaries). + + + +## Debugging + +Use [Cameo](https://github.com/lvsti/Cameo) to load and test the _ImagoPlugin_ or use any application that allow for Virtual Cameras. + + +### Notes +The error `"The file “ic_hevcdec.framework” couldn’t be opened because there is no such file."` is part of EDSDK and can be ignored. + + +## Troubleshooting +- Connect your camera via USB directly to your computer, not through a USB hub or Dock +- Try a different USB cable +- Turn off any other apps that are communicating with the Camera (EOS Utility, Camera Live, etc) +- Turn off your camera and turn it back on again + + +## Credits + +### [Icon By KDesign](https://dribbble.com/shots/7485922-Abstract-icons) + +## TODO + +- [ ] Improve Plugin re-connection +- [ ] Test with more Canon Cameras +- [ ] Implement an iOS App to act as remote camera +- [ ] Add more filter options and ability to configure +- [ ] Add a Live Preview mode +- [ ] Add preferences pane +- [ ] Add support for other DSLR manufacturers +- [ ] Calculate and Display frame rate +- [ ] Fix UI bugs on macOS 11 diff --git a/Shared/Const.swift b/Shared/Const.swift new file mode 100644 index 0000000..74e11a2 --- /dev/null +++ b/Shared/Const.swift @@ -0,0 +1,22 @@ +// +// Const.swift +// Imago +// +// Created by Nolan Brown on 7/20/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation + +let PLUGIN_NAME = "Imago" + +let PLUGIN_DEVICE_NAME = PLUGIN_NAME +let PLUGIN_DEVICE_MANUFACTURER = "Nolan Brown" +let PLUGIN_DEVICE_UID = "Imago Plugin Device" +let PLUGIN_DEVICE_MODEL_UID = "Imago Plugin Model" + +let PLUGIN_STREAM_NAME = PLUGIN_NAME + +let IMAGO_SERVER_NAME = "com.nolanbrown.Imago.conductor" + +let DEFAULT_PIXEL_FORMAT = kCVPixelFormatType_32ARGB diff --git a/Shared/DLog.swift b/Shared/DLog.swift new file mode 100644 index 0000000..ec80990 --- /dev/null +++ b/Shared/DLog.swift @@ -0,0 +1,25 @@ +// +// DLog.swift +// Imago +// +// Created by Nolan Brown on 7/19/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation + +func dlog(_ message: Any = "", function: String = #function) { + NSLog("Imago: \(function): \(message)") +} + +func flog(_ frame: Frame, _ message: Any = "", timestamp: UInt64 = mach_absolute_time()) { + //let diff = TimeInterval(timestamp - UInt64(frame.timestamp)) / TimeInterval(NSEC_PER_SEC) + //NSLog("Frame \(frame.id): \(message) - \(diff)s") +} + +func printTimeElapsedWhenRunningCode(title:String, operation:()->()) { + let startTime = CFAbsoluteTimeGetCurrent() + operation() + let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + print("Time elapsed for \(title): \(timeElapsed) s.") +} diff --git a/Shared/Extensions/CIImage+Imago.swift b/Shared/Extensions/CIImage+Imago.swift new file mode 100644 index 0000000..4d903f2 --- /dev/null +++ b/Shared/Extensions/CIImage+Imago.swift @@ -0,0 +1,49 @@ +// +// CIImage+Imago.swift +// Imago +// +// Created by Nolan Brown on 7/29/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import CoreImage + +extension CIImage { + + func toCGImage(withContext context: CIContext) -> CGImage? { + return context.createCGImage(self, from: self.extent) + } + + func toPixelBuffer(withContext context: CIContext, fromPool pool: CVPixelBufferPool? = nil) -> CVPixelBuffer? { + let imageRect = self.extent + let size = imageRect.size + + + guard let pixelBuffer = CVPixelBuffer.create(withSize: size, fromPool: pool) else { + return nil + } + + let rect = NSMakeRect(0, 0, size.width, size.height); + let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + + CVPixelBufferLockBaseAddress(pixelBuffer, []) + + context.render(self, to:pixelBuffer, bounds: rect, colorSpace: rgbColorSpace) + + CVPixelBufferUnlockBaseAddress(pixelBuffer, []) + + return pixelBuffer + } + + func toJpegData(_ context: CIContext? = nil) -> Data? { + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let ctx: CIContext = context ?? CIContext() + + let options = [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0] + + let data = ctx.jpegRepresentation(of: self, colorSpace: colorSpace, options: options) + return data + } +} diff --git a/Shared/Extensions/CVPixelBuffer+Data.swift b/Shared/Extensions/CVPixelBuffer+Data.swift new file mode 100644 index 0000000..4d8ee10 --- /dev/null +++ b/Shared/Extensions/CVPixelBuffer+Data.swift @@ -0,0 +1,129 @@ +// +// CVPixelBuffer+Data.swift +// Imago +// +// Created by Nolan Brown on 7/29/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import VideoToolbox + + +extension CVPixelBuffer { + + var width: Int { + return CVPixelBufferGetWidth(self) + } + var height: Int { + return CVPixelBufferGetHeight(self) + } + + var size: NSSize { + return NSSize(width: self.width, height: self.height) + } + + func toImage() -> CGImage? { + var cgImage: CGImage? + VTCreateCGImageFromCVPixelBuffer(self, options: nil, imageOut: &cgImage) + + return cgImage + } + + func toData() -> Data? { + CVPixelBufferLockBaseAddress(self,.readOnly) + + let bufferSize = CVPixelBufferGetDataSize(self) + + // UnsafeMutableRawPointer + guard let baseAddress = CVPixelBufferGetBaseAddress(self) else { + return nil + } + + //let data = Data(bytesNoCopy: baseAddress, count: bufferSize, deallocator: .free) + let data = Data(bytes: baseAddress, count: bufferSize) + + CVPixelBufferUnlockBaseAddress(self, .readOnly) + return data + } + + static func fromData(_ data: Data, height: Int, width: Int) -> CVPixelBuffer? { + + // Create an empty pixel buffer + var _pixelBuffer: CVPixelBuffer? + var err = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32ARGB, nil, &_pixelBuffer) + + guard let pixelBuffer = _pixelBuffer else {return nil} + + // Generate the video format description from that pixel buffer + var format: CMFormatDescription? + err = CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: pixelBuffer, formatDescriptionOut: &format) + if (err != noErr) { + dlog("CMVideoFormatDescriptionCreateForImageBuffer err \(err)") + return nil + } + + // Copy memory into the pixel buffer + CVPixelBufferLockBaseAddress(pixelBuffer, []) + let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) +// let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) +// let srcBytesPerRow = width * 2 + + data.withUnsafeBytes { rawBufferPointer in + let rawPtr = rawBufferPointer.baseAddress! + + memcpy(baseAddress, rawPtr, data.count) + + } + + CVPixelBufferUnlockBaseAddress(pixelBuffer, []); + return pixelBuffer + } + + + static func create(withSize size:NSSize, fromPool pool: CVPixelBufferPool? = nil, attributes: [String:Any]? = nil, pixelFormat: OSType = DEFAULT_PIXEL_FORMAT) -> CVPixelBuffer? { + + var attrs = attributes + if attrs == nil { + attrs = CVPixelBuffer.defaultAttributes(additionalAttributes: attributes, size: size, pixelFormat: pixelFormat) + + } + + var pixelBuffer : CVPixelBuffer? + var status: CVReturn + + if pool != nil { + status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool!, &pixelBuffer) + } + else { + status = CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width), Int(size.height), pixelFormat, attrs! as CFDictionary, &pixelBuffer) + } + + if status != kCVReturnSuccess { + dlog("Failed to create CVPixelBuffer: \(status)") + } + + return pixelBuffer + } + + + static func defaultAttributes(additionalAttributes: [String:Any]? = nil, size: NSSize? = nil, pixelFormat: OSType = DEFAULT_PIXEL_FORMAT) -> Dictionary { + var attributes: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: pixelFormat, + kCVPixelBufferCGImageCompatibilityKey as String: kCFBooleanTrue!, + kCVPixelBufferCGBitmapContextCompatibilityKey as String: kCFBooleanTrue!, + //kCVPixelBufferMetalCompatibilityKey as String: kCFBooleanTrue! + ] + + if size != nil { + attributes[kCVPixelBufferWidthKey as String] = Int(size!.width) + attributes[kCVPixelBufferHeightKey as String] = Int(size!.height) + } + if additionalAttributes != nil { + attributes.merge(additionalAttributes!) { (_, new) in new } + } + + return attributes + } + +} diff --git a/Shared/Extensions/CVPixelBufferPool+Imago.swift b/Shared/Extensions/CVPixelBufferPool+Imago.swift new file mode 100644 index 0000000..e3d42be --- /dev/null +++ b/Shared/Extensions/CVPixelBufferPool+Imago.swift @@ -0,0 +1,25 @@ +// +// CVPixelBufferPool+Imago.swift +// Imago +// +// Created by Nolan Brown on 8/10/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation + +extension CVPixelBufferPool { + static func create(size: NSSize) -> CVPixelBufferPool? { + + let outputPixelBufferAttributes = CVPixelBuffer.defaultAttributes(additionalAttributes: nil, size: size, pixelFormat: DEFAULT_PIXEL_FORMAT) + + let poolAttributes = [kCVPixelBufferPoolMinimumBufferCountKey as String: 5] + var cvPixelBufferPool: CVPixelBufferPool? + // Create a pixel buffer pool with the same pixel attributes as the input format description + CVPixelBufferPoolCreate(kCFAllocatorDefault, + poolAttributes as NSDictionary?, + outputPixelBufferAttributes as NSDictionary?, + &cvPixelBufferPool) + return cvPixelBufferPool + } +} diff --git a/Shared/Frame.swift b/Shared/Frame.swift new file mode 100644 index 0000000..ca334e0 --- /dev/null +++ b/Shared/Frame.swift @@ -0,0 +1,286 @@ +// +// Frame.swift +// Imago +// +// Created by Nolan Brown on 7/16/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import SwiftUI + + +struct Frame { + let id: String + let height: Int + let width: Int + let bytesPerRow: Int + let pixelSize: Int + var data: Data + + var bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue + var timestamp: UInt64 = 0 + var fps: Int = 30 + var sequence: UInt64 = 0 + var isDataPixelBuffer: Bool = false + + + init(data: Data, height: Int, width: Int, bytesPerRow: Int, pixelSize: Int, id: String? = nil) { + self.id = id ?? UUID().uuidString + self.data = data + self.height = height + self.width = width + self.bytesPerRow = bytesPerRow + self.pixelSize = pixelSize + } + func getSize() -> CGSize { + return CGSize(width: self.width, height: self.height) + } + + func copy() -> Frame { + var frame = Frame(data: data, height: height, width: width, bytesPerRow: bytesPerRow, pixelSize: pixelSize) + frame.isDataPixelBuffer = isDataPixelBuffer + frame.bitmapInfo = bitmapInfo + frame.timestamp = timestamp + frame.fps = fps + frame.sequence = UInt64(sequence) + return frame + } + + func elapsedSeconds(_ ts: UInt64 = mach_absolute_time()) -> Double { + let current_ts = self.timestamp + var recent_ts = ts + var past_ts = current_ts + if recent_ts < past_ts { + recent_ts = current_ts + past_ts = ts + } + + let diff = TimeInterval(recent_ts - past_ts) / TimeInterval(NSEC_PER_SEC) + let fpx = Double(fps) + let remainer = (diff * fpx).truncatingRemainder(dividingBy: 1) + let calcedFPS = fpx - (fpx * remainer) + let fpsString = String(format:"%.2f",calcedFPS) + //dlog("\(self) elapsedSeconds \(diff) \(fpsString)fps") + return diff + } + + func getTimingInfo() -> CMSampleTimingInfo { + let scale = UInt64(fps) * 100 + let duration = CMTime(value: CMTimeValue(scale / UInt64(fps)), timescale: CMTimeScale(scale)) + let timestamp = CMTime(value: duration.value * CMTimeValue(sequence), timescale: CMTimeScale(scale)) + + return CMSampleTimingInfo( + duration: duration, + presentationTimeStamp: timestamp, + decodeTimeStamp: timestamp + ) + } + + func getVideoFormatDescription() -> CMVideoFormatDescription? { + var formatDescription: CMVideoFormatDescription? + let error = CMVideoFormatDescriptionCreate( + allocator: kCFAllocatorDefault, + codecType: kCVPixelFormatType_32ARGB, + width: Int32(width), height: Int32(height), + extensions: nil, + formatDescriptionOut: &formatDescription) + if error != noErr { + dlog("Error creating CMVideoFormatDescription \(error)") + } + return formatDescription + } + + func hash() -> Int { + return data.hashValue + } +} + + +/* + +Serialization +CFPropertyListCreateData + +Time elapsed for serialize: 0.00884699821472168 s. +Time elapsed for create: 0.005692005157470703 s. +___________ + +JSONEncoder +Time elapsed for jsonserialize: 0.0524829626083374 s. +Time elapsed for jsoncreate: 0.049543023109436035 s. + + + We need to serialize Frame information into a Data object to send to the plugin + +*/ + +extension Frame { + func serialize() -> Data? { + let obj = [ + "id": id, + "height": height, + "width": width, + "bytesPerRow": bytesPerRow, + "pixelSize": pixelSize, + "bitmapInfo": bitmapInfo, + "timestamp": timestamp, + "fps": fps, + "sequence": sequence, + "data": data, + "isDataPixelBuffer": isDataPixelBuffer, + ] as [String : Any] + let data: Unmanaged = CFPropertyListCreateData(kCFAllocatorDefault, obj as CFPropertyList, CFPropertyListFormat.binaryFormat_v1_0, 0, nil) + return data.takeRetainedValue() as Data + } + + static func deserialize(fromData data: Data) -> Frame? { + var error: Unmanaged? + var inputFormat = CFPropertyListFormat.binaryFormat_v1_0 + let options: CFOptionFlags = 0 + guard let plist: CFPropertyList = CFPropertyListCreateWithData(kCFAllocatorDefault, data as CFData, options, &inputFormat, &error)?.takeRetainedValue() + else { + return nil + } + + let id = plist["id"] as! String + let w = plist["width"] as! Int + let h = plist["height"] as! Int + let bpr = plist["bytesPerRow"] as! Int + let ps = plist["pixelSize"] as! Int + let bi = plist["bitmapInfo"] as! UInt32 + let ts = plist["timestamp"] as! UInt64 + let fps = plist["fps"] as! Int + let sequence = plist["sequence"] as! UInt64 + let d = plist["data"] as! Data + let isDataPixelBuffer = plist["isDataPixelBuffer"] as! Bool + + var frame = Frame(data: d, height: h, width: w, bytesPerRow: bpr, pixelSize: ps, id: id) + frame.isDataPixelBuffer = isDataPixelBuffer + frame.bitmapInfo = UInt32(bi) + frame.timestamp = UInt64(ts) + frame.fps = fps + frame.sequence = UInt64(sequence) + return frame + + } + +} + + +extension Frame { + func buildCIImage() -> CIImage? { + let colorSpace = CGColorSpaceCreateDeviceRGB() + if self.bytesPerRow == 0 { + let bitmapImg = NSBitmapImageRep(data: data) + if bitmapImg != nil { + return CIImage(bitmapImageRep: bitmapImg!) + } + + } + else { + return CIImage(bitmapData: data, + bytesPerRow: bytesPerRow, + size: getSize(), + format: CIFormat.BGRA8, + colorSpace: colorSpace) + + } + return nil + } + + func findFaces(_ cicontext: CIContext?, _ ciimage: CIImage?) -> CIImage? { + + var ctx = cicontext + if ctx == nil { + ctx = CIContext() + } + + var ciimg = ciimage + if ciimg == nil { + ciimg = buildCIImage() + } + + // create a face detector - since speed is not an issue we'll use a high accuracy + // detector + let detector: CIDetector! = CIDetector(ofType: CIDetectorTypeFace, + context: ctx, + options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) + let features = detector.features(in: ciimage!, options: nil) + for faceFeature in features { + var faceRect = faceFeature.bounds + faceRect = CGRect(x: faceRect.minX - 30, y: faceRect.minY - 30, width: faceRect.width + 60, height: faceRect.height + 60) + + ciimg = ciimg!.cropped(to: faceRect) + + print("faceFeature \(faceFeature)") + return ciimg + + } + + return ciimg + } + + func buildCGContext() -> CGContext? { + let rawBytes: UnsafeMutableRawPointer = unsafeBitCast(self.data, to: UnsafeMutableRawPointer.self) + let colorSpace = CGColorSpaceCreateDeviceRGB() + + guard let imageContext = CGContext(data: rawBytes, + width: self.width, + height: self.height, + bitsPerComponent: 8, + bytesPerRow: self.bytesPerRow, + space: colorSpace, + bitmapInfo: self.bitmapInfo, + releaseCallback: nil, releaseInfo: nil) else {return nil} + + return imageContext + + //guard let cgImage = imageContext.makeImage() else {return nil} + } + + /* +https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_performance/ci_performance.html#//apple_ref/doc/uid/TP30001185-CH10-SW1 + https://docs.huihoo.com/apple/wwdc/2011/session_422__using_core_image_on_ios_and_mac_os_x.pdf + */ + func image(_ ciContext: CIContext? = nil) -> NSImage? { + guard let img = buildCIImage() else { + return nil + } + let ctx = ciContext ?? CIContext() + + if let cgImg = ctx.createCGImage(img, from: img.extent) { + return NSImage(cgImage: cgImg, size: self.getSize()) + } + return nil + } + + func cgimage(_ ciContext: CIContext? = nil) -> CGImage? { + guard let img = buildCIImage() else { + return nil + } + let ctx = ciContext ?? CIContext() + + if let cgImg = ctx.createCGImage(img, from: img.extent) { + return cgImg + } + return nil + } + + + func buildPixelBuffer(image: CIImage? = nil, inContext ciContext: CIContext? = nil) -> CVPixelBuffer? { + + if isDataPixelBuffer { + return CVPixelBuffer.fromData(data, height: height, width: width) + } + + guard let ciImage = image ?? buildCIImage() else { + return nil + } + + let ctx = ciContext ?? CIContext() + return ciImage.toPixelBuffer(withContext: ctx) + } + +} + diff --git a/Shared/IPC/CallerID.swift b/Shared/IPC/CallerID.swift new file mode 100644 index 0000000..5533567 --- /dev/null +++ b/Shared/IPC/CallerID.swift @@ -0,0 +1,148 @@ +// +// CallerID.swift +// Imago +// +// Created by Nolan Brown on 7/21/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation + +enum CallerID: Int32 { + case Register = 0x1 + case Deregister = 0x2 + case RegistrationConfirmation = 0x3 + case GetFrame = 0x4 + case ReceiveFrame = 0x5 + case Ping = 0x6 + case ReceiveData = 0x7 + case KillServer = 0x99 +} + +enum ResponseCode: Int8 { + case OK = 0x1 + case Success = 0x2 + case NotAvailable = 0x3 + case ConnectionClosed = 0x4 + + func data() -> Data { + var theData : Int8 = self.rawValue + return Data(bytes: &theData, count: 1) + } +} + + + +struct Response { + enum Code: UInt8 { + case OK = 1 + case Success = 2 + case NotAvailable = 3 + case ConnectionClosed = 4 + case Unknown = 99 + + func data() -> Data { + var theData : UInt8 = self.rawValue + return Data(bytes: &theData, count: 1) + } + } + + var code: Code = .Unknown + + let callerID: CallerID + let status: Int32 + let data: Data? + + init(callerID: CallerID, status: Int32, data: Data?) { + self.callerID = callerID + self.status = status + self.data = data + + if self.data?.count == 1 { + self.code = Code(rawValue: self.data![0]) ?? Code.Unknown + } + + } + + func asString() -> String? { + if data != nil { + return String(data: data!, encoding: .utf8) + } + return nil + } + func statusString() -> String { + switch status { + case kCFMessagePortSuccess: + return "CFMessagePortSuccess" + case kCFMessagePortSendTimeout: + return "CFMessagePortSendTimeout" + case kCFMessagePortReceiveTimeout: + return "CFMessagePortReceiveTimeout" + case kCFMessagePortIsInvalid: + return "CFMessagePortIsInvalid" + case kCFMessagePortTransportError: + return "CFMessagePortTransportError" + case kCFMessagePortBecameInvalidError: + return "CFMessagePortBecameInvalidError" + default: + return "Unknown Status" + } + } + + func isSuccess() -> Bool { + switch status { + case kCFMessagePortSuccess: + return true + default: + return false + } + } + + func isTimeoutError() -> Bool { + switch status { + case kCFMessagePortSendTimeout, kCFMessagePortReceiveTimeout: + return true + default: + return false + } + } + + func isInvalidPortError() -> Bool { + switch status { + case kCFMessagePortIsInvalid, kCFMessagePortBecameInvalidError: + return true + default: + return false + } + } +} + +struct Request { + let port: CFMessagePort + let callerID: CallerID + let data: Data? + let str: String? + + init(port: CFMessagePort, callerID: CallerID, data: Data? = nil, str: String? = nil) { + self.port = port + self.callerID = callerID + self.data = data + self.str = str + } + + func messageID() -> Int32 { + return callerID.rawValue + } + + func asCFData() -> CFData? { + var requestData: CFData? = nil + if str != nil { + requestData = str!.data(using: .utf8)! as CFData + + } + else if data != nil { + requestData = data! as CFData + } + return requestData + } +} diff --git a/Shared/IPC/PubSub.swift b/Shared/IPC/PubSub.swift new file mode 100644 index 0000000..1803187 --- /dev/null +++ b/Shared/IPC/PubSub.swift @@ -0,0 +1,560 @@ +// +// PubSub.swift +// Imago +// +// Created by Nolan Brown on 7/24/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import CoreFoundation + +let PortInvalidationCallback : CFMessagePortInvalidationCallBack = { + (port: CFMessagePort?, info: UnsafeMutableRawPointer?) -> Void in + print("PortInvalidationCallback \(String(describing: port))") +} + +/* + CFMessagePortCallBack + + All CFMessagePortCreateLocal methods are provided PortRequestCallback as the callback function. It's here we do: + 1. Determine what type the request is (ie CallerID) + 2. Load the related initialized Conductor object + 3. Process the request + + */ +let ServerPortCallback : CFMessagePortCallBack = { + (port: CFMessagePort?, messageID: Int32, data: CFData?, info: UnsafeMutableRawPointer?) -> Unmanaged? in + + let callbackID = CallerID(rawValue: messageID) + + var serverInstance : PubSub? + if info != nil { + serverInstance = unsafeBitCast(info, to: PubSub.self) + } + if serverInstance == nil { + print("ERROR: PubSub Instance Unavailable") + return nil + } + + var requestData: Data? + if data != nil && CFDataGetLength(data!) > 0 { + requestData = data! as Data + } + + guard let callerID = callbackID else { + return nil + } + + guard let responseData = serverInstance!.receivedRequest(requestData, callerID) else { + return nil + } + return Unmanaged.passRetained(responseData as CFData) +} + + +enum PortConnectionStatus { + case Ready + case Connecting + case Connected + case Disconnected // A connect was made invalid + case Closed // A connection was closed +} + + +typealias PortConnectionStatusChangedCallback = (PortConnection, PortConnectionStatus)->Void + +class PortConnection : Hashable, Identifiable, CustomDebugStringConvertible { + var automaticallyReconnect: Bool = true + var maxConnectionAttempts: Int? = nil + var retryConnectionInterval: Int = 1 + var timeout: Double = 10.0 + var lastResponseTime: Date? = nil + + var _queue: DispatchQueue = DispatchQueue(label: "PortConnection", qos: .userInteractive) + + private lazy var _retryConnectionTimer: RepeatingTimer = { + let timer = RepeatingTimer(secondsInterval: retryConnectionInterval) //, queue: _queue) + timer.eventHandler = _connect + return timer + }() + + private var _numConnectionAttempts: Int = 0 + + internal let _runLoopMode: CFRunLoopMode = CFRunLoopMode.defaultMode //CFRunLoopMode.commonModes + + private var _remotePort: CFMessagePort? + private let _remotePortName: String + + private var _retryingConnection: Bool = false + + public private(set) var status: PortConnectionStatus { + didSet { + self.onStatusChanged?(self, self.status) + } + } + + var onStatusChanged: PortConnectionStatusChangedCallback? + + private var _onConnect: PortConnectionStatusChangedCallback? + + deinit { + self.close() + //_retryConnectionTimer.cancel() + } + + public init(_ portName: String, queue: DispatchQueue? = nil) { + if queue != nil { + _queue = queue! + } + _remotePortName = portName + status = .Ready + + dlog("Using \(_remotePortName) for Connection") + } + + var debugDescription: String { + return "\(String(describing: type(of: self)))(port:\(portName()) status:'\(String(describing: type(of: status)))')" + } + + //var model: String + static func == (lhs: PortConnection, rhs: PortConnection) -> Bool { + return lhs.portName() == rhs.portName() + } + public func hash(into hasher: inout Hasher) { + hasher.combine(_remotePortName) + } + + func isConnected() -> Bool { + if _remotePort == nil { + return false + } + return true + } + + + func portName() -> String { + return _remotePortName + } + + + func connect(_ onConnect: PortConnectionStatusChangedCallback? = nil) { + // Don't attempt to connect if we are trying or are already connected + if status == .Connecting || status == .Connected { + if onConnect != nil && status == .Connected { + onConnect!(self, status) + } + return + } + if onConnect != nil { + _onConnect = onConnect + } + status = .Connecting + //_connect() + if _remotePort == nil { + _retryConnectionTimer.resume() + } + } + + func close() { + _invalidatePort() + _onConnect = nil + _retryConnectionTimer.suspend() + self.status = .Closed + } + + func sendRequest(_ callerID: CallerID, data: Data? = nil, str: String? = nil, onCompletion: @escaping((Response)->Void)) { + DispatchQueue.main.async { [unowned self] in + let messageID = callerID.rawValue + + var requestData: CFData? = nil + if str != nil { + requestData = str!.data(using: .utf8)! as CFData + + } + else if data != nil { + requestData = data! as CFData + } + + + var responseRawData: Unmanaged? + + let status = CFMessagePortSendRequest(self._remotePort, + messageID, + requestData, + self.timeout, + self.timeout, + self._runLoopMode.rawValue, + &responseRawData) + + var responseData: Data? + if responseRawData != nil { + responseData = responseRawData!.takeRetainedValue() as Data + if responseData?.count == 0 { + responseData = nil + } + } + + self.lastResponseTime = Date() + let response = Response(callerID: callerID, status: status, data: responseData) + onCompletion(response) + } + + } + func sendRequest(_ callerID: CallerID, data: Data? = nil, str: String? = nil) -> Response { + if status != .Connected { + return Response(callerID: callerID, status: kCFMessagePortIsInvalid, data: nil) + } + + let messageID = callerID.rawValue + + var requestData: CFData? = nil + if str != nil { + requestData = str!.data(using: .utf8)! as CFData + + } + else if data != nil { + requestData = data! as CFData + } + + + var responseRawData: Unmanaged? + + let status = CFMessagePortSendRequest(_remotePort, + messageID, + requestData, + timeout, + timeout, + _runLoopMode.rawValue, + &responseRawData) + + var responseData: Data? + if responseRawData != nil { + responseData = responseRawData!.takeRetainedValue() as Data + if responseData?.count == 0 { + responseData = nil + } + } + self.lastResponseTime = Date() + + let response = Response(callerID: callerID, status: status, data: responseData) + return response + } + + + private func _invalidatePort() { + if _remotePort == nil { + CFMessagePortInvalidate(_remotePort) + _remotePort = nil + } + } + + private func _resetConnection() { + _invalidatePort() + _numConnectionAttempts = 0 + _retryConnectionTimer.resume() + status = .Connecting + + } + + private func _didConnect() { + if status == .Connected { + return + } + status = .Connected + DispatchQueue.main.async { [unowned self] in + self._onConnect?(self, self.status) + + self._onConnect = nil + } + } + + // _retryConnectionTimer handler + private func _connect() { + _retryingConnection = true + dlog("CONNECTING ... \(String(describing: _remotePort))") + if _remotePort == nil { + // Close the connection after reaching the max number of attempts + if maxConnectionAttempts != nil { + if _numConnectionAttempts >= maxConnectionAttempts! { + self.close() + return + } + } + + + if let port = CFMessagePortCreateRemote(nil, _remotePortName as CFString) { + dlog("CONNECTED \(port)") + _remotePort = port + } + else { + // We couldn't connect to the remote port. Maybe it hasn't been opened yet?? + _numConnectionAttempts += 1 + return + } + } + + _numConnectionAttempts = 0 + _retryConnectionTimer.suspend() + _retryingConnection = false + + _didConnect() + } +} + + + +class Subscriber: PubSub { + let identifier: String = UUID().uuidString + + + private lazy var _periodicRegistrationTimer: RepeatingTimer = { + let timer = RepeatingTimer(secondsInterval: 5) //, queue: _queue) + timer.eventHandler = _periodicRegistration + return timer + }() + + private var _publisherConnection: PortConnection + private var _portName: String + private var _port: CFMessagePort? + + fileprivate var _runLoopSource: CFRunLoopSource? + + var receivedNewFrame : ((Frame) -> Void)? = nil + var receivedNewFrameData : ((Data) -> Void)? = nil + + override init(publisherPortName: String) { + _portName = "\(publisherPortName).\(identifier)" + + // Connect to publisher and register so we can start receiving data + _publisherConnection = PortConnection(publisherPortName) + + super.init(publisherPortName: publisherPortName) + + // Create port for Publisher to call + let (createdPort, createdRunLoopSource) = createLocalPort(_portName) + + _port = createdPort + _runLoopSource = createdRunLoopSource + dlog("Started Subscriber at \(_portName)") + + _publisherConnection.onStatusChanged = self.onPublisherConnectionStatusChange + + } + + func teardown() { + stopLocalPort(port: _port, runLoopSource: _runLoopSource) + _port = nil + _runLoopSource = nil + } + + override func start() { + _publisherConnection.connect { [weak self] (conn, status) in + let response = conn.sendRequest(.Register, str: self?.identifier) + dlog("received publisher register response \(response)") + if response.isSuccess() { + + } + } + _periodicRegistrationTimer.resume() + + } + override func stop() { + _periodicRegistrationTimer.suspend() + // we don't want to get any more frames + + } + + deinit { + _periodicRegistrationTimer.suspend() + _publisherConnection.close() + stopLocalPort(port: _port, runLoopSource: _runLoopSource) + _port = nil + _runLoopSource = nil + } + + // can we make this run on a concucrent queue + fileprivate override func receivedRequest(_ data: Data?, _ callerID: CallerID) -> Data? { + //dlog("Subscriber receivedRequest \(callerID) \(data) \(Thread.current) \(Thread.isMainThread)") + + switch callerID { + case .ReceiveFrame: + guard let frame_data = data else { + return nil + } + + if receivedNewFrameData != nil { + receivedNewFrameData?(frame_data) + + return Response.Code.OK.data() + + } + return Response.Code.NotAvailable.data() + default: + break + } + return nil + } + + private func onPublisherConnectionStatusChange(connection: PortConnection, status: PortConnectionStatus) { + dlog("onPublisherConnectionStatusChange connection \(connection) \(status)") + if status == .Connected { + dlog("connected to publisher \(connection) \(status)") + } + + } + + private func _periodicRegistration() { + let response = _publisherConnection.sendRequest(.Register, str: self.identifier) + if response.isSuccess() { + + } + else { + dlog("not connected to publisher") + _publisherConnection.close() + _publisherConnection = PortConnection(self._publisherPortName) + _publisherConnection.onStatusChanged = self.onPublisherConnectionStatusChange + _publisherConnection.connect() + + } + } + +} + +// Handles connection management and setting up local connections +class PubSub { + fileprivate var _publisherPortName: String + private var _publisherPort: CFMessagePort? + private var _publisherRunLoopSource: CFRunLoopSource? + + internal let _runLoopMode: CFRunLoopMode = CFRunLoopMode.defaultMode //CFRunLoopMode.commonModes + + private var _connections: Set = [] + + fileprivate var _queue: DispatchQueue = DispatchQueue(label: "PubSub", qos: .userInteractive) + + public init(publisherPortName: String) { + dlog("PubSub INIT \(publisherPortName)") + _publisherPortName = publisherPortName + } + + func publisherName() -> String { + return _publisherPortName + } + + + func getSubscriberMetrics() -> Dictionary { + var metrics: [String: Any] = ["NumSubscribers": _connections.count] + + var subscribers: [[String:Any?]] = [] + for conn in _connections { + let subscriber: [String : Any?] = ["LastResponseTime":conn.lastResponseTime, "ID": conn.portName(), "Connected": conn.isConnected()] + subscribers.append(subscriber) + } + metrics["Subscribers"] = subscribers + return metrics + } + + func isRunning() -> Bool { + if _publisherPort != nil && _publisherRunLoopSource != nil { + return true + } + return false + } + + func start() { + if _publisherPort != nil { + return + } + // Create port for Publisher to call + let (createdPort, createdRunLoopSource) = createLocalPort(_publisherPortName) + + _publisherPort = createdPort + _publisherRunLoopSource = createdRunLoopSource + + } + func stop() { + stopLocalPort(port: _publisherPort, runLoopSource: _publisherRunLoopSource) + _publisherPort = nil + _publisherRunLoopSource = nil + } + + + func publishFrameData(_ frameData: Data) { + for conn in _connections { + let response = conn.sendRequest(.ReceiveFrame, data: frameData) + if response.isSuccess() { + //print("response \(response)") + } + } + } + + private func onConnectionStatusChange(connection: PortConnection, status: PortConnectionStatus) { + dlog("onConnectionStatusChange connection \(connection) \(status)") + if status == .Connected { +// dlog("sending ping to \(connection)") +// let response = connection.sendRequest(.Ping) +// print("ping response \(response)") + } + } + + private func addConnectionByIdentifier(_ identifier: String) -> Bool { + let clientName = "\(publisherName()).\(identifier)" + let conn = PortConnection(clientName) + if !_connections.contains(conn) { + _connections.insert(conn) + conn.onStatusChanged = self.onConnectionStatusChange + conn.connect() + return true + } + return false + } + + fileprivate func receivedRequest(_ data: Data?, _ callerID: CallerID) -> Data? { + + switch callerID { + case .Register: + guard let id_data = data else { + return nil + } + let identifier = String(data: id_data, encoding: .utf8)! + + dlog("Recieved \(callerID) request from client \(identifier)") + + if addConnectionByIdentifier(identifier) { // we added the connection + return Response.Code.Success.data() + } + return Response.Code.OK.data() + + + default: + break + } + return nil + } + + fileprivate func createLocalPort(_ portName: String) -> (CFMessagePort?, CFRunLoopSource?) { + //return DispatchQueue.main.sync { + var context = CFMessagePortContext(version: 0, info: Unmanaged.passUnretained(self).toOpaque(), retain: nil, release: nil, copyDescription: nil) + if let port = CFMessagePortCreateLocal(nil, portName as CFString, ServerPortCallback, &context, nil) { + CFMessagePortSetInvalidationCallBack(port, PortInvalidationCallback) + + CFMessagePortSetDispatchQueue(port, _queue) + + return (port, nil) + } + return (nil, nil) + //} + + } + + fileprivate func stopLocalPort(port: CFMessagePort?, runLoopSource: CFRunLoopSource? ) { + if port != nil { + CFMessagePortInvalidate(port) + } + if runLoopSource != nil { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoopSource, _runLoopMode) + } + } + +} diff --git a/Shared/ImagoStream.swift b/Shared/ImagoStream.swift new file mode 100644 index 0000000..fd8acad --- /dev/null +++ b/Shared/ImagoStream.swift @@ -0,0 +1,90 @@ +// +// ImagoStream.swift +// Imago +// +// Created by Nolan Brown on 7/20/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +import Foundation +import CoreImage + +class ImagoStream { + private var _subscriber: Subscriber? + private var _fps: Int = 30 + private var _startReceivingFrames: Bool = true + private var _isRegistered: Bool = false + private var _isTimerRunning: Bool = false + private var _queue: DispatchQueue = DispatchQueue(label: "com.Imago.ImagoStream") + private var _currentFrame: Frame? + private var _pubsub: PubSub = PubSub(publisherPortName:IMAGO_SERVER_NAME) + + private var _renderedFrames: [UInt64] = [] + private var _startTS: UInt64 = 0 + + private var _sequenceNumber: UInt64 = 0 + + var isRunning: Bool = false + + var receivedFrame : ((Frame) -> Void)? = nil + + init() { + print("ImagoStream INIT") + } + + func framesPerSecond() -> Int { + return _fps + } + + func start() { + if !isRunning { + _startReceivingFrames = true + isRunning = true + if _subscriber == nil { + _subscriber = Subscriber(publisherPortName: IMAGO_SERVER_NAME) + _subscriber!.start() + _subscriber!.receivedNewFrameData = setFrameData + } + } + _subscriber!.start() + _subscriber!.receivedNewFrameData = setFrameData + + } + + func stop() { + _subscriber!.stop() + _startReceivingFrames = false + isRunning = false + + } + + + func setFrameData(_ frameData: Data) { + + guard let frame: Frame = Frame.deserialize(fromData:frameData) else { + return + } + + if _currentFrame != nil && _currentFrame?.id == frame.id { + return + } + if _currentFrame == nil { // first frame + _startTS = mach_absolute_time() + } + + var newFrame = frame + if receivedFrame != nil { + + receivedFrame!(newFrame) + + if _currentFrame?.timestamp != nil { + let _ = newFrame.elapsedSeconds(_currentFrame!.timestamp) + } + } + _currentFrame = newFrame + + _sequenceNumber += 1 + + } + +} diff --git a/Shared/RepeatingTimer.swift b/Shared/RepeatingTimer.swift new file mode 100644 index 0000000..aafd8cf --- /dev/null +++ b/Shared/RepeatingTimer.swift @@ -0,0 +1,76 @@ +// +// RepeatingTimer.swift +// Imago +// +// Created by Nolan Brown on 7/27/20. +// Copyright © 2020 Nolan Brown. All rights reserved. +// + +/* + Code by danielgalasko + https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9 + https://gist.github.com/danielgalasko/1da90276f23ea24cb3467c33d2c05768#file-repeatingtimer-swift + */ + +import Foundation + +class RepeatingTimer { + + let interval: DispatchTimeInterval + let queue: DispatchQueue? + + init(secondsInterval: Int, queue: DispatchQueue? = nil) { + self.queue = queue + self.interval = DispatchTimeInterval.seconds(secondsInterval) + } + + init(timeInterval: DispatchTimeInterval, queue: DispatchQueue? = nil) { + self.queue = queue + self.interval = timeInterval + } + + private lazy var timer: DispatchSourceTimer = { + let t = DispatchSource.makeTimerSource()//(queue: self.queue) + t.schedule(deadline: .now() + self.interval, repeating: self.interval) + t.setEventHandler(handler: { [weak self] in + self?.eventHandler?() + }) + return t + }() + + var eventHandler: (() -> Void)? + + private enum State { + case suspended + case resumed + } + + private var state: State = .suspended + + deinit { + timer.setEventHandler {} + timer.cancel() + /* + If the timer is suspended, calling cancel without resuming + triggers a crash. This is documented here https://forums.developer.apple.com/thread/15902 + */ + resume() + eventHandler = nil + } + + func resume() { + if state == .resumed { + return + } + state = .resumed + timer.resume() + } + + func suspend() { + if state == .suspended { + return + } + state = .suspended + timer.suspend() + } +} diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..12ba489 --- /dev/null +++ b/setup.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +PROJECTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +TURBOJPEG_LIB_NAME=libturbojpeg.0.dylib +TURBOJPEG_LIB_PATH=/opt/libjpeg-turbo/lib/$TURBOJPEG_LIB_NAME + +TURBOJPEG_RELEASE_URL=https://sourceforge.net/projects/libjpeg-turbo/files/ + +FRAMEWORKS_DIR=Frameworks + + +EDSDK_ZIP_URL=https://downloads.canon.com/sdk/EDSDK_v131231_Macintosh.zip + +if [ ! -d "Imago.xcodeproj" ]; then + echo "This script must be executed in the base directory of the Imago project" + exit 1 +fi + +installTurboJpeg() { + if [ -f "$TURBOJPEG_LIB_PATH" ]; then + return 0 + else + if isBrewInstalled; then + while true; do + read -p "Do you wish to install TurboJPEG?" yn + case $yn in + [Yy]* ) brew install jpeg-turbo; return installTurboJpeg;; + [Nn]* ) askUserToInstallTurboJpeg ;; + * ) echo "Please answer yes or no.";; + esac + done + else + askUserToInstallTurboJpeg + fi + fi +} + +askUserToInstallTurboJpeg() { + echo "TurboJPEG not found. Please install and re-run this script" + open $TURBOJPEG_RELEASE_URL + exit +} + +isBrewInstalled() { + which -s brew + if [[ $? != 0 ]] ; then + return 0 + else + return 1 + fi +} + +isTurboJpegSetup() { + if [ -f "$FRAMEWORKS_DIR/$TURBOJPEG_LIB_NAME" ]; then + return 0 + else + return 1 + fi +} + +setupTurboJpegAsFramework() { + if installTurboJpeg; then + local newLibPath=$FRAMEWORKS_DIR/$TURBOJPEG_LIB_NAME + + if [ -f "$TURBOJPEG_LIB_PATH" ]; then + echo "Copying $TURBOJPEG_LIB_NAME to $newLibPath" + cp $TURBOJPEG_LIB_PATH $newLibPath + + echo "Updating $TURBOJPEG_LIB_NAME location to look in app bundle" + install_name_tool -id @executable_path/../Frameworks/$TURBOJPEG_LIB_NAME $newLibPath + + echo "Updating $TURBOJPEG_LIB_NAME use system libgcc" + install_name_tool -change /opt/local/lib/libgcc/libgcc_s.1.dylib /usr/lib/libgcc_s.1.dylib $newLibPath + + echo "Removing uncessary architectures from $TURBOJPEG_LIB_NAME" + lipo -thin x86_64 $newLibPath -o $newLibPath + fi + else + echo "TurboJPEG could not be installed." + fi + +} + +isEDSDKSetup() { + if [ -d "$PROJECTDIR/$FRAMEWORKS_DIR/EDSDK/EDSDK.framework" ]; then + return 0 + else + return 1 + fi +} + +setupEDSDKAsFramework() { + local tmpDir=`mktemp -d` + local zipName=$(basename $EDSDK_ZIP_URL) + + local diskImageName=Macintosh.dmg + local mountedImagePath=/Volumes/Macintosh + local edsdkDirectory="$mountedImagePath/EDSDK" + + local frameworkDirectory="$PROJECTDIR/$FRAMEWORKS_DIR/EDSDK" + + local frameworkName=EDSDK.framework + + + if [ ! -d "$frameworkDirectory/$frameworkName" ]; then + + cd $tmpDir + + echo "Downloading EDSDK.framework..." + curl -O $EDSDK_ZIP_URL &> /dev/null + + if [ ! -f $zipName ]; then + echo "Download of $EDSDK_ZIP_URL failed" + cd $PROJECTDIR + return 1 + fi + + echo "Unziping EDSDK" + unzip -a $zipName &> /dev/null + unzip -a "$diskImageName.zip" &> /dev/null + + if [ ! -f $diskImageName ]; then + echo "EDSDK disk image not found. Can not complete setup" + cd $PROJECTDIR + return 1 + fi + + echo "Attaching EDSDK disk image" + hdiutil attach $diskImageName &> /dev/null + + echo "Creating required directory structure" + mkdir -p $frameworkDirectory/Header + + echo "Copying $frameworkName and Headers" + cp -R $edsdkDirectory/Framework/$frameworkName $frameworkDirectory/$frameworkName + cp -R $edsdkDirectory/Header/* $frameworkDirectory/Header/ + + echo "Cleaning up..." + hdiutil detach $mountedImagePath &> /dev/null + + cd $PROJECTDIR + + echo "Done" + fi +} + + +if isTurboJpegSetup; then + echo "TurboJPEG is setup for Imago" +else + setupTurboJpegAsFramework +fi + + +if isEDSDKSetup; then + echo "EDSDK is setup for Imago" +else + setupEDSDKAsFramework +fi + + + + +