Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de69cfaaf6 | |||
| e6eddcce30 | |||
| eae0dcacd5 | |||
| 81f5045f7c | |||
| 3c3326129e | |||
| 8299d7c0f8 | |||
| d624343a30 | |||
| edcadab554 | |||
| 42613ed528 | |||
| 6a9e2393c6 | |||
| 5786fffaf7 | |||
| 87101908c8 | |||
| 31ef66b903 | |||
| 67fc993dc4 | |||
| 1e9968c1a3 | |||
| 2cac116d16 | |||
| cc7019f687 | |||
| 359e3dcbda | |||
| 7ff20805b0 | |||
| d94ae2125a | |||
| 49ea34ac8b | |||
| dba2398267 | |||
| 71094b27e2 | |||
| 6505e7346d | |||
| 9894d2a9f8 | |||
| ed3468f487 | |||
| f7f98881c3 | |||
| 232dab4f57 | |||
| 6bb55a923b |
Executable
+1
@@ -0,0 +1 @@
|
||||
echo "4.0" > .swift-version
|
||||
@@ -0,0 +1,22 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "ARVideoKit"
|
||||
s.version = "1.3"
|
||||
s.summary = "Capture & record ARKit videos 📹, photos 🌄, Live Photos 🎇, and GIFs 🎆."
|
||||
s.description = "Enabling developers to capture videos 📹, photos 🌄, Live Photos 🎇, and GIFs 🎆 with augmented reality components."
|
||||
s.homepage = "https://github.com/AFathi/ARVideoKit"
|
||||
s.screenshots = "http://www.ahmedbekhit.com/SK_PREV.gif", "http://www.ahmedbekhit.com/SCN_PREVIEW.gif"
|
||||
|
||||
|
||||
|
||||
s.license = { :type => "Apache 2.0", :file => "LICENSE" }
|
||||
|
||||
|
||||
s.author = { "Ahmed Fathi Bekhit" => "me@ahmedbekhit.com" }
|
||||
s.social_media_url = "http://ahmedbekhit.com"
|
||||
|
||||
s.platform = :ios, "11.0"
|
||||
|
||||
s.source = { :git => "https://github.com/AFathi/ARVideoKit.git", :tag => "1.3" }
|
||||
s.source_files = "ARVideoKit", "ARVideoKit/**/*.{h,m,swift}"
|
||||
s.resources = "ARVideoKit/Assets/*.scnassets"
|
||||
end
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
FB2E36891FAE29C00035B8D6 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = FB2E36881FAE29BF0035B8D6 /* LICENSE */; };
|
||||
FB404FFE20D72A190056EA1D /* JPEG.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB404FFD20D72A190056EA1D /* JPEG.swift */; };
|
||||
FBD604DF1FA969DD00EC9804 /* ARVideoKit.h in Headers */ = {isa = PBXBuildFile; fileRef = FBD604DD1FA969DD00EC9804 /* ARVideoKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
FBD604EB1FA96B1C00EC9804 /* video.scnassets in Resources */ = {isa = PBXBuildFile; fileRef = FBD604EA1FA96B1C00EC9804 /* video.scnassets */; };
|
||||
FBD604EE1FA96B2700EC9804 /* ARVideoOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD604EC1FA96B2700EC9804 /* ARVideoOptions.swift */; };
|
||||
@@ -22,7 +23,6 @@
|
||||
FBD605001FA96B5500EC9804 /* Generate+GIF.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD604FF1FA96B5500EC9804 /* Generate+GIF.swift */; };
|
||||
FBD605071FA96B6B00EC9804 /* LoveLiver_LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = FBD605021FA96B6B00EC9804 /* LoveLiver_LICENSE */; };
|
||||
FBD605081FA96B6B00EC9804 /* Generate+LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD605031FA96B6B00EC9804 /* Generate+LivePhoto.swift */; };
|
||||
FBD605091FA96B6B00EC9804 /* JPEG.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD605041FA96B6B00EC9804 /* JPEG.swift */; };
|
||||
FBD6050A1FA96B6B00EC9804 /* QuickTimeMov.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD605051FA96B6B00EC9804 /* QuickTimeMov.swift */; };
|
||||
FBD6050B1FA96B6B00EC9804 /* PHLivePhotoPlus.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD605061FA96B6B00EC9804 /* PHLivePhotoPlus.swift */; };
|
||||
FBD605111FA96BA100EC9804 /* ARView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD6050D1FA96BA100EC9804 /* ARView.swift */; };
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
FB2E36881FAE29BF0035B8D6 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
FB404FFD20D72A190056EA1D /* JPEG.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JPEG.swift; sourceTree = "<group>"; };
|
||||
FBD604DA1FA969DD00EC9804 /* ARVideoKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ARVideoKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FBD604DD1FA969DD00EC9804 /* ARVideoKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARVideoKit.h; sourceTree = "<group>"; };
|
||||
FBD604DE1FA969DD00EC9804 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -50,7 +51,6 @@
|
||||
FBD604FF1FA96B5500EC9804 /* Generate+GIF.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Generate+GIF.swift"; sourceTree = "<group>"; };
|
||||
FBD605021FA96B6B00EC9804 /* LoveLiver_LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LoveLiver_LICENSE; sourceTree = "<group>"; };
|
||||
FBD605031FA96B6B00EC9804 /* Generate+LivePhoto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Generate+LivePhoto.swift"; sourceTree = "<group>"; };
|
||||
FBD605041FA96B6B00EC9804 /* JPEG.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JPEG.swift; sourceTree = "<group>"; };
|
||||
FBD605051FA96B6B00EC9804 /* QuickTimeMov.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickTimeMov.swift; sourceTree = "<group>"; };
|
||||
FBD605061FA96B6B00EC9804 /* PHLivePhotoPlus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PHLivePhotoPlus.swift; sourceTree = "<group>"; };
|
||||
FBD6050D1FA96BA100EC9804 /* ARView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARView.swift; sourceTree = "<group>"; };
|
||||
@@ -173,8 +173,8 @@
|
||||
children = (
|
||||
FBD605021FA96B6B00EC9804 /* LoveLiver_LICENSE */,
|
||||
FBD605031FA96B6B00EC9804 /* Generate+LivePhoto.swift */,
|
||||
FBD605041FA96B6B00EC9804 /* JPEG.swift */,
|
||||
FBD605061FA96B6B00EC9804 /* PHLivePhotoPlus.swift */,
|
||||
FB404FFD20D72A190056EA1D /* JPEG.swift */,
|
||||
FBD605051FA96B6B00EC9804 /* QuickTimeMov.swift */,
|
||||
);
|
||||
path = "Live Photo";
|
||||
@@ -228,7 +228,7 @@
|
||||
FBD604D11FA969DD00EC9804 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0920;
|
||||
LastUpgradeCheck = 0940;
|
||||
ORGANIZATIONNAME = "Ahmed Fathit Bekhit";
|
||||
TargetAttributes = {
|
||||
FBD604D91FA969DD00EC9804 = {
|
||||
@@ -276,6 +276,7 @@
|
||||
FBD604F81FA96B3300EC9804 /* UIView+isType.swift in Sources */,
|
||||
FBD604F71FA96B3300EC9804 /* RecordAR+PhotoRender.swift in Sources */,
|
||||
FBD6050A1FA96B6B00EC9804 /* QuickTimeMov.swift in Sources */,
|
||||
FB404FFE20D72A190056EA1D /* JPEG.swift in Sources */,
|
||||
FBD604FD1FA96B3E00EC9804 /* RenderARDelegate.swift in Sources */,
|
||||
FBD605131FA96BA100EC9804 /* WritAR.swift in Sources */,
|
||||
FBD604F51FA96B3300EC9804 /* UIImage+VideoBuffer.swift in Sources */,
|
||||
@@ -291,7 +292,6 @@
|
||||
FBD604FC1FA96B3E00EC9804 /* RecordARDelegate.swift in Sources */,
|
||||
FBD604EF1FA96B2700EC9804 /* ARInputViewOptions.swift in Sources */,
|
||||
FBD6050B1FA96B6B00EC9804 /* PHLivePhotoPlus.swift in Sources */,
|
||||
FBD605091FA96B6B00EC9804 /* JPEG.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -302,6 +302,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
BITCODE_GENERATION_MODE = marker;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
@@ -312,6 +313,7 @@
|
||||
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;
|
||||
@@ -319,6 +321,7 @@
|
||||
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;
|
||||
@@ -350,6 +353,7 @@
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = "-fembed-bitcode-marker";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -362,6 +366,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
BITCODE_GENERATION_MODE = bitcode;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
@@ -372,6 +377,7 @@
|
||||
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;
|
||||
@@ -379,6 +385,7 @@
|
||||
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;
|
||||
@@ -403,6 +410,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_CFLAGS = "-fembed-bitcode";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0920"
|
||||
LastUpgradeVersion = "0940"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,7 +26,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
@@ -37,7 +36,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2</string>
|
||||
<string>1.31</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -19,10 +19,10 @@ internal class JPEG {
|
||||
}
|
||||
|
||||
func read() -> String? {
|
||||
guard let makerNote = metadata()?.object(forKey: kCGImagePropertyMakerAppleDictionary) as! NSDictionary? else {
|
||||
guard let makerNote = metadata()?.object(forKey: kCGImagePropertyMakerAppleDictionary) as? NSDictionary? else {
|
||||
return nil
|
||||
}
|
||||
return makerNote.object(forKey: kFigAppleMakerNote_AssetIdentifier) as! String?
|
||||
return makerNote?.object(forKey: kFigAppleMakerNote_AssetIdentifier) as? String
|
||||
}
|
||||
|
||||
func write(_ dest : String, assetIdentifier : String) {
|
||||
@@ -30,7 +30,7 @@ internal class JPEG {
|
||||
else { return }
|
||||
defer { CGImageDestinationFinalize(dest) }
|
||||
guard let imageSource = self.imageSource() else { return }
|
||||
guard let metadata = self.metadata()?.mutableCopy() as! NSMutableDictionary! else { return }
|
||||
guard let metadata = self.metadata()?.mutableCopy() as? NSMutableDictionary else { return }
|
||||
|
||||
let makerNote = NSMutableDictionary()
|
||||
makerNote.setObject(assetIdentifier, forKey: kFigAppleMakerNote_AssetIdentifier as NSCopying)
|
||||
|
||||
@@ -90,11 +90,10 @@ fileprivate var renderer:RenderAR!
|
||||
/**
|
||||
A boolean that enables or disables using envronment light rendering. Default is `false`.
|
||||
*/
|
||||
@objc public var enableAdjsutEnvironmentLighting:Bool = false {
|
||||
|
||||
@objc public var enableAdjustEnvironmentLighting:Bool = false {
|
||||
didSet{
|
||||
if (renderEngine != nil) {
|
||||
renderEngine.autoenablesDefaultLighting = enableAdjsutEnvironmentLighting
|
||||
renderEngine.autoenablesDefaultLighting = enableAdjustEnvironmentLighting
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -492,35 +491,25 @@ fileprivate var renderer:RenderAR!
|
||||
`permissionStatus`
|
||||
A `PHAuthorizationStatus` object that returns the current application's status for exporting media to the Photo Library.
|
||||
*/
|
||||
@objc public func export(video path:URL, _ finished: ((_ exported:Bool, _ permissionStatus:PHAuthorizationStatus) -> Swift.Void)? = nil) {
|
||||
@objc public func export(video path: URL, _ finished: ((_ exported: Bool, _ permissionStatus: PHAuthorizationStatus) -> Void)? = nil) {
|
||||
audioSessionQueue.async {
|
||||
let photos = PHPhotoLibrary.authorizationStatus()
|
||||
if photos == .notDetermined {
|
||||
PHPhotoLibrary.requestAuthorization({ status in
|
||||
if status == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: path)
|
||||
}) { saved, _ in
|
||||
if saved && self.deleteCacheWhenExported {
|
||||
logAR.remove(from: path)
|
||||
}
|
||||
finished?(saved, status)
|
||||
}
|
||||
}else{
|
||||
finished?(false, status)
|
||||
}
|
||||
})
|
||||
}else if photos == .authorized {
|
||||
let status = PHPhotoLibrary.authorizationStatus()
|
||||
if status == .notDetermined {
|
||||
PHPhotoLibrary.requestAuthorization() { status in
|
||||
// Recursive call after authorization request
|
||||
self.export(video: path, finished)
|
||||
}
|
||||
} else if status == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: path)
|
||||
}) { saved, error in
|
||||
if saved && self.deleteCacheWhenExported {
|
||||
logAR.remove(from: path)
|
||||
}
|
||||
finished?(saved, photos)
|
||||
finished?(saved, status)
|
||||
}
|
||||
}else if photos == .denied || photos == .restricted {
|
||||
finished?(false, photos)
|
||||
} else if status == .denied || status == .restricted {
|
||||
finished?(false, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,34 +528,18 @@ fileprivate var renderer:RenderAR!
|
||||
`permissionStatus`
|
||||
A `PHAuthorizationStatus` object that returns the current application's status for exporting media to the Photo Library.
|
||||
*/
|
||||
@objc public func export(image path:URL?=nil, UIImage:UIImage?=nil, _ finished: ((_ exported:Bool, _ permissionStatus:PHAuthorizationStatus) -> Swift.Void)? = nil) {
|
||||
let photos = PHPhotoLibrary.authorizationStatus()
|
||||
if photos == .notDetermined {
|
||||
PHPhotoLibrary.requestAuthorization({ status in
|
||||
if status == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
if let path = path {
|
||||
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: path)
|
||||
}else if let image = UIImage {
|
||||
PHAssetChangeRequest.creationRequestForAsset(from: image)
|
||||
}
|
||||
}) { saved, error in
|
||||
if saved && self.deleteCacheWhenExported {
|
||||
if let path = path {
|
||||
logAR.remove(from: path)
|
||||
}
|
||||
}
|
||||
finished?(saved, status)
|
||||
}
|
||||
}else{
|
||||
finished?(false, status)
|
||||
}
|
||||
})
|
||||
}else if photos == .authorized {
|
||||
@objc public func export(image path: URL? = nil, UIImage: UIImage? = nil, _ finished: ((_ exported: Bool, _ permissionStatus: PHAuthorizationStatus) -> Void)? = nil) {
|
||||
let status = PHPhotoLibrary.authorizationStatus()
|
||||
if status == .notDetermined {
|
||||
PHPhotoLibrary.requestAuthorization() { status in
|
||||
// Recursive call after authorization request
|
||||
self.export(image: path, UIImage: UIImage, finished)
|
||||
}
|
||||
} else if status == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
if let path = path {
|
||||
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: path)
|
||||
}else if let image = UIImage {
|
||||
} else if let image = UIImage {
|
||||
PHAssetChangeRequest.creationRequestForAsset(from: image)
|
||||
}
|
||||
}) { saved, error in
|
||||
@@ -575,10 +548,10 @@ fileprivate var renderer:RenderAR!
|
||||
logAR.remove(from: path)
|
||||
}
|
||||
}
|
||||
finished?(saved, photos)
|
||||
finished?(saved, status)
|
||||
}
|
||||
}else if photos == .denied || photos == .restricted {
|
||||
finished?(false, photos)
|
||||
} else if status == .denied || status == .restricted {
|
||||
finished?(false, status)
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -596,55 +569,36 @@ fileprivate var renderer:RenderAR!
|
||||
`permissionStatus`
|
||||
A `PHAuthorizationStatus` object that returns the current application's status for exporting media to the Photo Library.
|
||||
*/
|
||||
@objc public func export(live photo:PHLivePhotoPlus, _ finished: ((_ exported:Bool, _ permissionStatus:PHAuthorizationStatus) -> Swift.Void)? = nil) {
|
||||
@objc public func export(live photo: PHLivePhotoPlus, _ finished: ((_ exported: Bool, _ permissionStatus: PHAuthorizationStatus) -> Void)? = nil) {
|
||||
guard let keyPhotoPath = photo.keyPhotoPath else{logAR.message("An error occurred while exporting a live photo"); return}
|
||||
guard let videoPath = photo.pairedVideoPath else{logAR.message("An error occurred while exporting a live photo"); return}
|
||||
|
||||
let photos = PHPhotoLibrary.authorizationStatus()
|
||||
if photos == .notDetermined {
|
||||
PHPhotoLibrary.requestAuthorization({ status in
|
||||
if status == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let request = PHAssetCreationRequest.forAsset()
|
||||
let options = PHAssetResourceCreationOptions()
|
||||
request.addResource(with: .photo, fileURL: keyPhotoPath, options: options)
|
||||
request.addResource(with: .pairedVideo, fileURL: videoPath, options: options)
|
||||
}, completionHandler: { saved, error in
|
||||
if saved {
|
||||
if self.deleteCacheWhenExported {
|
||||
logAR.remove(from: keyPhotoPath)
|
||||
logAR.remove(from: videoPath)
|
||||
}
|
||||
self.fileCount = 0
|
||||
}else{
|
||||
logAR.message("An error occurred while exporting a live photo: \(error!)")
|
||||
}
|
||||
finished?(saved, status)
|
||||
})
|
||||
}else{
|
||||
finished?(false, status)
|
||||
}
|
||||
})
|
||||
}else if photos == .authorized {
|
||||
let status = PHPhotoLibrary.authorizationStatus()
|
||||
if status == .notDetermined {
|
||||
PHPhotoLibrary.requestAuthorization() { status in
|
||||
// Recursive call after authorization request
|
||||
self.export(live: photo, finished)
|
||||
}
|
||||
} else if status == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let request = PHAssetCreationRequest.forAsset()
|
||||
let options = PHAssetResourceCreationOptions()
|
||||
request.addResource(with: .photo, fileURL: keyPhotoPath, options: options)
|
||||
request.addResource(with: .pairedVideo, fileURL: videoPath, options: options)
|
||||
}, completionHandler: { saved, error in
|
||||
}) { saved, error in
|
||||
if saved {
|
||||
if self.deleteCacheWhenExported {
|
||||
logAR.remove(from: keyPhotoPath)
|
||||
logAR.remove(from: videoPath)
|
||||
}
|
||||
self.fileCount = 0
|
||||
}else{
|
||||
} else {
|
||||
logAR.message("An error occurred while exporting a live photo: \(error!)")
|
||||
}
|
||||
finished?(saved, photos)
|
||||
})
|
||||
}else if photos == .denied || photos == .restricted {
|
||||
finished?(false, photos)
|
||||
finished?(saved, status)
|
||||
}
|
||||
} else if status == .denied || status == .restricted {
|
||||
finished?(false, status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,27 +632,25 @@ fileprivate var renderer:RenderAR!
|
||||
Recommended to use in the `UIViewController`'s method `func viewWillAppear(_ animated: Bool)`
|
||||
- parameter configuration: An object that defines motion and scene tracking behaviors for the session.
|
||||
*/
|
||||
@objc public func prepare(_ configuration:ARConfiguration) {
|
||||
@objc public func prepare(_ configuration:ARConfiguration?=nil) {
|
||||
ARcontentMode = contentMode
|
||||
onlyRenderWhileRec = onlyRenderWhileRecording
|
||||
if let view = view as? ARSCNView {
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
||||
ViewAR.orientation = .portrait
|
||||
|
||||
//try resetting anchors for the initial landscape orientation issue.
|
||||
|
||||
view.session.run(configuration)
|
||||
//try resetting anchors for the initial landscape orientation issue.
|
||||
guard let config = configuration else {return}
|
||||
view.session.run(config)
|
||||
}else if let view = view as? ARSKView {
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
||||
ViewAR.orientation = .portrait
|
||||
|
||||
view.session.run(configuration)
|
||||
guard let config = configuration else {return}
|
||||
view.session.run(config)
|
||||
}else if let _ = view as? SCNView {
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
||||
ViewAR.orientation = .portrait
|
||||
|
||||
}
|
||||
|
||||
onlyRenderWhileRec = onlyRenderWhileRecording
|
||||
}
|
||||
/**
|
||||
A method that switches off the orientation lock used in a `UIViewController` with AR scenes 📐😴.
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -52,7 +52,7 @@
|
||||
recorder.deleteCacheWhenExported = NO;
|
||||
|
||||
// Configure the envronment light rendering.
|
||||
recorder.enableAdjsutEnvironmentLighting = YES;
|
||||
recorder.enableAdjustEnvironmentLighting = YES;
|
||||
}
|
||||
|
||||
-(void)viewWillAppear:(BOOL)animated {
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
FB2E368C1FAE2A510035B8D6 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = FB2E368B1FAE2A510035B8D6 /* LICENSE */; };
|
||||
FB36BDB020086BBB00002808 /* ARVideoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB36BDAF20086BB000002808 /* ARVideoKit.framework */; };
|
||||
FB36BDB120086BBB00002808 /* ARVideoKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FB36BDAF20086BB000002808 /* ARVideoKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
FB3B51A620489CBA000377AE /* overlay_2d_img.png in Resources */ = {isa = PBXBuildFile; fileRef = FB3B51A520489CB9000377AE /* overlay_2d_img.png */; };
|
||||
FB9B5D2E1FC49E3E005DDD60 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB9B5D2D1FC49E3E005DDD60 /* MainViewController.swift */; };
|
||||
FBDCC5E71FABDFC600E3184D /* Scene.sks in Resources */ = {isa = PBXBuildFile; fileRef = FBDCC5E51FABDFC500E3184D /* Scene.sks */; };
|
||||
FBDCC5E81FABDFC600E3184D /* Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDCC5E61FABDFC500E3184D /* Scene.swift */; };
|
||||
@@ -57,7 +56,6 @@
|
||||
/* Begin PBXFileReference section */
|
||||
FB2E368B1FAE2A510035B8D6 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
FB36BDAA20086BB000002808 /* ARVideoKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ARVideoKit.xcodeproj; path = ../../ARVideoKit.xcodeproj; sourceTree = "<group>"; };
|
||||
FB3B51A520489CB9000377AE /* overlay_2d_img.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = overlay_2d_img.png; sourceTree = "<group>"; };
|
||||
FB9B5D2D1FC49E3E005DDD60 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
|
||||
FBDCC5E51FABDFC500E3184D /* Scene.sks */ = {isa = PBXFileReference; lastKnownFileType = file.sks; path = Scene.sks; sourceTree = "<group>"; };
|
||||
FBDCC5E61FABDFC500E3184D /* Scene.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scene.swift; sourceTree = "<group>"; };
|
||||
@@ -167,7 +165,6 @@
|
||||
FBE134B81FAAD3DD00BEC469 /* ARVideoKit-Example */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB3B51A520489CB9000377AE /* overlay_2d_img.png */,
|
||||
FBE134B91FAAD3DD00BEC469 /* AppDelegate.swift */,
|
||||
FBA0AA0D1FAD9E4B006C481B /* View Controllers */,
|
||||
FBA0AA0C1FAD9E2C006C481B /* Storyboards */,
|
||||
@@ -259,7 +256,6 @@
|
||||
FBE134C11FAAD3DD00BEC469 /* Assets.xcassets in Resources */,
|
||||
FBDCC6401FAC2CD900E3184D /* art.scnassets in Resources */,
|
||||
FBE134BF1FAAD3DD00BEC469 /* Main.storyboard in Resources */,
|
||||
FB3B51A620489CBA000377AE /* overlay_2d_img.png in Resources */,
|
||||
FBDCC5E71FABDFC600E3184D /* Scene.sks in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,19 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="N8h-YD-r0X">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14113" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="N8h-YD-r0X">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<customFonts key="customFonts">
|
||||
<array key="Avenir.ttc">
|
||||
<string>Avenir-Black</string>
|
||||
<string>Avenir-Medium</string>
|
||||
</array>
|
||||
</customFonts>
|
||||
<scenes>
|
||||
<!--Main View Controller-->
|
||||
<scene sceneID="wdx-bS-4nS">
|
||||
@@ -88,10 +82,6 @@
|
||||
<arscnView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IMN-9B-haO">
|
||||
<rect key="frame" x="0.0" y="10" width="375" height="667"/>
|
||||
</arscnView>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="overlay_2d_img.png" translatesAutoresizingMaskIntoConstraints="NO" id="fyP-vk-9kb">
|
||||
<rect key="frame" x="0.0" y="10" width="375" height="657"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="AR w/ SceneKit" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fCR-qg-FqO">
|
||||
<rect key="frame" x="97" y="20" width="181" height="65"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="0.65000000000000002" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
@@ -239,7 +229,6 @@
|
||||
</view>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="overlayImg" destination="fyP-vk-9kb" id="VJg-xp-ImM"/>
|
||||
<outlet property="pauseBtn" destination="pCi-Bx-pwj" id="Vbx-Qf-GiU"/>
|
||||
<outlet property="recordBtn" destination="1R6-Xa-VsB" id="xFW-bY-b3A"/>
|
||||
<outlet property="sceneView" destination="IMN-9B-haO" id="6l9-yC-XES"/>
|
||||
@@ -417,7 +406,4 @@
|
||||
<point key="canvasLocation" x="53.600000000000001" y="133.5832083958021"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="overlay_2d_img.png" width="1080" height="1920"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -13,7 +13,6 @@ import Photos
|
||||
|
||||
class SCNViewController: UIViewController, ARSCNViewDelegate, RenderARDelegate, RecordARDelegate {
|
||||
|
||||
@IBOutlet weak var overlayImg: UIImageView!
|
||||
@IBOutlet var sceneView: ARSCNView!
|
||||
@IBOutlet var recordBtn: UIButton!
|
||||
@IBOutlet var pauseBtn: UIButton!
|
||||
@@ -59,7 +58,7 @@ class SCNViewController: UIViewController, ARSCNViewDelegate, RenderARDelegate,
|
||||
recorder?.contentMode = .aspectFill
|
||||
|
||||
//record or photo add environment light rendering, Default is false
|
||||
recorder?.enableAdjsutEnvironmentLighting = true
|
||||
recorder?.enableAdjustEnvironmentLighting = true
|
||||
|
||||
// Set the UIViewController orientations
|
||||
recorder?.inputViewOrientations = [.landscapeLeft, .landscapeRight, .portrait]
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 258 KiB |
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
// Generated by Apple Swift version 4.0.3 (swiftlang-900.0.74.1 clang-900.0.39.2)
|
||||
// Generated by Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wgcc-compat"
|
||||
|
||||
@@ -15,15 +15,6 @@
|
||||
# define __has_warning(x) 0
|
||||
#endif
|
||||
|
||||
#if __has_attribute(external_source_symbol)
|
||||
# define SWIFT_STRINGIFY(str) #str
|
||||
# define SWIFT_MODULE_NAMESPACE_PUSH(module_name) _Pragma(SWIFT_STRINGIFY(clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in=module_name, generated_declaration))), apply_to=any(function, enum, objc_interface, objc_category, objc_protocol))))
|
||||
# define SWIFT_MODULE_NAMESPACE_POP _Pragma("clang attribute pop")
|
||||
#else
|
||||
# define SWIFT_MODULE_NAMESPACE_PUSH(module_name)
|
||||
# define SWIFT_MODULE_NAMESPACE_POP
|
||||
#endif
|
||||
|
||||
#if __has_include(<swift/objc-prologue.h>)
|
||||
# include <swift/objc-prologue.h>
|
||||
#endif
|
||||
@@ -38,7 +29,7 @@
|
||||
# define SWIFT_TYPEDEFS 1
|
||||
# if __has_include(<uchar.h>)
|
||||
# include <uchar.h>
|
||||
# elif !defined(__cplusplus) || __cplusplus < 201103L
|
||||
# elif !defined(__cplusplus)
|
||||
typedef uint_least16_t char16_t;
|
||||
typedef uint_least32_t char32_t;
|
||||
# endif
|
||||
@@ -188,7 +179,13 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
|
||||
#pragma clang diagnostic ignored "-Wunknown-pragmas"
|
||||
#pragma clang diagnostic ignored "-Wnullability"
|
||||
|
||||
SWIFT_MODULE_NAMESPACE_PUSH("ARVideoKit")
|
||||
#if __has_attribute(external_source_symbol)
|
||||
# pragma push_macro("any")
|
||||
# undef any
|
||||
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="ARVideoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
|
||||
# pragma pop_macro("any")
|
||||
#endif
|
||||
|
||||
/// Allows specifying the final video orientation.
|
||||
typedef SWIFT_ENUM(NSInteger, ARFrameMode) {
|
||||
ARFrameModeAuto = 0,
|
||||
@@ -248,6 +245,7 @@ typedef SWIFT_ENUM(NSInteger, ARVideoOrientation) {
|
||||
SWIFT_CLASS("_TtC10ARVideoKit6ARView") SWIFT_AVAILABILITY(ios,introduced=11.0)
|
||||
@interface ARView : NSObject
|
||||
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
|
||||
+ (nonnull instancetype)new SWIFT_DEPRECATED_MSG("-init is unavailable");
|
||||
@end
|
||||
|
||||
@class NSCoder;
|
||||
@@ -468,7 +466,7 @@ SWIFT_AVAILABILITY(ios,introduced=11.0)
|
||||
/// Recommended to use in the <code>UIViewController</code>’s method <code>func viewWillAppear(_ animated: Bool)</code>
|
||||
/// \param configuration An object that defines motion and scene tracking behaviors for the session.
|
||||
///
|
||||
- (void)prepare:(ARConfiguration * _Nonnull)configuration;
|
||||
- (void)prepare:(ARConfiguration * _Nullable)configuration;
|
||||
/// A method that switches off the orientation lock used in a <code>UIViewController</code> with AR scenes 📐😴.
|
||||
/// Recommended to use in the <code>UIViewController</code>’s method <code>func viewWillDisappear(_ animated: Bool)</code>.
|
||||
- (void)rest;
|
||||
@@ -608,5 +606,7 @@ SWIFT_CLASS_PROPERTY(@property (nonatomic, class, readonly) UIInterfaceOrientati
|
||||
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
|
||||
@end
|
||||
|
||||
SWIFT_MODULE_NAMESPACE_POP
|
||||
#if __has_attribute(external_source_symbol)
|
||||
# pragma clang attribute pop
|
||||
#endif
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
// swift-tools-version:4.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "ARVideoKit"
|
||||
)
|
||||
@@ -12,11 +12,12 @@ In other words, you **NO LONGER** have to ~screen record~/~screenshot~ to captur
|
||||
| [Key Features](#key-features) | Lists the key features `ARVideoKit` offers |
|
||||
| [Compatibility](#compatibility) | Describes the `ARVideoKit` device and iOS compatibality |
|
||||
| [Example Projects](#example-projects) | Explains how to run the example project provided in this repository |
|
||||
| [Installation](#installation) | Describes the [Manual](#manual) option to install `ARVideoKit` |
|
||||
| [Installation](#installation) | Describes the [Cocoapods](#cocoapods) & [Manual](#manual) options to install `ARVideoKit` |
|
||||
| [Implementation](#implementation) | Lists the [steps needed](#implementation) for Objective-C & Swift, [notes](#note), and [reference](#youre-all-set-) for more options |
|
||||
| [Publishing to the App Store](#publishing-to-the-app-store) | Describes the steps **required** before submitting an application using `ARVideoKit` to the App Store. |
|
||||
|[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=ahmedfbekhit@gmail.com&item_name=Support+ARVideoKit+Developer&item_number=ARVideoKit+Framework+Donations&amount=0%2e00¤cy_code=USD) | [Donations](#donate) will support me to keep maintaining `ARVideoKit` ❤️|
|
||||
| [Contributions](#contributions) | Describes how you can contribute to this project |
|
||||
| [Apps using ARVideoKit](#apps-using-arvideokit) | A list of published applications using ARVideoKit |
|
||||
| [License](#license) | Describes `ARVideoKit` license |
|
||||
| [AppCoda Tutorial](https://www.appcoda.com/record-arkit-video/) | Check out a detailed tutorial about implementing `ARVideoKit` with SpriteKit ☺️ |
|
||||
|
||||
@@ -26,11 +27,11 @@ In other words, you **NO LONGER** have to ~screen record~/~screenshot~ to captur
|
||||
|  | |
|
||||
|
||||
## Key Features
|
||||
✅ Capture [Photos](https://github.com/AFathi/ARVideoKit/wiki/RecordAR#func-photo---uiimage) from [`ARSCNView`](https://developer.apple.com/documentation/arkit/arscnview) and [`ARSKView`](https://developer.apple.com/documentation/arkit/arskview)
|
||||
✅ Capture [Photos](https://github.com/AFathi/ARVideoKit/wiki/RecordAR#func-photo---uiimage) from [`ARSCNView`](https://developer.apple.com/documentation/arkit/arscnview), [`ARSKView`](https://developer.apple.com/documentation/arkit/arskview), and [`SCNView`](https://developer.apple.com/documentation/scenekit/scnview)
|
||||
|
||||
✅ Capture [Live Photos](https://github.com/AFathi/ARVideoKit/wiki/RecordAR#func-livephotoexportbool-_-finished-_-statusbool-_-livephotophlivephotoplus-_-permissionstatusphauthorizationstatus-_-exportedbool---swiftvoid--nil) & [GIFs](https://github.com/AFathi/ARVideoKit/wiki/RecordAR#func-gifforduration-durationtimeinterval-exportbool-_-finished-_-statusbool-_-gifpath-url-_-permissionstatusphauthorizationstatus-_-exportedbool---swiftvoid--nil) from [`ARSCNView`](https://developer.apple.com/documentation/arkit/arscnview) and [`ARSKView`](https://developer.apple.com/documentation/arkit/arskview)
|
||||
✅ Capture [Live Photos](https://github.com/AFathi/ARVideoKit/wiki/RecordAR#func-livephotoexportbool-_-finished-_-statusbool-_-livephotophlivephotoplus-_-permissionstatusphauthorizationstatus-_-exportedbool---swiftvoid--nil) & [GIFs](https://github.com/AFathi/ARVideoKit/wiki/RecordAR#func-gifforduration-durationtimeinterval-exportbool-_-finished-_-statusbool-_-gifpath-url-_-permissionstatusphauthorizationstatus-_-exportedbool---swiftvoid--nil) from [`ARSCNView`](https://developer.apple.com/documentation/arkit/arscnview), [`ARSKView`](https://developer.apple.com/documentation/arkit/arskview), and [`SCNView`](https://developer.apple.com/documentation/scenekit/scnview)
|
||||
|
||||
✅ [Record](https://github.com/AFathi/ARVideoKit/wiki/RecordAR#func-record) Videos from [`ARSCNView`](https://developer.apple.com/documentation/arkit/arscnview) and [`ARSKView`](https://developer.apple.com/documentation/arkit/arskview)
|
||||
✅ [Record](https://github.com/AFathi/ARVideoKit/wiki/RecordAR#func-record) Videos from [`ARSCNView`](https://developer.apple.com/documentation/arkit/arscnview), [`ARSKView`](https://developer.apple.com/documentation/arkit/arskview), and [`SCNView`](https://developer.apple.com/documentation/scenekit/scnview)
|
||||
|
||||
✅ [Pause/Resume](https://github.com/AFathi/ARVideoKit/wiki/RecordAR#func-pause) video
|
||||
|
||||
@@ -48,6 +49,28 @@ In other words, you **NO LONGER** have to ~screen record~/~screenshot~ to captur
|
||||
To try the example project, simply clone this repository and open the `Examples` folder to choose between the Objective-C and Swift project files.
|
||||
|
||||
## Installation
|
||||
### Cocoapods
|
||||
1. Download [CocoaPods](http://cocoapods.org) using this command in `Terminal`
|
||||
```
|
||||
$ sudo gem install cocoapods
|
||||
```
|
||||
2. Redirect to your project folder in `Terminal`
|
||||
```
|
||||
$ cd YOUR_PROJECT_FILE_PATH
|
||||
```
|
||||
3. Initialize a pod in `Terminal`
|
||||
```
|
||||
$ pod init
|
||||
```
|
||||
4. Open Podfile in a text editor and add this line
|
||||
```
|
||||
pod 'ARVideoKit'
|
||||
```
|
||||
5. Go back to the `Terminal` and install the pod
|
||||
```
|
||||
$ pod install
|
||||
```
|
||||
|
||||
### Manual
|
||||
Drag the `ARVideoKit.framework` file as an embedded binary of your project targets. `ARVideoKit.framework` can be found in the `/Framework Build/` folder of this repository.
|
||||

|
||||
@@ -80,27 +103,38 @@ Check [`RecordAR`](https://github.com/AFathi/ARVideoKit/wiki/RecordAR) documenta
|
||||
## Publishing to the App Store
|
||||
Before publishing to the App Store make sure to add the [ARVideoKit License](#license) to your app licences list.
|
||||
|
||||
1. Build ARVideoKit for release
|
||||
> Open ARVideoKit.xcodeproj
|
||||
|
||||
> Select ARVideoKitRelease scheme with Generic iOS Device
|
||||
|
||||
> Build the project (cmd + B)
|
||||
|
||||
> Right click on Products/ARVideoKit.framework -> Show in finder
|
||||
|
||||
> Copy and replace ARVideoKit.framework in your project
|
||||
|
||||
Additionally, if you are using the binary build from `Framework Build` or the latest release, you MUST **strip out the simulator architectures** from the framework before pushing an application to the App Store.
|
||||
|
||||
To do so, follow those steps:
|
||||
|
||||
1. Install Carthage
|
||||
2. Install Carthage
|
||||
> Download `Carthage.pkg` [from here](https://github.com/Carthage/Carthage/releases)
|
||||
|
||||
> Or install with Homebrew using this command `brew install carthage`
|
||||
2. Go to your project target's `Build Phase`
|
||||
3. Go to your project target's `Build Phase`
|
||||
<img width="684" alt="screen shot 2017-11-14 at 8 21 44 pm" src="https://user-images.githubusercontent.com/4106695/32813978-e70ae5a0-c97a-11e7-9d19-3ef434e4c4f1.png">
|
||||
|
||||
3. Add a new `Run Script Phase`
|
||||
4. Add a new `Run Script Phase`
|
||||
<img width="686" alt="screen shot 2017-11-14 at 8 22 14 pm" src="https://user-images.githubusercontent.com/4106695/32814003-0ab4cffc-c97b-11e7-97d0-cf3143afec6d.png">
|
||||
|
||||
4. Add the following command to the `Run Script Phase`
|
||||
5. Add the following command to the `Run Script Phase`
|
||||
```
|
||||
/usr/local/bin/carthage copy-frameworks
|
||||
```
|
||||
<img width="676" alt="screen shot 2017-11-14 at 8 30 12 pm" src="https://user-images.githubusercontent.com/4106695/32814033-3302bece-c97b-11e7-867c-e8707ac7dd6b.png">
|
||||
|
||||
5. Finally, add `ARVideoKit.framework` file path as an `Input File`. In my case, I have it in a folder named `Frameworks` inside my project folder
|
||||
6. Finally, add `ARVideoKit.framework` file path as an `Input File`. In my case, I have it in a folder named `Frameworks` inside my project folder
|
||||
<img width="672" alt="screen shot 2017-11-14 at 8 41 06 pm" src="https://user-images.githubusercontent.com/4106695/32814258-327bd048-c97c-11e7-8148-8d606d545214.png">
|
||||
|
||||
## Donate
|
||||
@@ -113,7 +147,13 @@ If you have an idea for a new **ARVideoKit** feature/functionality and want to a
|
||||
|
||||
Also, feel free to create an issue if you have any suggestions or need any help ☺️
|
||||
|
||||
## [License](LICENSE)
|
||||
## Apps using ARVideoKit
|
||||
| App | Description |
|
||||
| ------------------ |:------------------:|
|
||||
| [Our SolAR](https://itunes.apple.com/app/id1267675913) | An app that allows you to see our Solar System anywhere at all! |
|
||||
|
||||
_Feel free to add your application to this list!_
|
||||
## License
|
||||
Copyright 2017 Ahmed Fathi Bekhit, www.ahmedbekhit.com, me@ahmedbekhit.com
|
||||
|
||||
`ARVideoKit` is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// ARInputViewOptions.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/18/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Allows specifying the accepted orientaions in a `UIViewController` with AR scenes.
|
||||
@objc public enum ARInputViewOrientation: Int {
|
||||
/// Enables the portrait input views orientation.
|
||||
case portrait = 1
|
||||
/// Enables the landscape left input views orientation.
|
||||
case landscapeLeft = 3
|
||||
/// Enables the landscape right input views orientation.
|
||||
case landscapeRight = 4
|
||||
}
|
||||
|
||||
/// Allows specifying which subviews will rotate in a `UIViewController` with AR scenes.
|
||||
public enum ARInputViewOrientationMode {
|
||||
/// The framework automatically detects and rotates key objects in a `UIViewController`.
|
||||
case auto
|
||||
/// Rotates all objects in a `UIViewController`.
|
||||
case all
|
||||
/// Rotates manually specified `UIView` subviews in a `UIViewController`.
|
||||
case manual(subviews:[UIView])
|
||||
/// Disables rotating any objects in a `UIViewController`.
|
||||
case disabled
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// ARVideoOptions.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/18/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
/// Allows specifying the final video orientation.
|
||||
@objc public enum ARFrameMode: Int {
|
||||
case auto
|
||||
case aspectFit
|
||||
/// Recommended for iPhone X
|
||||
case aspectFill
|
||||
}
|
||||
/// Allows specifying the video rendering frame per second `FPS` rate.
|
||||
@objc public enum ARVideoFrameRate: Int {
|
||||
/// The framework automatically sets the most appropriate `FPS` based on the device support.
|
||||
case auto = 0
|
||||
/// Sets the `FPS` to 30 frames per second.
|
||||
case fps30 = 30
|
||||
/// Sets the `FPS` to 60 frames per second.
|
||||
case fps60 = 60
|
||||
}
|
||||
/// Allows specifying the final video orientation.
|
||||
@objc public enum ARVideoOrientation: Int {
|
||||
/// The framework automatically sets the video orientation based on the active `ARInputViewOrientation` orientations.
|
||||
case auto
|
||||
/// Sets the video orientation to always portrait.
|
||||
case alwaysPortrait
|
||||
/// Sets the video orientation to always landscape.
|
||||
case alwaysLandscape
|
||||
}
|
||||
/// Allows specifying when to request Microphone access.
|
||||
@objc public enum RecordARMicrophonePermission: Int {
|
||||
/// The framework automatically requests Microphone access when needed.
|
||||
case auto
|
||||
/// Allows manual permission request.
|
||||
case manual
|
||||
}
|
||||
/// An object that returns the AR recorder current status.
|
||||
@objc public enum RecordARStatus: Int {
|
||||
/// The current status of the recorder is unknown.
|
||||
case unknown
|
||||
/// The current recorder is ready to record.
|
||||
case readyToRecord
|
||||
/// The current recorder is recording.
|
||||
case recording
|
||||
/// The current recorder is paused.
|
||||
case paused
|
||||
}
|
||||
/// An object that returns the current Microphone status.
|
||||
@objc public enum RecordARMicrophoneStatus: Int {
|
||||
// The current status of the Microphone access is unknown.
|
||||
case unknown
|
||||
// The current status of the Microphone access is enabled.
|
||||
case enabled
|
||||
// The current status of the Microphone access is disabled.
|
||||
case disabled
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// CGImage+Resize.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/27/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
internal extension CGImage {
|
||||
internal func resize(with ratio:Float) -> CGImage? {
|
||||
let imageWidth = Float(self.width)
|
||||
let imageHeight = Float(self.height)
|
||||
|
||||
let width = imageWidth * ratio
|
||||
let height = imageHeight * ratio
|
||||
|
||||
guard let colorSpace = self.colorSpace else { return nil }
|
||||
guard let context = CGContext(data: nil, width: Int(width), height: Int(height), bitsPerComponent: self.bitsPerComponent, bytesPerRow: self.bytesPerRow, space: colorSpace, bitmapInfo: self.alphaInfo.rawValue) else { return nil }
|
||||
|
||||
context.interpolationQuality = .low
|
||||
context.draw(self, in: CGRect(x: 0, y: 0, width: Int(width), height: Int(height)))
|
||||
|
||||
return context.makeImage()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// RecordAR+PhotoRender.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/27/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Photos
|
||||
@available(iOS 11.0, *)
|
||||
internal extension RecordAR {
|
||||
|
||||
internal func adjustTime(current:CMTime, resume:CMTime, pause:CMTime) -> CMTime {
|
||||
return CMTimeSubtract(current, CMTimeSubtract(resume, pause))
|
||||
}
|
||||
|
||||
internal func imageFromBuffer(buffer:CVPixelBuffer) -> UIImage {
|
||||
let coreImg = CIImage(cvPixelBuffer: buffer)
|
||||
let context = CIContext()
|
||||
let cgImg = context.createCGImage(coreImg, from: coreImg.extent)
|
||||
|
||||
var angleEnabled: Bool {
|
||||
for v in inputViewOrientations {
|
||||
if UIDevice.current.orientation.rawValue == v.rawValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var recentAngle:CGFloat = 0
|
||||
var rotationAngle:CGFloat = 0
|
||||
switch UIDevice.current.orientation {
|
||||
case .landscapeLeft:
|
||||
rotationAngle = -90
|
||||
recentAngle = -90
|
||||
case .landscapeRight:
|
||||
rotationAngle = 90
|
||||
recentAngle = 90
|
||||
case .faceUp, .faceDown, .portraitUpsideDown:
|
||||
rotationAngle = recentAngle
|
||||
default:
|
||||
rotationAngle = 0
|
||||
recentAngle = 0
|
||||
}
|
||||
|
||||
if !angleEnabled {
|
||||
rotationAngle = 0
|
||||
}
|
||||
|
||||
switch videoOrientation {
|
||||
case .alwaysPortrait:
|
||||
rotationAngle = 0
|
||||
case .alwaysLandscape:
|
||||
if rotationAngle == 90 || rotationAngle == -90 {
|
||||
}else{
|
||||
rotationAngle = -90
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return UIImage(cgImage: cgImg!).rotate(by: rotationAngle, flip: false)
|
||||
}
|
||||
|
||||
@objc internal func appWillEnterBackground() {
|
||||
delegate?.recorder(willEnterBackground: status)
|
||||
}
|
||||
}
|
||||
|
||||
extension FloatingPoint {
|
||||
var degreesToRadians: Self { return self * .pi / 180 }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// UIImage+VideoBuffer.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/18/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreVideo
|
||||
import UIKit
|
||||
internal extension UIImage
|
||||
{
|
||||
internal func rotate(by degrees: CGFloat, flip:Bool?=nil) -> UIImage
|
||||
{
|
||||
let radians = CGFloat(degrees * (CGFloat.pi / 180.0))
|
||||
|
||||
let bufferView = UIView(frame: CGRect(origin: CGPoint.zero, size: self.size))
|
||||
let t: CGAffineTransform = CGAffineTransform(rotationAngle: radians)
|
||||
bufferView.transform = t
|
||||
let bufferSize = bufferView.frame.size
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(bufferSize, false, self.scale)
|
||||
let bitmap = UIGraphicsGetCurrentContext()
|
||||
bitmap?.translateBy(x: bufferSize.width / 2, y: bufferSize.height / 2)
|
||||
bitmap?.rotate(by: radians)
|
||||
if let isFlipped = flip {
|
||||
if !isFlipped {
|
||||
bitmap?.scaleBy(x: 1.0, y: -1.0)
|
||||
}else{
|
||||
bitmap?.scaleBy(x: -1.0, y: -1.0)
|
||||
}
|
||||
}else{
|
||||
bitmap?.scaleBy(x: -1.0, y: -1.0)
|
||||
}
|
||||
bitmap?.draw(self.cgImage!, in: CGRect(origin: CGPoint(x: -self.size.width / 2, y: -self.size.height / 2), size: self.size))
|
||||
|
||||
let finalBuffer = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return finalBuffer!
|
||||
}
|
||||
|
||||
var buffer: CVPixelBuffer? {
|
||||
let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary
|
||||
var pixelBuffer : CVPixelBuffer?
|
||||
let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(self.size.width), Int(self.size.height), kCVPixelFormatType_32ARGB, attrs, &pixelBuffer)
|
||||
guard (status == kCVReturnSuccess) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!)
|
||||
|
||||
let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let context = CGContext(data: pixelData, width: Int(self.size.width), height: Int(self.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)
|
||||
|
||||
context?.translateBy(x: 0, y: self.size.height)
|
||||
context?.scaleBy(x: 1.0, y: -1.0)
|
||||
|
||||
UIGraphicsPushContext(context!)
|
||||
self.draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
|
||||
UIGraphicsPopContext()
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
|
||||
|
||||
return pixelBuffer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// UIView+isType.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/18/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import ARKit
|
||||
@available(iOS 11.0, *)
|
||||
internal extension UIScreen {
|
||||
/**
|
||||
`isiPhone10` is a boolean that returns if the device is iPhone X or not.
|
||||
*/
|
||||
internal var isiPhone10: Bool {
|
||||
return self.nativeBounds.size == CGSize(width: 1125, height: 2436) || self.nativeBounds.size == CGSize(width: 2436, height: 1125)
|
||||
}
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
internal extension UIView {
|
||||
var parent: UIViewController? {
|
||||
var responder: UIResponder? = self
|
||||
while responder != nil {
|
||||
responder = responder!.next
|
||||
if let viewController = responder as? UIViewController {
|
||||
return viewController
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var isButton: Bool {
|
||||
if let _ = self as? UIButton {
|
||||
return true
|
||||
}else{
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isARView: Bool {
|
||||
if let _ = self as? ARSCNView {
|
||||
return true
|
||||
}else if let _ = self as? ARSKView {
|
||||
return true
|
||||
}else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// UIViewController+hasType.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/18/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import ARKit
|
||||
@available(iOS 11.0, *)
|
||||
internal extension UIViewController {
|
||||
internal var hasARView: Bool {
|
||||
let views = self.view.subviews
|
||||
for v in views {
|
||||
if let _ = v as? ARSCNView {
|
||||
return true
|
||||
}else if let _ = v as? ARSKView {
|
||||
return true
|
||||
}else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// RecordARDelegate.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/18/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreVideo
|
||||
import CoreMedia
|
||||
import ARKit
|
||||
/**
|
||||
The recorder protocol.
|
||||
|
||||
- Author: Ahmed Fathi Bekhit
|
||||
* [Github](http://github.com/AFathi)
|
||||
* [Website](http://ahmedbekhit.com)
|
||||
* [Twitter](http://twitter.com/iAFapps)
|
||||
* [Email](mailto:me@ahmedbekhit.com)
|
||||
*/
|
||||
@available(iOS 11.0, *)
|
||||
@objc public protocol RecordARDelegate {
|
||||
/**
|
||||
A protocol method that is triggered when a recorder ends recording.
|
||||
- parameter path: A `URL` object that returns the video file path.
|
||||
- parameter noError: A boolean that returns true when the recorder ends without errors. Otherwise, it returns false.
|
||||
*/
|
||||
func recorder(didEndRecording path:URL, with noError:Bool)
|
||||
/**
|
||||
A protocol method that is triggered when a recorder fails recording.
|
||||
- parameter error: An `Error` object that returns the error value.
|
||||
- parameter status: A string that returns the reason of the recorder failure in a string literal format.
|
||||
*/
|
||||
func recorder(didFailRecording error:Error?, and status:String)
|
||||
|
||||
/**
|
||||
A protocol method that is triggered when the application will resign active.
|
||||
- parameter status: A `RecordARStatus` object that returns the AR recorder current status.
|
||||
|
||||
|
||||
- NOTE: Check [applicationWillResignActive(_:)](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622950-applicationwillresignactive) for more information.
|
||||
*/
|
||||
@objc func recorder(willEnterBackground status:RecordARStatus)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// RenderARDelegate.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/21/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreVideo
|
||||
import CoreMedia
|
||||
import ARKit
|
||||
|
||||
/**
|
||||
The renderer protocol.
|
||||
|
||||
- Author: Ahmed Fathi Bekhit
|
||||
* [Github](http://github.com/AFathi)
|
||||
* [Website](http://ahmedbekhit.com)
|
||||
* [Twitter](http://twitter.com/iAFapps)
|
||||
* [Email](mailto:me@ahmedbekhit.com)
|
||||
*/
|
||||
@available(iOS 11.0, *)
|
||||
@objc public protocol RenderARDelegate {
|
||||
/**
|
||||
A protocol method that is triggered when a frame renders the `ARSCNView` or `ARSKView` content with the device's camera stream.
|
||||
- parameter buffer: A `CVPixelBuffer` object that returns the rendered buffer.
|
||||
- parameter time: A `CMTime` object that returns the time a buffer was rendered with.
|
||||
- parameter rawBuffer: A `CVPixelBuffer` object that returns the raw buffer.
|
||||
*/
|
||||
func frame(didRender buffer:CVPixelBuffer, with time:CMTime, using rawBuffer:CVPixelBuffer)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// Generate+GIF.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/27/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
import ImageIO
|
||||
import MobileCoreServices
|
||||
|
||||
internal class GIFGenerator {
|
||||
internal let gifQueue = DispatchQueue(label:"com.ahmedbekhit.GIFQueue", attributes: .concurrent)
|
||||
fileprivate var currentGIFPath:URL?
|
||||
fileprivate var newGIFPath:URL {
|
||||
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
|
||||
let documentsDirectory = paths[0]
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .full
|
||||
formatter.timeStyle = .full
|
||||
formatter.dateFormat = "yyyy-MM-dd'@'HH-mm-ssZZZZ"
|
||||
|
||||
let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970)
|
||||
|
||||
let gifPath = "\(documentsDirectory)/\(formatter.string(from: date))AR.gif"
|
||||
return URL(fileURLWithPath: gifPath, isDirectory: false)
|
||||
}
|
||||
|
||||
internal func generate(gif images:[UIImage], with delay:Float, loop count:Int = 0, adjust:Bool, _ finished: ((_ status: Bool, _ path: URL?) -> Void)? = nil) {
|
||||
currentGIFPath = newGIFPath
|
||||
gifQueue.async {
|
||||
let gifSettings = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: count]]
|
||||
let imageSettings = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: delay]]
|
||||
|
||||
guard let path = self.currentGIFPath else{return}
|
||||
guard let destination = CGImageDestinationCreateWithURL(path as CFURL, kUTTypeGIF, images.count, nil) else{finished?(false, nil);return}
|
||||
logAR.message("\(destination)")
|
||||
CGImageDestinationSetProperties(destination, gifSettings as CFDictionary)
|
||||
for image in images {
|
||||
if let imageRef = image.cgImage {
|
||||
var ratio:Float = 0.0
|
||||
if adjust{ratio=0.5}else{ratio=1.0}
|
||||
CGImageDestinationAddImage(destination, imageRef.resize(with: ratio)!, imageSettings as CFDictionary)
|
||||
}
|
||||
}
|
||||
|
||||
if !CGImageDestinationFinalize(destination){
|
||||
finished?(false, nil);
|
||||
return
|
||||
}else{
|
||||
finished?(true, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// Generate+LivePhoto.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/28/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Photos
|
||||
@available(iOS 11.0, *)
|
||||
internal class LivePhotoGenerator {
|
||||
fileprivate var keyPhotoPath:URL?
|
||||
|
||||
fileprivate var finalKeyPhotoPath:URL?
|
||||
fileprivate var finalPairedVideoPath:URL?
|
||||
|
||||
internal let livePhotoQueue = DispatchQueue(label:"com.ahmedbekhit.livePhotoQueue", attributes: .concurrent)
|
||||
|
||||
internal func generate(livePhoto video:URL?, _ finished: ((_ status: Bool, _ photo: PHLivePhotoPlus?, _ pairedVideoPath:URL?, _ keyFramePath:URL?) -> Void)? = nil) {
|
||||
livePhotoQueue.async {
|
||||
guard let liveFrames = video else{finished?(false, nil, nil, nil); return}
|
||||
let asset = AVURLAsset(url: video!)
|
||||
let generator = AVAssetImageGenerator(asset: asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
|
||||
//retrieves the key photo frame from the middle of the video asset
|
||||
let time = NSValue(time: CMTimeMultiplyByFloat64(asset.duration, 0.5))
|
||||
|
||||
//generates the key photo CGImage asynchronously
|
||||
generator.generateCGImagesAsynchronously(forTimes: [time], completionHandler:{_,image,_,_,_ in
|
||||
if let cgImg = image, let imgData = UIImagePNGRepresentation(UIImage(cgImage: cgImg)) {
|
||||
do {
|
||||
self.keyPhotoPath = self.newPath(for: true, and: false)
|
||||
try imgData.write(to: self.keyPhotoPath!, options: [.atomic])
|
||||
}catch let error {
|
||||
self.keyPhotoPath = nil
|
||||
logAR.message("An error occurred while capturing a live photo: \(error)")
|
||||
finished?(false, nil, nil, nil)
|
||||
return
|
||||
}
|
||||
self.finalKeyPhotoPath = self.newPath(for: true, and: true)
|
||||
self.finalPairedVideoPath = self.newPath(for: false, and: true)
|
||||
|
||||
guard let keyFrame = self.keyPhotoPath else{finished?(false, nil, nil, nil); return}
|
||||
guard let keyLiveFrame = self.finalKeyPhotoPath else{finished?(false, nil, nil, nil); return}
|
||||
guard let keyLiveFrames = self.finalPairedVideoPath else{finished?(false, nil, nil, nil); return}
|
||||
|
||||
let assetIdentifier = UUID().uuidString
|
||||
|
||||
JPEG(path: keyFrame.path).write(keyLiveFrame.path, assetIdentifier: assetIdentifier)
|
||||
QuickTimeMov(path: liveFrames.path).write(keyLiveFrames.path, assetIdentifier: assetIdentifier)
|
||||
|
||||
|
||||
PHLivePhoto.request(withResourceFileURLs: [keyLiveFrames, keyLiveFrame], placeholderImage: UIImage(cgImage: cgImg), targetSize: .zero, contentMode: .aspectFit) { photo, settings in
|
||||
logAR.remove(from: keyFrame)
|
||||
logAR.remove(from: liveFrames)
|
||||
if let livePhoto = photo {
|
||||
let finalPhoto = PHLivePhotoPlus(photo: livePhoto)
|
||||
finalPhoto.keyPhotoPath = keyLiveFrame
|
||||
finalPhoto.pairedVideoPath = keyLiveFrames
|
||||
finished?(true, finalPhoto, finalPhoto.pairedVideoPath, finalPhoto.keyPhotoPath)
|
||||
return
|
||||
}else{
|
||||
let finalPhoto = PHLivePhotoPlus(photo: photo!)
|
||||
finalPhoto.keyPhotoPath = keyLiveFrame
|
||||
finalPhoto.pairedVideoPath = keyLiveFrames
|
||||
finished?(false, finalPhoto, finalPhoto.pairedVideoPath, finalPhoto.keyPhotoPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newPath(for JPEG:Bool, and live:Bool) -> URL {
|
||||
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
|
||||
let documentsDirectory = paths[0]
|
||||
|
||||
let livePhotosFolder = "\(documentsDirectory)/livePhotos"
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .full
|
||||
formatter.timeStyle = .full
|
||||
formatter.dateFormat = "yyyy-MM-dd'@'HH-mm-ssZZZZ"
|
||||
|
||||
let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970)
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(atPath: livePhotosFolder, withIntermediateDirectories: true, attributes: nil)
|
||||
}catch let error {
|
||||
logAR.message("An error occurred while rendering the live photo: \(error)")
|
||||
return URL(fileURLWithPath: "\(documentsDirectory)/\(formatter.string(from: date))AR.jpg", isDirectory: false)
|
||||
}
|
||||
|
||||
if JPEG && live {
|
||||
return URL(fileURLWithPath: "\(livePhotosFolder)/\(formatter.string(from: date))AR.jpg", isDirectory: false)
|
||||
}else if JPEG && !live {
|
||||
return URL(fileURLWithPath: "\(documentsDirectory)/\(formatter.string(from: date))AR.jpg", isDirectory: false)
|
||||
}else{
|
||||
return URL(fileURLWithPath: "\(livePhotosFolder)/\(formatter.string(from: date))AR.mov", isDirectory: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// JPEG.swift
|
||||
// LoveLiver
|
||||
//
|
||||
// Created by mzp on 10/10/15.
|
||||
// Copyright © 2015 mzp. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MobileCoreServices
|
||||
import ImageIO
|
||||
|
||||
internal class JPEG {
|
||||
fileprivate let kFigAppleMakerNote_AssetIdentifier = "17"
|
||||
fileprivate let path : String
|
||||
|
||||
init(path : String) {
|
||||
self.path = path
|
||||
}
|
||||
|
||||
func read() -> String? {
|
||||
guard let makerNote = metadata()?.object(forKey: kCGImagePropertyMakerAppleDictionary) as! NSDictionary? else {
|
||||
return nil
|
||||
}
|
||||
return makerNote.object(forKey: kFigAppleMakerNote_AssetIdentifier) as! String?
|
||||
}
|
||||
|
||||
func write(_ dest : String, assetIdentifier : String) {
|
||||
guard let dest = CGImageDestinationCreateWithURL(URL(fileURLWithPath: dest) as CFURL, kUTTypeJPEG, 1, nil)
|
||||
else { return }
|
||||
defer { CGImageDestinationFinalize(dest) }
|
||||
guard let imageSource = self.imageSource() else { return }
|
||||
guard let metadata = self.metadata()?.mutableCopy() as? NSMutableDictionary else { return }
|
||||
|
||||
let makerNote = NSMutableDictionary()
|
||||
makerNote.setObject(assetIdentifier, forKey: kFigAppleMakerNote_AssetIdentifier as NSCopying)
|
||||
metadata.setObject(makerNote, forKey: kCGImagePropertyMakerAppleDictionary as String as String as NSCopying)
|
||||
CGImageDestinationAddImageFromSource(dest, imageSource, 0, metadata)
|
||||
}
|
||||
|
||||
fileprivate func metadata() -> NSDictionary? {
|
||||
return self.imageSource().flatMap {
|
||||
CGImageSourceCopyPropertiesAtIndex($0, 0, nil) as NSDictionary?
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func imageSource() -> CGImageSource? {
|
||||
return self.data().flatMap {
|
||||
CGImageSourceCreateWithData($0 as CFData, nil)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func data() -> Data? {
|
||||
return (try? Data(contentsOf: URL(fileURLWithPath: path)))
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2015 mzp.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// PHLivePhotoPlus.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/30/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import Photos
|
||||
/**
|
||||
A `PHLivePhotoPlus` object is a `PHLivePhoto` sub-class that contains objects to allow manual exporting of a live photo.
|
||||
|
||||
- Author: Ahmed Fathi Bekhit
|
||||
* [Github](http://github.com/AFathi)
|
||||
* [Website](http://ahmedbekhit.com)
|
||||
* [Twitter](http://twitter.com/iAFapps)
|
||||
* [Email](mailto:me@ahmedbekhit.com)
|
||||
*/
|
||||
@available(iOS 9.1, *)
|
||||
@objc public class PHLivePhotoPlus: PHLivePhoto {
|
||||
internal var pairedVideoPath:URL?
|
||||
internal var keyPhotoPath:URL?
|
||||
|
||||
/// A `PHLivePhoto` object that returns the Live Photo content from `PHLivePhotoPlus`.
|
||||
@objc public var livePhoto:PHLivePhoto?
|
||||
|
||||
@objc public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc public init(photo:PHLivePhoto) {
|
||||
super.init()
|
||||
livePhoto = photo
|
||||
}
|
||||
|
||||
@objc required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
|
||||
//
|
||||
// QuickTimeMov.swift
|
||||
// LoveLiver
|
||||
//
|
||||
// Created by mzp on 10/10/15.
|
||||
// Copyright © 2015 mzp. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
@available(iOS 11.0, *)
|
||||
internal class QuickTimeMov {
|
||||
fileprivate let kKeyContentIdentifier = "com.apple.quicktime.content.identifier"
|
||||
fileprivate let kKeyStillImageTime = "com.apple.quicktime.still-image-time"
|
||||
fileprivate let kKeySpaceQuickTimeMetadata = "mdta"
|
||||
fileprivate let path : String
|
||||
fileprivate let dummyTimeRange = CMTimeRangeMake(CMTimeMake(0, 1000), CMTimeMake(200, 3000))
|
||||
|
||||
fileprivate lazy var asset : AVURLAsset = {
|
||||
let url = URL(fileURLWithPath: self.path)
|
||||
return AVURLAsset(url: url)
|
||||
}()
|
||||
|
||||
init(path : String) {
|
||||
self.path = path
|
||||
}
|
||||
|
||||
func readAssetIdentifier() -> String? {
|
||||
for item in metadata() {
|
||||
if item.key as? String == kKeyContentIdentifier &&
|
||||
item.keySpace!.rawValue == kKeySpaceQuickTimeMetadata {
|
||||
return item.value as? String
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readStillImageTime() -> NSNumber? {
|
||||
if let track = track(AVMediaType.metadata) {
|
||||
let (reader, output) = try! self.reader(track, settings: nil)
|
||||
reader.startReading()
|
||||
|
||||
while true {
|
||||
guard let buffer = output.copyNextSampleBuffer() else { return nil }
|
||||
if CMSampleBufferGetNumSamples(buffer) != 0 {
|
||||
let group = AVTimedMetadataGroup(sampleBuffer: buffer)
|
||||
for item in group?.items ?? [] {
|
||||
if item.key as? String == kKeyStillImageTime &&
|
||||
item.keySpace!.rawValue == kKeySpaceQuickTimeMetadata {
|
||||
return item.numberValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func write(_ dest : String, assetIdentifier : String) {
|
||||
|
||||
var audioReader : AVAssetReader? = nil
|
||||
var audioWriterInput : AVAssetWriterInput? = nil
|
||||
var audioReaderOutput : AVAssetReaderOutput? = nil
|
||||
do {
|
||||
// --------------------------------------------------
|
||||
// reader for source video
|
||||
// --------------------------------------------------
|
||||
guard let track = self.track(AVMediaType.video) else {
|
||||
logAR.message("not found video track")
|
||||
return
|
||||
}
|
||||
let (reader, output) = try self.reader(track,
|
||||
settings: [kCVPixelBufferPixelFormatTypeKey as String:
|
||||
NSNumber(value: kCVPixelFormatType_32BGRA as UInt32)])
|
||||
// --------------------------------------------------
|
||||
// writer for mov
|
||||
// --------------------------------------------------
|
||||
let writer = try AVAssetWriter(outputURL: URL(fileURLWithPath: dest), fileType: AVFileType.mov)
|
||||
writer.metadata = [metadataFor(assetIdentifier)]
|
||||
|
||||
// video track
|
||||
let input = AVAssetWriterInput(mediaType: AVMediaType.video,
|
||||
outputSettings: videoSettings(track.naturalSize))
|
||||
input.expectsMediaDataInRealTime = true
|
||||
input.transform = track.preferredTransform
|
||||
writer.add(input)
|
||||
|
||||
|
||||
let url = URL(fileURLWithPath: self.path)
|
||||
let aAudioAsset : AVAsset = AVAsset(url: url)
|
||||
|
||||
if aAudioAsset.tracks.count > 1 {
|
||||
logAR.message("Has Audio")
|
||||
//setup audio writer
|
||||
audioWriterInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: nil)
|
||||
|
||||
audioWriterInput?.expectsMediaDataInRealTime = false
|
||||
if writer.canAdd(audioWriterInput!){
|
||||
writer.add(audioWriterInput!)
|
||||
}
|
||||
//setup audio reader
|
||||
let audioTrack:AVAssetTrack = aAudioAsset.tracks(withMediaType: AVMediaType.audio).first!
|
||||
audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil)
|
||||
|
||||
do{
|
||||
audioReader = try AVAssetReader(asset: aAudioAsset)
|
||||
}catch{
|
||||
fatalError("Unable to read Asset: \(error) : ")
|
||||
}
|
||||
//let audioReader:AVAssetReader = AVAssetReader(asset: aAudioAsset, error: &error)
|
||||
if (audioReader?.canAdd(audioReaderOutput!))! {
|
||||
audioReader?.add(audioReaderOutput!)
|
||||
} else {
|
||||
logAR.message("cant add audio reader")
|
||||
}
|
||||
}
|
||||
|
||||
// metadata track
|
||||
let adapter = metadataAdapter()
|
||||
writer.add(adapter.assetWriterInput)
|
||||
|
||||
// --------------------------------------------------
|
||||
// creating video
|
||||
// --------------------------------------------------
|
||||
writer.startWriting()
|
||||
reader.startReading()
|
||||
writer.startSession(atSourceTime: kCMTimeZero)
|
||||
|
||||
// write metadata track
|
||||
adapter.append(AVTimedMetadataGroup(items: [metadataForStillImageTime()],
|
||||
timeRange: dummyTimeRange))
|
||||
|
||||
// write video track
|
||||
input.requestMediaDataWhenReady(on: DispatchQueue(label: "assetVideoWriterQueue", attributes: [])) {
|
||||
while(input.isReadyForMoreMediaData) {
|
||||
if reader.status == .reading {
|
||||
if let buffer = output.copyNextSampleBuffer() {
|
||||
if !input.append(buffer) {
|
||||
logAR.message("cannot write: \(String(describing: writer.error))")
|
||||
reader.cancelReading()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
input.markAsFinished()
|
||||
if reader.status == .completed && aAudioAsset.tracks.count > 1 {
|
||||
audioReader?.startReading()
|
||||
writer.startSession(atSourceTime: kCMTimeZero)
|
||||
let media_queue = DispatchQueue(label: "assetAudioWriterQueue", attributes: [])
|
||||
audioWriterInput?.requestMediaDataWhenReady(on: media_queue) {
|
||||
while (audioWriterInput?.isReadyForMoreMediaData)! {
|
||||
let sampleBuffer2:CMSampleBuffer? = audioReaderOutput?.copyNextSampleBuffer()
|
||||
if audioReader?.status == .reading && sampleBuffer2 != nil {
|
||||
if !(audioWriterInput?.append(sampleBuffer2!))! {
|
||||
audioReader?.cancelReading()
|
||||
}
|
||||
}else {
|
||||
audioWriterInput?.markAsFinished()
|
||||
logAR.message("Audio writer finish")
|
||||
writer.finishWriting() {
|
||||
if let e = writer.error {
|
||||
logAR.message("cannot write: \(e)")
|
||||
} else {
|
||||
logAR.message("finish writing.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
logAR.message("Video Reader not completed")
|
||||
writer.finishWriting() {
|
||||
if let e = writer.error {
|
||||
logAR.message("cannot write: \(e)")
|
||||
} else {
|
||||
logAR.message("finish writing.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while writer.status == .writing {
|
||||
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.5))
|
||||
}
|
||||
if let e = writer.error {
|
||||
logAR.message("cannot write: \(e)")
|
||||
}
|
||||
} catch {
|
||||
logAR.message("error")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func metadata() -> [AVMetadataItem] {
|
||||
return asset.metadata(forFormat: AVMetadataFormat.quickTimeMetadata)
|
||||
}
|
||||
|
||||
fileprivate func track(_ mediaType : AVMediaType) -> AVAssetTrack? {
|
||||
return asset.tracks(withMediaType: mediaType).first
|
||||
}
|
||||
|
||||
fileprivate func reader(_ track : AVAssetTrack, settings: [String:AnyObject]?) throws -> (AVAssetReader, AVAssetReaderOutput) {
|
||||
let output = AVAssetReaderTrackOutput(track: track, outputSettings: settings)
|
||||
let reader = try AVAssetReader(asset: asset)
|
||||
reader.add(output)
|
||||
return (reader, output)
|
||||
}
|
||||
|
||||
fileprivate func metadataAdapter() -> AVAssetWriterInputMetadataAdaptor {
|
||||
let spec : NSDictionary = [
|
||||
kCMMetadataFormatDescriptionMetadataSpecificationKey_Identifier as NSString:
|
||||
"\(kKeySpaceQuickTimeMetadata)/\(kKeyStillImageTime)",
|
||||
kCMMetadataFormatDescriptionMetadataSpecificationKey_DataType as NSString:
|
||||
"com.apple.metadata.datatype.int8" ]
|
||||
|
||||
var desc : CMFormatDescription? = nil
|
||||
CMMetadataFormatDescriptionCreateWithMetadataSpecifications(kCFAllocatorDefault, kCMMetadataFormatType_Boxed, [spec] as CFArray, &desc)
|
||||
let input = AVAssetWriterInput(mediaType: AVMediaType.metadata,
|
||||
outputSettings: nil, sourceFormatHint: desc)
|
||||
return AVAssetWriterInputMetadataAdaptor(assetWriterInput: input)
|
||||
}
|
||||
|
||||
fileprivate func videoSettings(_ size : CGSize) -> [String:AnyObject] {
|
||||
return [
|
||||
AVVideoCodecKey: AVVideoCodecType.h264 as AnyObject,
|
||||
AVVideoWidthKey: size.width as AnyObject,
|
||||
AVVideoHeightKey: size.height as AnyObject
|
||||
]
|
||||
}
|
||||
|
||||
fileprivate func metadataFor(_ assetIdentifier: String) -> AVMetadataItem {
|
||||
let item = AVMutableMetadataItem()
|
||||
item.key = kKeyContentIdentifier as (NSCopying & NSObjectProtocol)?
|
||||
item.keySpace = AVMetadataKeySpace(rawValue: kKeySpaceQuickTimeMetadata)
|
||||
item.value = assetIdentifier as (NSCopying & NSObjectProtocol)?
|
||||
item.dataType = "com.apple.metadata.datatype.UTF-8"
|
||||
return item
|
||||
}
|
||||
|
||||
fileprivate func metadataForStillImageTime() -> AVMetadataItem {
|
||||
let item = AVMutableMetadataItem()
|
||||
item.key = kKeyStillImageTime as (NSCopying & NSObjectProtocol)?
|
||||
item.keySpace = AVMetadataKeySpace(rawValue: kKeySpaceQuickTimeMetadata)
|
||||
item.value = 0 as (NSCopying & NSObjectProtocol)?
|
||||
item.dataType = "com.apple.metadata.datatype.int8"
|
||||
return item
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// RenderAR.swift
|
||||
// ARVideoKit
|
||||
//
|
||||
// Created by Ahmed Bekhit on 1/7/18.
|
||||
// Copyright © 2018 Ahmed Fathit Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ARKit
|
||||
fileprivate var view:Any?
|
||||
fileprivate var renderEngine:SCNRenderer!
|
||||
@available(iOS 11.0, *)
|
||||
internal struct RenderAR {
|
||||
internal var ARcontentMode:ARFrameMode!
|
||||
init(_ ARview:Any?, renderer:SCNRenderer, contentMode:ARFrameMode) {
|
||||
view = ARview
|
||||
renderEngine = renderer
|
||||
ARcontentMode = contentMode
|
||||
}
|
||||
internal let pixelsQueue = DispatchQueue(label:"com.ahmedbekhit.PixelsQueue", attributes: .concurrent)
|
||||
internal var time:CFTimeInterval {return CACurrentMediaTime()}
|
||||
internal var rawBuffer:CVPixelBuffer? {
|
||||
if let view = view as? ARSCNView {
|
||||
guard let rawBuffer = view.session.currentFrame?.capturedImage else{return nil}
|
||||
return rawBuffer
|
||||
}else if let view = view as? ARSKView {
|
||||
guard let rawBuffer = view.session.currentFrame?.capturedImage else{return nil}
|
||||
return rawBuffer
|
||||
}else if let _ = view as? SCNView {
|
||||
return buffer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
internal var bufferSize:CGSize? {
|
||||
guard let raw = rawBuffer else{return nil};
|
||||
var width = CVPixelBufferGetWidth(raw);
|
||||
var height = CVPixelBufferGetHeight(raw);
|
||||
|
||||
switch ARcontentMode {
|
||||
case .auto:
|
||||
if UIScreen.main.isiPhone10 {
|
||||
width = Int(UIScreen.main.nativeBounds.width)
|
||||
height = Int(UIScreen.main.nativeBounds.height)
|
||||
}
|
||||
case .aspectFit:
|
||||
width = CVPixelBufferGetWidth(raw);
|
||||
height = CVPixelBufferGetHeight(raw);
|
||||
case .aspectFill:
|
||||
width = Int(UIScreen.main.nativeBounds.width)
|
||||
height = Int(UIScreen.main.nativeBounds.height)
|
||||
default:
|
||||
if UIScreen.main.isiPhone10 {
|
||||
width = Int(UIScreen.main.nativeBounds.width)
|
||||
height = Int(UIScreen.main.nativeBounds.height)
|
||||
}
|
||||
}
|
||||
|
||||
if width > height {
|
||||
return CGSize(width: height, height: width)
|
||||
}else{
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
internal var bufferSizeFill:CGSize? {
|
||||
guard let raw = rawBuffer else{return nil};
|
||||
let width = CVPixelBufferGetWidth(raw);
|
||||
let height = CVPixelBufferGetHeight(raw);
|
||||
if width > height {
|
||||
return CGSize(width: height, height: width)
|
||||
}else{
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
internal var buffer:CVPixelBuffer? {
|
||||
if let _ = view as? ARSCNView {
|
||||
guard let size = bufferSize else{return nil};
|
||||
//UIScreen.main.bounds.size
|
||||
var renderedFrame:UIImage?
|
||||
pixelsQueue.sync {
|
||||
renderedFrame = renderEngine.snapshot(atTime: self.time, with: size, antialiasingMode: .none);
|
||||
}
|
||||
if let _ = renderedFrame {
|
||||
}else{
|
||||
renderedFrame = renderEngine.snapshot(atTime: time, with: size, antialiasingMode: .none);
|
||||
}
|
||||
guard let buffer = renderedFrame!.buffer else{return nil};
|
||||
return buffer;
|
||||
}else if let _ = view as? ARSKView {
|
||||
guard let size = bufferSize else{return nil};
|
||||
var renderedFrame:UIImage?
|
||||
pixelsQueue.sync {
|
||||
renderedFrame = renderEngine.snapshot(atTime: self.time, with: size, antialiasingMode: .none).rotate(by: 180);
|
||||
}
|
||||
if let _ = renderedFrame {
|
||||
}else{
|
||||
renderedFrame = renderEngine.snapshot(atTime: time, with: size, antialiasingMode: .none).rotate(by: 180);
|
||||
}
|
||||
guard let buffer = renderedFrame!.buffer else{return nil};
|
||||
return buffer;
|
||||
}else if let _ = view as? SCNView {
|
||||
let size = UIScreen.main.bounds.size
|
||||
var renderedFrame:UIImage?
|
||||
pixelsQueue.sync {
|
||||
renderedFrame = renderEngine.snapshot(atTime: self.time, with: size, antialiasingMode: .none);
|
||||
}
|
||||
if let _ = renderedFrame {
|
||||
}else{
|
||||
renderedFrame = renderEngine.snapshot(atTime: time, with: size, antialiasingMode: .none);
|
||||
}
|
||||
guard let buffer = renderedFrame!.buffer else{return nil};
|
||||
return buffer;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
//
|
||||
// WritAR.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/19/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import CoreImage
|
||||
import UIKit
|
||||
@available(iOS 11.0, *)
|
||||
internal class WritAR:NSObject, AVCaptureAudioDataOutputSampleBufferDelegate {
|
||||
fileprivate var assetWriter: AVAssetWriter!
|
||||
fileprivate var videoInput: AVAssetWriterInput!
|
||||
fileprivate var audioInput: AVAssetWriterInput!
|
||||
fileprivate var session: AVCaptureSession!
|
||||
|
||||
fileprivate var pixelBufferInput: AVAssetWriterInputPixelBufferAdaptor!
|
||||
fileprivate var videoOutputSettings: Dictionary<String, AnyObject>!
|
||||
fileprivate var audioSettings: [String : Any]?
|
||||
|
||||
internal let audioBufferQueue = DispatchQueue(label: "com.ahmedbekhit.AudioBufferQueue")
|
||||
|
||||
fileprivate var isRecording:Bool = false
|
||||
|
||||
internal var delegate:RecordARDelegate?
|
||||
internal var videoInputOrientation:ARVideoOrientation = .auto
|
||||
|
||||
init(output:URL, width:Int, height:Int, adjustForSharing:Bool, audioEnabled:Bool, orientaions:[ARInputViewOrientation], queue:DispatchQueue, allowMix:Bool){
|
||||
super.init()
|
||||
do {
|
||||
assetWriter = try AVAssetWriter(outputURL: output, fileType: AVFileType.mp4)
|
||||
}catch{
|
||||
return
|
||||
}
|
||||
if audioEnabled {
|
||||
if allowMix {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: [AVAudioSessionCategoryOptions.mixWithOthers , AVAudioSessionCategoryOptions.allowBluetooth, AVAudioSessionCategoryOptions.defaultToSpeaker, AVAudioSessionCategoryOptions.interruptSpokenAudioAndMixWithOthers])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
}catch{}
|
||||
}
|
||||
AVAudioSession.sharedInstance().requestRecordPermission({ permitted in
|
||||
if permitted {
|
||||
self.prepareAudioDevice(with: queue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//HEVC file format only supports A10 Fusion Chip or higher.
|
||||
//to support HEVC, make sure to check if the device is iPhone 7 or higher
|
||||
videoOutputSettings = [
|
||||
AVVideoCodecKey : AVVideoCodecType.h264 as AnyObject,
|
||||
AVVideoWidthKey : width as AnyObject,
|
||||
AVVideoHeightKey : height as AnyObject
|
||||
]
|
||||
|
||||
let attributes : [String:Bool] = [
|
||||
kCVPixelBufferCGImageCompatibilityKey as String : true,
|
||||
kCVPixelBufferCGBitmapContextCompatibilityKey as String : true
|
||||
]
|
||||
videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoOutputSettings)
|
||||
|
||||
videoInput.expectsMediaDataInRealTime = true
|
||||
pixelBufferInput = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoInput, sourcePixelBufferAttributes: attributes)
|
||||
|
||||
var angleEnabled: Bool {
|
||||
for v in orientaions {
|
||||
if UIDevice.current.orientation.rawValue == v.rawValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var recentAngle:CGFloat = 0
|
||||
var rotationAngle:CGFloat = 0
|
||||
switch UIDevice.current.orientation {
|
||||
case .landscapeLeft:
|
||||
rotationAngle = -90
|
||||
recentAngle = -90
|
||||
case .landscapeRight:
|
||||
rotationAngle = 90
|
||||
recentAngle = 90
|
||||
case .faceUp, .faceDown, .portraitUpsideDown:
|
||||
rotationAngle = recentAngle
|
||||
default:
|
||||
rotationAngle = 0
|
||||
recentAngle = 0
|
||||
}
|
||||
|
||||
if !angleEnabled {
|
||||
rotationAngle = 0
|
||||
}
|
||||
|
||||
var t = CGAffineTransform.identity
|
||||
|
||||
switch videoInputOrientation {
|
||||
case .auto:
|
||||
t = t.rotated(by:((rotationAngle*CGFloat.pi)/180))
|
||||
case .alwaysPortrait:
|
||||
t = t.rotated(by:0)
|
||||
case .alwaysLandscape:
|
||||
if rotationAngle == 90 || rotationAngle == -90 {
|
||||
t = t.rotated(by:((rotationAngle*CGFloat.pi)/180))
|
||||
}else{
|
||||
t = t.rotated(by:((-90*CGFloat.pi)/180))
|
||||
}
|
||||
}
|
||||
|
||||
videoInput.transform = t
|
||||
|
||||
if assetWriter.canAdd(videoInput) {
|
||||
assetWriter.add(videoInput)
|
||||
}else{
|
||||
delegate?.recorder(didFailRecording: assetWriter.error, and: "An error occurred while adding video input.")
|
||||
isWritingWithoutError = false
|
||||
}
|
||||
assetWriter.shouldOptimizeForNetworkUse = adjustForSharing
|
||||
}
|
||||
|
||||
internal func prepareAudioDevice(with queue:DispatchQueue) {
|
||||
let device: AVCaptureDevice = AVCaptureDevice.default(for: .audio)!
|
||||
var audioDeviceInput:AVCaptureDeviceInput?
|
||||
do {
|
||||
audioDeviceInput = try AVCaptureDeviceInput(device: device)
|
||||
}catch{
|
||||
audioDeviceInput = nil
|
||||
}
|
||||
|
||||
let audioDataOutput = AVCaptureAudioDataOutput()
|
||||
audioDataOutput.setSampleBufferDelegate(self, queue: queue)
|
||||
|
||||
session = AVCaptureSession()
|
||||
session.sessionPreset = .medium
|
||||
session.usesApplicationAudioSession = true
|
||||
session.automaticallyConfiguresApplicationAudioSession = false
|
||||
|
||||
if session.canAddInput(audioDeviceInput!) {
|
||||
session.addInput(audioDeviceInput!)
|
||||
}
|
||||
if session.canAddOutput(audioDataOutput) {
|
||||
session.addOutput(audioDataOutput)
|
||||
}
|
||||
|
||||
|
||||
audioSettings = audioDataOutput.recommendedAudioSettingsForAssetWriter(writingTo: .m4v) as? [String : Any]
|
||||
|
||||
audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings)
|
||||
audioInput.expectsMediaDataInRealTime = true
|
||||
|
||||
audioBufferQueue.async {
|
||||
self.session.startRunning()
|
||||
}
|
||||
|
||||
if assetWriter.canAdd(audioInput) {
|
||||
assetWriter.add(audioInput)
|
||||
}
|
||||
}
|
||||
|
||||
internal var startingVideoTime:CMTime?
|
||||
internal var isWritingWithoutError: Bool?
|
||||
|
||||
internal func insert(pixel buffer:CVPixelBuffer, with intervals:CFTimeInterval) {
|
||||
var time:CMTime {return CMTimeMakeWithSeconds(intervals, 1000000);}
|
||||
if assetWriter.status == .unknown {
|
||||
if let _ = startingVideoTime {isWritingWithoutError = false; return}else{startingVideoTime = time}
|
||||
if assetWriter.startWriting() {
|
||||
assetWriter.startSession(atSourceTime: startingVideoTime!)
|
||||
session.startRunning()
|
||||
isRecording = true
|
||||
isWritingWithoutError = true
|
||||
}else{
|
||||
delegate?.recorder(didFailRecording: assetWriter.error, and: "An error occurred while starting the video session.")
|
||||
isWritingWithoutError = false
|
||||
}
|
||||
}else if assetWriter.status == .failed {
|
||||
delegate?.recorder(didFailRecording: assetWriter.error, and: "Video session failed while recording.")
|
||||
logAR.message("An error occurred while recording the video, status: \(assetWriter.status.rawValue), error: \(assetWriter.error!.localizedDescription)")
|
||||
isWritingWithoutError = false
|
||||
return
|
||||
}
|
||||
if videoInput.isReadyForMoreMediaData {
|
||||
append(pixel: buffer, with: time)
|
||||
isWritingWithoutError = true
|
||||
}
|
||||
}
|
||||
|
||||
internal func insert(pixel buffer:CVPixelBuffer, with time:CMTime) {
|
||||
if assetWriter.status == .unknown {
|
||||
if let _ = startingVideoTime {isWritingWithoutError = false; return}else{startingVideoTime = time}
|
||||
if assetWriter.startWriting() {
|
||||
assetWriter.startSession(atSourceTime: startingVideoTime!)
|
||||
isRecording = true
|
||||
isWritingWithoutError = true
|
||||
}else{
|
||||
delegate?.recorder(didFailRecording: assetWriter.error, and: "An error occurred while starting the video session.")
|
||||
isRecording = false
|
||||
isWritingWithoutError = false
|
||||
}
|
||||
}else if assetWriter.status == .failed {
|
||||
delegate?.recorder(didFailRecording: assetWriter.error, and: "Video session failed while recording.")
|
||||
logAR.message("An error occurred while recording the video, status: \(assetWriter.status.rawValue), error: \(assetWriter.error!.localizedDescription)")
|
||||
isRecording = false
|
||||
isWritingWithoutError = false
|
||||
return
|
||||
}
|
||||
|
||||
if videoInput.isReadyForMoreMediaData {
|
||||
append(pixel: buffer, with: time)
|
||||
isRecording = true
|
||||
isWritingWithoutError = true
|
||||
}
|
||||
}
|
||||
|
||||
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
|
||||
if let input = audioInput {
|
||||
audioBufferQueue.async { [weak self] in
|
||||
if input.isReadyForMoreMediaData && (self?.isRecording)! {
|
||||
input.append(sampleBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
isRecording = false
|
||||
}
|
||||
func end(writing finished: @escaping () -> Void){
|
||||
if let session = session {
|
||||
if session.isRunning {
|
||||
session.stopRunning()
|
||||
}
|
||||
}
|
||||
assetWriter.finishWriting(completionHandler: finished)
|
||||
}
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
fileprivate extension WritAR {
|
||||
func append(pixel buffer:CVPixelBuffer, with time: CMTime) {
|
||||
pixelBufferInput.append(buffer, withPresentationTime: time)
|
||||
}
|
||||
}
|
||||
|
||||
//Simple Logging to show logs only while debugging.
|
||||
internal class logAR{
|
||||
internal class func message(_ message: String) {
|
||||
#if DEBUG
|
||||
print("ARVideoKit @ \(Date().timeIntervalSince1970):- \(message)")
|
||||
#endif
|
||||
}
|
||||
internal class func remove(from path: URL?) {
|
||||
if let file = path?.path {
|
||||
let manager = FileManager.default
|
||||
if manager.fileExists(atPath: file) {
|
||||
do{
|
||||
try manager.removeItem(atPath: file)
|
||||
self.message("Successfuly deleted media file from cached after exporting to Camera Roll.")
|
||||
} catch let error {
|
||||
self.message("An error occurred while deleting cached media: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
//
|
||||
// ARView.swift
|
||||
// ARVideoKit
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/14/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import ARKit
|
||||
|
||||
fileprivate var parentVC:UIViewController?
|
||||
fileprivate var recentAngle = 0
|
||||
|
||||
/**
|
||||
A class that configures the Augmented Reality View orientations.
|
||||
|
||||
- Author: Ahmed Fathi Bekhit
|
||||
* [Github](http://github.com/AFathi)
|
||||
* [Website](http://ahmedbekhit.com)
|
||||
* [Twitter](http://twitter.com/iAFapps)
|
||||
* [Email](mailto:me@ahmedbekhit.com)
|
||||
*/
|
||||
@available(iOS 11.0, *)
|
||||
@objc public class ARView: NSObject {
|
||||
fileprivate var ivo:[ARInputViewOrientation] = []
|
||||
/// An array of `ARInputViewOrientation` objects that allow customizing the accepted orientations in a `UIViewController` that contains Augmented Reality scenes.
|
||||
public var inputViewOrientations:[ARInputViewOrientation] {
|
||||
get{
|
||||
return ivo
|
||||
}
|
||||
set{
|
||||
if newValue.count == 0 {
|
||||
ivo = [.portrait]
|
||||
}else{
|
||||
ivo = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var ivom:ARInputViewOrientationMode = .auto
|
||||
/// An object that allow customizing which subviews will rotate in a `UIViewController` that contains Augmented Reality scenes.
|
||||
public var inputViewOrientationMode:ARInputViewOrientationMode {
|
||||
get{
|
||||
return ivom
|
||||
}
|
||||
set{
|
||||
ivom = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc init?(ARSceneKit:ARSCNView) {
|
||||
super.init()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
|
||||
|
||||
let value = UIInterfaceOrientation.portrait.rawValue
|
||||
UIDevice.current.setValue(value, forKey: "orientation")
|
||||
|
||||
ViewAR.orientation = .portrait
|
||||
|
||||
guard let vc = ARSceneKit.parent else {
|
||||
return
|
||||
}
|
||||
|
||||
parentVC = vc
|
||||
}
|
||||
|
||||
@objc init?(ARSpriteKit:ARSKView) {
|
||||
super.init()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
|
||||
|
||||
let value = UIInterfaceOrientation.portrait.rawValue
|
||||
UIDevice.current.setValue(value, forKey: "orientation")
|
||||
|
||||
ViewAR.orientation = .portrait
|
||||
guard let vc = ARSpriteKit.parent else {
|
||||
return
|
||||
}
|
||||
parentVC = vc
|
||||
}
|
||||
|
||||
@objc init?(SceneKit:SCNView) {
|
||||
super.init()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
|
||||
|
||||
let value = UIInterfaceOrientation.portrait.rawValue
|
||||
UIDevice.current.setValue(value, forKey: "orientation")
|
||||
|
||||
ViewAR.orientation = .portrait
|
||||
|
||||
guard let vc = SceneKit.parent else {
|
||||
return
|
||||
}
|
||||
|
||||
parentVC = vc
|
||||
}
|
||||
|
||||
@objc fileprivate func deviceDidRotate() {
|
||||
guard var views = parentVC?.view.subviews else {
|
||||
return
|
||||
}
|
||||
|
||||
var rotationAngle = 0
|
||||
|
||||
switch inputViewOrientationMode {
|
||||
case .auto:
|
||||
views = views.filter{!$0.isARView && $0.isButton}
|
||||
case .all:
|
||||
views = views.filter{!$0.isARView}
|
||||
case .manual(let subviews):
|
||||
views = subviews.filter{!$0.isARView}
|
||||
case .disabled:
|
||||
views = []
|
||||
}
|
||||
var angleEnabled: Bool {
|
||||
for v in inputViewOrientations {
|
||||
if UIDevice.current.orientation.rawValue == v.rawValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
switch UIDevice.current.orientation {
|
||||
case .landscapeLeft:
|
||||
rotationAngle = 90
|
||||
recentAngle = 90
|
||||
case .landscapeRight:
|
||||
rotationAngle = -90
|
||||
recentAngle = -90
|
||||
case .faceUp, .faceDown, .portraitUpsideDown:
|
||||
rotationAngle = recentAngle
|
||||
default:
|
||||
rotationAngle = 0
|
||||
recentAngle = 0
|
||||
}
|
||||
|
||||
if !angleEnabled {
|
||||
rotationAngle = 0
|
||||
}
|
||||
|
||||
for view in views {
|
||||
UIView.animate(withDuration: 0.2, animations: {
|
||||
view.transform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle).degreesToRadians)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,786 @@
|
||||
//
|
||||
// RecordAR.swift
|
||||
// ARVideoKit
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/18/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Metal
|
||||
import ARKit
|
||||
import Photos
|
||||
import PhotosUI
|
||||
|
||||
fileprivate var view:Any?
|
||||
fileprivate var renderEngine:SCNRenderer!
|
||||
fileprivate var gpuLoop:CADisplayLink!
|
||||
fileprivate var isResting = false
|
||||
fileprivate var ARcontentMode:ARFrameMode!
|
||||
@available(iOS 11.0, *)
|
||||
fileprivate var renderer:RenderAR!
|
||||
/**
|
||||
This class renders the `ARSCNView` or `ARSKView` content with the device's camera stream to generate a video 📹, photo 🌄, live photo 🎇 or GIF 🎆.
|
||||
|
||||
- Author: 🤓 Ahmed Fathi Bekhit © 2017
|
||||
* [Github](http://github.com/AFathi)
|
||||
* [Website](http://ahmedbekhit.com)
|
||||
* [Twitter](http://twitter.com/iAFapps)
|
||||
* [Email](mailto:me@ahmedbekhit.com)
|
||||
*/
|
||||
@available(iOS 11.0, *)
|
||||
@objc public class RecordAR: ARView {
|
||||
//MARK: - Public objects to configure RecordAR
|
||||
/**
|
||||
An object that passes the AR recorder errors and status in the protocol methods.
|
||||
*/
|
||||
@objc public var delegate:RecordARDelegate?
|
||||
/**
|
||||
An object that passes the AR rendered content in the protocol method.
|
||||
*/
|
||||
@objc public var renderAR: RenderARDelegate?
|
||||
/**
|
||||
An object that returns the AR recorder current status.
|
||||
*/
|
||||
@objc public internal(set)var status:RecordARStatus = .unknown
|
||||
/**
|
||||
An object that returns the current Microphone status.
|
||||
*/
|
||||
@objc public internal(set)var micStatus:RecordARMicrophoneStatus = .unknown
|
||||
/**
|
||||
An object that allow customizing when to ask for Microphone permission, if needed. Default is `.auto`.
|
||||
*/
|
||||
@objc public var requestMicPermission:RecordARMicrophonePermission = .auto
|
||||
/**
|
||||
An object that allow customizing the video frame per second rate. Default is `.auto`.
|
||||
*/
|
||||
@objc public var fps:ARVideoFrameRate = .auto
|
||||
/**
|
||||
An object that allow customizing the video orientation. Default is `.auto`.
|
||||
*/
|
||||
@objc public var videoOrientation:ARVideoOrientation = .auto
|
||||
/**
|
||||
An object that allow customizing the AR content mode. Default is `.auto`.
|
||||
*/
|
||||
@objc public var contentMode:ARFrameMode = .auto
|
||||
/**
|
||||
A boolean that enables or disables AR content rendering before recording for image & video processing. Default is `true`.
|
||||
*/
|
||||
@objc public var onlyRenderWhileRecording:Bool = true
|
||||
/**
|
||||
A boolean that enables or disables audio recording. Default is `true`.
|
||||
*/
|
||||
@objc public var enableAudio:Bool = true
|
||||
/**
|
||||
A boolean that enables or disables audio `mixWithOthers` if audio recording is enabled. This allows playing music and recording audio at the same time. Default is `true`.
|
||||
*/
|
||||
@objc public var enableMixWithOthers:Bool = true
|
||||
/**
|
||||
A boolean that enables or disables adjusting captured media for sharing online. Default is `true`.
|
||||
*/
|
||||
@objc public var adjustVideoForSharing:Bool = true
|
||||
/**
|
||||
A boolean that enables or disables adjusting captured GIFs for sharing online. Default is `true`.
|
||||
*/
|
||||
@objc public var adjustGIFForSharing:Bool = true
|
||||
/**
|
||||
A boolean that enables or disables clearing cached media after exporting to Camera Roll. Default is `true`.
|
||||
*/
|
||||
@objc public var deleteCacheWhenExported:Bool = true
|
||||
/**
|
||||
A boolean that enables or disables using envronment light rendering. Default is `false`.
|
||||
*/
|
||||
@objc public var enableAdjsutEnvironmentLighting:Bool = false {
|
||||
|
||||
didSet{
|
||||
if (renderEngine != nil) {
|
||||
renderEngine.autoenablesDefaultLighting = enableAdjsutEnvironmentLighting
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Public initialization methods
|
||||
/**
|
||||
Initialize 🌞🍳 `RecordAR` with an `ARSCNView` 🚀.
|
||||
*/
|
||||
@objc override public init?(ARSceneKit: ARSCNView) {
|
||||
super.init(ARSceneKit: ARSceneKit)
|
||||
view = ARSceneKit
|
||||
setup()
|
||||
}
|
||||
|
||||
/**
|
||||
Initialize 🌞🍳 `RecordAR` with an `ARSKView` 👾.
|
||||
*/
|
||||
@objc override public init?(ARSpriteKit: ARSKView) {
|
||||
super.init(ARSpriteKit: ARSpriteKit)
|
||||
view = ARSpriteKit
|
||||
scnView = SCNView(frame: UIScreen.main.bounds)
|
||||
|
||||
let bundle = Bundle(for: RecordAR.self)
|
||||
let url = bundle.url(forResource: "video.scnassets/vid", withExtension: "scn")
|
||||
|
||||
do {
|
||||
let scene = try SCNScene(url: url!, options: nil)
|
||||
scnView.scene = scene
|
||||
setup()
|
||||
}catch let error {
|
||||
logAR.message("Error occurred while loading SK Video Assets : \(error). Please download \"video.scnassets\" from\nwww.ahmedbekhit.com/ARVideoKitAssets")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Initialize 🌞🍳 `RecordAR` with an `SCNView` 🚀.
|
||||
*/
|
||||
@objc override public init?(SceneKit: SCNView) {
|
||||
super.init(SceneKit: SceneKit)
|
||||
view = SceneKit
|
||||
setup()
|
||||
}
|
||||
|
||||
//MARK: - Internal threads
|
||||
internal let writerQueue = DispatchQueue(label:"com.ahmedbekhit.WriterQueue")
|
||||
internal let gifWriterQueue = DispatchQueue(label: "com.ahmedbekhit.GIFWriterQueue", attributes: .concurrent)
|
||||
internal let audioSessionQueue = DispatchQueue(label: "com.ahmedbekhit.AudioSessionQueue", attributes: .concurrent)
|
||||
|
||||
//MARK: - Internal Objects
|
||||
fileprivate var scnView:SCNView!
|
||||
fileprivate var fileCount = 0
|
||||
|
||||
internal var parent:UIViewController? {
|
||||
if let view = view as? ARSCNView {
|
||||
return view.parent!
|
||||
}else if let view = view as? ARSKView {
|
||||
return view.parent!
|
||||
}else if let view = view as? SCNView {
|
||||
return view.parent!
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//Used for gif capturing
|
||||
internal var gifImages:[UIImage] = []
|
||||
//Used for checking current recorder status
|
||||
internal var isCapturingPhoto = false
|
||||
internal var isRecordingGIF = false
|
||||
internal var isRecording = false
|
||||
internal var adjustPausedTime = false
|
||||
internal var backFromPause = false
|
||||
internal var recordingWithLimit = false
|
||||
internal var onlyRenderWhileRec = false
|
||||
//Used to modify video time when paused
|
||||
internal var pausedFrameTime:CMTime?
|
||||
internal var resumeFrameTime:CMTime?
|
||||
//Used to locate the path of the video recording
|
||||
internal var currentVideoPath:URL?
|
||||
//Used to locate the path of the audio recording
|
||||
internal var currentAudioPath:URL?
|
||||
//Used to initialize the video writer
|
||||
internal var writer:WritAR?
|
||||
//Used to generate a new video path
|
||||
internal var newVideoPath:URL {
|
||||
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
|
||||
let documentsDirectory = paths[0]
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .full
|
||||
formatter.timeStyle = .full
|
||||
formatter.dateFormat = "yyyy-MM-dd'@'HH-mm-ssZZZZ"
|
||||
|
||||
let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970)
|
||||
|
||||
let vidPath = "\(documentsDirectory)/\(formatter.string(from: date))ARVideo.mp4"
|
||||
return URL(fileURLWithPath: vidPath, isDirectory: false)
|
||||
}
|
||||
|
||||
//MARK: - Internal Video Setup
|
||||
internal func setup() {
|
||||
if let view = view as? ARSCNView {
|
||||
guard let mtlDevice = MTLCreateSystemDefaultDevice() else {logAR.message("ERROR:- This device does not support Metal");return}
|
||||
renderEngine = SCNRenderer(device: mtlDevice, options: nil)
|
||||
renderEngine.scene = view.scene
|
||||
|
||||
gpuLoop = CADisplayLink(target: self, selector: #selector(renderFrame))
|
||||
gpuLoop.preferredFramesPerSecond = fps.rawValue
|
||||
gpuLoop.add(to: .main, forMode: .commonModes)
|
||||
|
||||
status = .readyToRecord
|
||||
}else if let view = view as? ARSKView {
|
||||
guard let mtlDevice = MTLCreateSystemDefaultDevice() else {logAR.message("ERROR:- This device does not support Metal");return}
|
||||
let material = SCNMaterial()
|
||||
material.diffuse.contents = view.scene
|
||||
|
||||
let plane = SCNPlane(width: view.bounds.width, height: view.bounds.height)
|
||||
let node = SCNNode(geometry: plane)
|
||||
node.geometry?.firstMaterial = material
|
||||
node.position = SCNVector3Make(0, 0, 0)
|
||||
|
||||
scnView.scene?.rootNode.addChildNode(node)
|
||||
|
||||
renderEngine = SCNRenderer(device: mtlDevice, options: nil)
|
||||
renderEngine.scene = scnView.scene
|
||||
|
||||
gpuLoop = CADisplayLink(target: self, selector: #selector(renderFrame))
|
||||
gpuLoop.preferredFramesPerSecond = fps.rawValue
|
||||
gpuLoop.add(to: .main, forMode: .commonModes)
|
||||
|
||||
status = .readyToRecord
|
||||
}else if let view = view as? SCNView {
|
||||
guard let mtlDevice = MTLCreateSystemDefaultDevice() else {logAR.message("ERROR:- This device does not support Metal");return}
|
||||
renderEngine = SCNRenderer(device: mtlDevice, options: nil)
|
||||
renderEngine.scene = view.scene
|
||||
|
||||
gpuLoop = CADisplayLink(target: self, selector: #selector(renderFrame))
|
||||
gpuLoop.preferredFramesPerSecond = fps.rawValue
|
||||
gpuLoop.add(to: .main, forMode: .commonModes)
|
||||
|
||||
status = .readyToRecord
|
||||
}
|
||||
|
||||
switch requestMicPermission {
|
||||
case .auto:
|
||||
AVAudioSession.sharedInstance().requestRecordPermission({ permitted in
|
||||
if permitted {
|
||||
self.micStatus = .enabled
|
||||
}else{
|
||||
self.micStatus = .disabled
|
||||
}
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
onlyRenderWhileRec = onlyRenderWhileRecording
|
||||
|
||||
renderer = RenderAR(view, renderer: renderEngine, contentMode: contentMode)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterBackground), name: Notification.Name.UIApplicationWillResignActive, object: nil)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//MARK: - Public methods for capturing videos, photos, Live Photos, and GIFs
|
||||
|
||||
/// A method that renders a photo 🌄 and returns it as `UIImage`.
|
||||
@objc public func photo() -> UIImage {
|
||||
if let buffer = renderer.buffer {
|
||||
return imageFromBuffer(buffer: buffer)
|
||||
}
|
||||
return UIImage()
|
||||
}
|
||||
/**
|
||||
A method that renders a `PHLivePhoto` 🎇 and returns `PHLivePhotoPlus` in the completion handler.
|
||||
|
||||
In order to manually export the `PHLivePhotoPlus`, use `export(live photo:PHLivePhotoPlus)` method.
|
||||
- parameter export: A boolean that enables or disables automatically exporting the `PHLivePhotoPlus` when ready.
|
||||
- parameter finished: A block that will be called when Live Photo rendering is complete.
|
||||
|
||||
The block returns the following parameters:
|
||||
|
||||
`status`
|
||||
A boolean that returns `true` when a `PHLivePhotoPlus` is successfully rendered. Otherwise, it returns `false`.
|
||||
|
||||
`livePhoto`
|
||||
A `PHLivePhotoPlus` object that contains a `PHLivePhoto` and other objects to allow manual exporting of a live photo.
|
||||
|
||||
`permissionStatus`
|
||||
A `PHAuthorizationStatus` object that returns the current application's status for exporting media to the Photo Library. It returns `nil` if the `export` parameter is `false`.
|
||||
|
||||
`exported`
|
||||
A boolean that returns `true` when a `PHLivePhotoPlus` is successfully exported to the Photo Library. Otherwise, it returns `false`.
|
||||
*/
|
||||
@objc public func livePhoto(export:Bool, _ finished: ((_ status:Bool, _ livePhoto:PHLivePhotoPlus, _ permissionStatus:PHAuthorizationStatus, _ exported:Bool) -> Swift.Void)? = nil) {
|
||||
self.record(forDuration: 3.0) { path in
|
||||
let generator:LivePhotoGenerator? = LivePhotoGenerator()
|
||||
generator?.generate(livePhoto: path) { success, photo, frames, keyFrame in
|
||||
if success && export {
|
||||
if self.fileCount == 0 {
|
||||
self.fileCount += 1
|
||||
self.export(live: photo!) { done, status in
|
||||
finished?(true, photo!, status, done)
|
||||
}
|
||||
}
|
||||
}else{
|
||||
finished?(success, photo!, PHAuthorizationStatus.notDetermined, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
A method that generates a GIF 🎆 image and returns its local path (`URL`) in the completion handler.
|
||||
|
||||
In order to manually export the GIF image `URL`, use `func export(image path:URL)` method.
|
||||
- parameter duration: A `TimeInterval` object that can be set to the duration specified in seconds.
|
||||
- parameter export: A boolean that enables or disables automatically exporting the GIF image `URL` when ready.
|
||||
- parameter finished: A block that will be called when GIF image rendering is complete.
|
||||
|
||||
The block returns the following parameters:
|
||||
|
||||
`status`
|
||||
A boolean that returns `true` when a GIF image `URL` is successfully rendered. Otherwise, it returns `false`.
|
||||
|
||||
`gifPath`
|
||||
A `URL` object that contains the local file path of the GIF image to allow manual exporting of a GIF.
|
||||
|
||||
`permissionStatus`
|
||||
A `PHAuthorizationStatus` object that returns the current application's status for exporting media to the Photo Library. It returns `nil` if the `export` parameter is `false`.
|
||||
|
||||
`exported`
|
||||
A boolean that returns `true` when a GIF image `URL` is successfully exported to the Photo Library. Otherwise, it returns `false`.
|
||||
*/
|
||||
@objc public func gif(forDuration duration:TimeInterval, export:Bool, _ finished: ((_ status:Bool, _ gifPath: URL, _ permissionStatus:PHAuthorizationStatus, _ exported:Bool) -> Swift.Void)? = nil) {
|
||||
writerQueue.sync {
|
||||
self.isRecordingGIF = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
|
||||
self.isRecordingGIF = false
|
||||
let generator:GIFGenerator? = GIFGenerator()
|
||||
generator?.generate(gif: self.gifImages, with: 0.1, loop: 0, adjust: self.adjustGIFForSharing) {ready, path in
|
||||
if ready {
|
||||
self.gifImages.removeAll()
|
||||
if export {
|
||||
self.export(image: path!) { done, status in
|
||||
finished?(ready, path!, status, done)
|
||||
}
|
||||
}else{
|
||||
finished?(ready, path!, .notDetermined, false)
|
||||
}
|
||||
}else{
|
||||
self.gifImages.removeAll()
|
||||
finished?(ready, path!, .notDetermined, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
///A method that starts or resumes ⏯ recording a video 📹.
|
||||
@objc public func record() {
|
||||
writerQueue.sync {
|
||||
self.isRecording = true
|
||||
self.status = .recording
|
||||
}
|
||||
}
|
||||
/**
|
||||
A method that starts recording a video 📹 with a specified duration ⏳ in seconds.
|
||||
|
||||
In order to stop the recording before the specified duration, simply call `stop()` or `stopAndExport()` methods.
|
||||
|
||||
- WARNING : You CAN NOT `pause()` video recording when a duration is specified.
|
||||
- parameter duration: A `TimeInterval` object that can be set to the duration specified in seconds.
|
||||
- parameter finished: A block that will be called when the specified `duration` has ended.
|
||||
|
||||
The block returns the following parameter:
|
||||
|
||||
`videoPath`
|
||||
A `URL` object that contains the local file path of the video to allow manual exporting or preview of the video.
|
||||
*/
|
||||
@objc public func record(forDuration duration:TimeInterval, _ finished: ((_ videoPath: URL) -> Swift.Void)? = nil) {
|
||||
writerQueue.sync {
|
||||
self.recordingWithLimit = true
|
||||
self.isRecording = true
|
||||
self.status = .recording
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
|
||||
self.stop { path in
|
||||
finished?(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
A method that pauses recording a video ⏸📹.
|
||||
|
||||
In order to resume recording, simply call the `record()` method.
|
||||
*/
|
||||
@objc public func pause() {
|
||||
if !recordingWithLimit {
|
||||
onlyRenderWhileRec = false
|
||||
isRecording = false
|
||||
adjustPausedTime = true
|
||||
}else{
|
||||
logAR.message("NOT PERMITTED: The [ pause() ] method CAN NOT be used while using [ record(forDuration duration:TimeInterval) ]")
|
||||
}
|
||||
}
|
||||
/**
|
||||
A method that stops ⏹ recording a video 📹 and exports it to the Photo Library 📲💾.
|
||||
|
||||
- parameter finished: A block that will be called when the export process is complete.
|
||||
|
||||
The block returns the following parameters:
|
||||
|
||||
`videoPath`
|
||||
A `URL` object that contains the local file path of the video to allow manual exporting or preview of the video.
|
||||
|
||||
`permissionStatus`
|
||||
A `PHAuthorizationStatus` object that returns the current application's status for exporting media to the Photo Library.
|
||||
|
||||
`exported`
|
||||
A boolean that returns `true` when a video is successfully exported to the Photo Library. Otherwise, it returns `false`.
|
||||
*/
|
||||
@objc public func stopAndExport(_ finished: ((_ videoPath: URL, _ permissionStatus:PHAuthorizationStatus, _ exported:Bool) -> Swift.Void)? = nil) {
|
||||
writerQueue.sync {
|
||||
self.isRecording = false
|
||||
self.adjustPausedTime = false
|
||||
self.backFromPause = false
|
||||
self.recordingWithLimit = false
|
||||
|
||||
self.pausedFrameTime = nil
|
||||
self.resumeFrameTime = nil
|
||||
|
||||
self.writer?.end {
|
||||
if let path = self.currentVideoPath {
|
||||
self.export(video: path) { exported, status in
|
||||
finished?(path, status, exported)
|
||||
}
|
||||
self.delegate?.recorder(didEndRecording: path, with: true)
|
||||
self.status = .readyToRecord
|
||||
}else{
|
||||
finished?(self.currentVideoPath!, .notDetermined, false)
|
||||
self.status = .readyToRecord
|
||||
self.delegate?.recorder(didFailRecording: errSecDecode as? Error, and: "An error occured while stopping your video.")
|
||||
}
|
||||
self.writer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
A method that stops ⏹ recording a video 📹 and returns the video path in the completion handler.
|
||||
|
||||
- parameter finished: A block that will be called when the specified `duration` has ended.
|
||||
|
||||
The block returns the following parameter:
|
||||
|
||||
`videoPath`
|
||||
A `URL` object that contains the local file path of the video to allow manual exporting or preview of the video.
|
||||
*/
|
||||
@objc public func stop(_ finished:((_ videoPath: URL) -> Swift.Void)? = nil) {
|
||||
writerQueue.sync {
|
||||
isRecording = false
|
||||
adjustPausedTime = false
|
||||
backFromPause = false
|
||||
recordingWithLimit = false
|
||||
|
||||
pausedFrameTime = nil
|
||||
resumeFrameTime = nil
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.writer?.end {
|
||||
if let path = self.currentVideoPath {
|
||||
finished?(path)
|
||||
self.delegate?.recorder(didEndRecording: path, with: true)
|
||||
self.status = .readyToRecord
|
||||
}else{
|
||||
self.status = .readyToRecord
|
||||
self.delegate?.recorder(didFailRecording: errSecDecode as? Error, and: "An error occured while stopping your video.")
|
||||
}
|
||||
self.writer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
A method that exports a video 📹 file path to the Photo Library 📲💾.
|
||||
|
||||
- parameter path: A `URL` object that can be set to a local video file path to export to the Photo Library.
|
||||
|
||||
- parameter finished: A block that will be called when the export process is complete.
|
||||
|
||||
The block returns the following parameters:
|
||||
|
||||
`exported`
|
||||
A boolean that returns `true` when a video is successfully exported to the Photo Library. Otherwise, it returns `false`.
|
||||
|
||||
`permissionStatus`
|
||||
A `PHAuthorizationStatus` object that returns the current application's status for exporting media to the Photo Library.
|
||||
*/
|
||||
@objc public func export(video path:URL, _ finished: ((_ exported:Bool, _ permissionStatus:PHAuthorizationStatus) -> Swift.Void)? = nil) {
|
||||
audioSessionQueue.async {
|
||||
let photos = PHPhotoLibrary.authorizationStatus()
|
||||
if photos == .notDetermined {
|
||||
PHPhotoLibrary.requestAuthorization({ status in
|
||||
if status == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: path)
|
||||
}) { saved, _ in
|
||||
if saved && self.deleteCacheWhenExported {
|
||||
logAR.remove(from: path)
|
||||
}
|
||||
finished?(saved, status)
|
||||
}
|
||||
}else{
|
||||
finished?(false, status)
|
||||
}
|
||||
})
|
||||
}else if photos == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: path)
|
||||
}) { saved, error in
|
||||
if saved && self.deleteCacheWhenExported {
|
||||
logAR.remove(from: path)
|
||||
}
|
||||
finished?(saved, photos)
|
||||
}
|
||||
}else if photos == .denied || photos == .restricted {
|
||||
finished?(false, photos)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
A method that exports any image 🌄/🎆 (including gif, jpeg, and png) to the Photo Library 📲💾.
|
||||
|
||||
- parameter path: A `URL` object that can be set to a local image file path to export to the Photo Library.
|
||||
- parameter UIImage: A `UIImage` object.
|
||||
- parameter finished: A block that will be called when the export process is complete.
|
||||
|
||||
The block returns the following parameters:
|
||||
|
||||
`exported`
|
||||
A boolean that returns `true` when an image is successfully exported to the Photo Library. Otherwise, it returns `false`.
|
||||
|
||||
`permissionStatus`
|
||||
A `PHAuthorizationStatus` object that returns the current application's status for exporting media to the Photo Library.
|
||||
*/
|
||||
@objc public func export(image path:URL?=nil, UIImage:UIImage?=nil, _ finished: ((_ exported:Bool, _ permissionStatus:PHAuthorizationStatus) -> Swift.Void)? = nil) {
|
||||
let photos = PHPhotoLibrary.authorizationStatus()
|
||||
if photos == .notDetermined {
|
||||
PHPhotoLibrary.requestAuthorization({ status in
|
||||
if status == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
if let path = path {
|
||||
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: path)
|
||||
}else if let image = UIImage {
|
||||
PHAssetChangeRequest.creationRequestForAsset(from: image)
|
||||
}
|
||||
}) { saved, error in
|
||||
if saved && self.deleteCacheWhenExported {
|
||||
if let path = path {
|
||||
logAR.remove(from: path)
|
||||
}
|
||||
}
|
||||
finished?(saved, status)
|
||||
}
|
||||
}else{
|
||||
finished?(false, status)
|
||||
}
|
||||
})
|
||||
}else if photos == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
if let path = path {
|
||||
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: path)
|
||||
}else if let image = UIImage {
|
||||
PHAssetChangeRequest.creationRequestForAsset(from: image)
|
||||
}
|
||||
}) { saved, error in
|
||||
if saved && self.deleteCacheWhenExported {
|
||||
if let path = path {
|
||||
logAR.remove(from: path)
|
||||
}
|
||||
}
|
||||
finished?(saved, photos)
|
||||
}
|
||||
}else if photos == .denied || photos == .restricted {
|
||||
finished?(false, photos)
|
||||
}
|
||||
}
|
||||
/**
|
||||
A method that exports a `PHLivePhotoPlus` 🎇 object to the Photo Library 📲💾.
|
||||
|
||||
- parameter photo: A `PHLivePhotoPlus` object that can be set to the returned `PHLivePhotoPlus` object in the `livePhoto(export:Bool, _ finished:{})` method.
|
||||
|
||||
- parameter finished: A block that will be called when the export process is complete.
|
||||
|
||||
The block returns the following parameters:
|
||||
|
||||
`exported`
|
||||
A boolean that returns `true` when the Live Photo is successfully exported to the Photo Library. Otherwise, it returns `false`.
|
||||
|
||||
`permissionStatus`
|
||||
A `PHAuthorizationStatus` object that returns the current application's status for exporting media to the Photo Library.
|
||||
*/
|
||||
@objc public func export(live photo:PHLivePhotoPlus, _ finished: ((_ exported:Bool, _ permissionStatus:PHAuthorizationStatus) -> Swift.Void)? = nil) {
|
||||
guard let keyPhotoPath = photo.keyPhotoPath else{logAR.message("An error occurred while exporting a live photo"); return}
|
||||
guard let videoPath = photo.pairedVideoPath else{logAR.message("An error occurred while exporting a live photo"); return}
|
||||
|
||||
let photos = PHPhotoLibrary.authorizationStatus()
|
||||
if photos == .notDetermined {
|
||||
PHPhotoLibrary.requestAuthorization({ status in
|
||||
if status == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let request = PHAssetCreationRequest.forAsset()
|
||||
let options = PHAssetResourceCreationOptions()
|
||||
request.addResource(with: .photo, fileURL: keyPhotoPath, options: options)
|
||||
request.addResource(with: .pairedVideo, fileURL: videoPath, options: options)
|
||||
}, completionHandler: { saved, error in
|
||||
if saved {
|
||||
if self.deleteCacheWhenExported {
|
||||
logAR.remove(from: keyPhotoPath)
|
||||
logAR.remove(from: videoPath)
|
||||
}
|
||||
self.fileCount = 0
|
||||
}else{
|
||||
logAR.message("An error occurred while exporting a live photo: \(error!)")
|
||||
}
|
||||
finished?(saved, status)
|
||||
})
|
||||
}else{
|
||||
finished?(false, status)
|
||||
}
|
||||
})
|
||||
}else if photos == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let request = PHAssetCreationRequest.forAsset()
|
||||
let options = PHAssetResourceCreationOptions()
|
||||
request.addResource(with: .photo, fileURL: keyPhotoPath, options: options)
|
||||
request.addResource(with: .pairedVideo, fileURL: videoPath, options: options)
|
||||
}, completionHandler: { saved, error in
|
||||
if saved {
|
||||
if self.deleteCacheWhenExported {
|
||||
logAR.remove(from: keyPhotoPath)
|
||||
logAR.remove(from: videoPath)
|
||||
}
|
||||
self.fileCount = 0
|
||||
}else{
|
||||
logAR.message("An error occurred while exporting a live photo: \(error!)")
|
||||
}
|
||||
finished?(saved, photos)
|
||||
})
|
||||
}else if photos == .denied || photos == .restricted {
|
||||
finished?(false, photos)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A method that requsts microphone 🎙 permission manually, if micPermission is set to `manual`.
|
||||
- parameter finished: A block that will be called when the audio permission is requested.
|
||||
|
||||
The block returns the following parameter:
|
||||
|
||||
`status`
|
||||
A boolean that returns `true` when a the Microphone access is permitted. Otherwise, it returns `false`.
|
||||
*/
|
||||
@objc public func requestMicrophonePermission(_ finished: ((_ status: Bool) -> Swift.Void)? = nil) {
|
||||
AVAudioSession.sharedInstance().requestRecordPermission({ permitted in
|
||||
finished?(permitted)
|
||||
if permitted {
|
||||
self.micStatus = .enabled
|
||||
}else{
|
||||
self.micStatus = .disabled
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Public methods for setting up UIViewController orientations
|
||||
@available(iOS 11.0, *)
|
||||
@objc public extension RecordAR {
|
||||
/**
|
||||
A method that prepares the video recorder with `ARConfiguration` 📝.
|
||||
|
||||
Recommended to use in the `UIViewController`'s method `func viewWillAppear(_ animated: Bool)`
|
||||
- parameter configuration: An object that defines motion and scene tracking behaviors for the session.
|
||||
*/
|
||||
@objc public func prepare(_ configuration:ARConfiguration?=nil) {
|
||||
ARcontentMode = contentMode
|
||||
onlyRenderWhileRec = onlyRenderWhileRecording
|
||||
if let view = view as? ARSCNView {
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
||||
ViewAR.orientation = .portrait
|
||||
|
||||
//try resetting anchors for the initial landscape orientation issue.
|
||||
guard let config = configuration else {return}
|
||||
view.session.run(config)
|
||||
}else if let view = view as? ARSKView {
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
||||
ViewAR.orientation = .portrait
|
||||
guard let config = configuration else {return}
|
||||
view.session.run(config)
|
||||
}else if let _ = view as? SCNView {
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
||||
ViewAR.orientation = .portrait
|
||||
}
|
||||
}
|
||||
/**
|
||||
A method that switches off the orientation lock used in a `UIViewController` with AR scenes 📐😴.
|
||||
|
||||
Recommended to use in the `UIViewController`'s method `func viewWillDisappear(_ animated: Bool)`.
|
||||
*/
|
||||
@objc public func rest() {
|
||||
ViewAR.orientation = UIInterfaceOrientationMask(ViewAR.orientations)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Internal AR Video Frames Rendering
|
||||
@available(iOS 11.0, *)
|
||||
internal extension RecordAR {
|
||||
@objc internal func renderFrame() {
|
||||
//frame rendering
|
||||
if self.onlyRenderWhileRec && !isRecording && !isRecordingGIF {return}
|
||||
guard let buffer = renderer.buffer else{return}
|
||||
guard let rawBuffer = renderer.rawBuffer else{logAR.message("ERROR:- An error occurred while rendering the camera's main buffers.");return}
|
||||
guard let size = renderer.bufferSize else{logAR.message("ERROR:- An error occurred while rendering the camera buffer.");return}
|
||||
renderer.ARcontentMode = contentMode
|
||||
|
||||
self.writerQueue.sync {
|
||||
var time:CMTime {return CMTimeMakeWithSeconds(renderer.time, 1000000);}
|
||||
|
||||
self.renderAR?.frame(didRender: buffer, with: time, using: rawBuffer)
|
||||
|
||||
//gif images writing
|
||||
if self.isRecordingGIF {
|
||||
self.gifWriterQueue.sync {
|
||||
self.gifImages.append(self.imageFromBuffer(buffer: buffer))
|
||||
}
|
||||
}
|
||||
|
||||
//frame writing
|
||||
if self.isRecording {
|
||||
if let frameWriter = self.writer {
|
||||
var finalFrameTime:CMTime?
|
||||
if self.backFromPause {
|
||||
if self.resumeFrameTime == nil {
|
||||
self.resumeFrameTime = time
|
||||
}
|
||||
//Formula: (currentTime - (timeWhenResume - timeWhenPaused))
|
||||
guard let resumeTime = self.resumeFrameTime else {return}
|
||||
guard let pausedTime = self.pausedFrameTime else {return}
|
||||
finalFrameTime = self.adjustTime(current: time, resume: resumeTime, pause: pausedTime)
|
||||
}else{
|
||||
finalFrameTime = time
|
||||
}
|
||||
|
||||
frameWriter.insert(pixel: buffer, with: finalFrameTime!)
|
||||
|
||||
guard let isWriting = frameWriter.isWritingWithoutError else {return}
|
||||
if !isWriting {
|
||||
self.isRecording = false
|
||||
|
||||
self.status = .readyToRecord
|
||||
self.delegate?.recorder(didFailRecording: errSecDecode as? Error, and: "An error occured while recording your video.")
|
||||
self.delegate?.recorder(didEndRecording: self.currentVideoPath!, with: false)
|
||||
}
|
||||
}else{
|
||||
self.currentVideoPath = self.newVideoPath
|
||||
|
||||
self.writer = WritAR(output: self.currentVideoPath!, width: Int(size.width), height: Int(size.height), adjustForSharing: self.adjustVideoForSharing, audioEnabled: self.enableAudio, orientaions: self.inputViewOrientations, queue: self.writerQueue, allowMix: self.enableMixWithOthers)
|
||||
self.writer?.videoInputOrientation = self.videoOrientation
|
||||
self.writer?.delegate = self.delegate
|
||||
}
|
||||
}else if !self.isRecording && self.adjustPausedTime {
|
||||
writer?.pause()
|
||||
|
||||
self.adjustPausedTime = false
|
||||
|
||||
if self.pausedFrameTime != nil {
|
||||
self.pausedFrameTime = self.adjustTime(current: time, resume: self.resumeFrameTime!, pause: self.pausedFrameTime!)
|
||||
}else{
|
||||
self.pausedFrameTime = time
|
||||
}
|
||||
|
||||
self.backFromPause = true
|
||||
self.resumeFrameTime = nil
|
||||
|
||||
self.status = .paused
|
||||
self.onlyRenderWhileRec = onlyRenderWhileRecording
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// prepare.swift
|
||||
// AR Video
|
||||
//
|
||||
// Created by Ahmed Bekhit on 10/16/17.
|
||||
// Copyright © 2017 Ahmed Fathi Bekhit. All rights reserved.
|
||||
//
|
||||
|
||||
import ARKit
|
||||
/**
|
||||
A struct that identifies the application `UIViewController`s and their orientations.
|
||||
|
||||
- Author: Ahmed Fathi Bekhit
|
||||
* [Github](http://github.com/AFathi)
|
||||
* [Website](http://ahmedbekhit.com)
|
||||
* [Twitter](http://twitter.com/iAFapps)
|
||||
* [Email](mailto:me@ahmedbekhit.com)
|
||||
*/
|
||||
@available(iOS 11.0, *)
|
||||
@objc public class ViewAR: NSObject {
|
||||
/**
|
||||
A `UIInterfaceOrientationMask` object that returns the recommended orientations for a `UIViewController` with AR scenes.
|
||||
|
||||
Recommended to return in the application delegate method `func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask`.
|
||||
*/
|
||||
@objc internal(set) public static var orientation: UIInterfaceOrientationMask {get {return mask;}set {mask = newValue}}
|
||||
|
||||
internal static var orientations:[UIInterfaceOrientationMask] {
|
||||
var all:[UIInterfaceOrientationMask] = []
|
||||
if let info = Bundle.main.infoDictionary {
|
||||
if let supportedOrientaions = info["UISupportedInterfaceOrientations"] as? NSArray {
|
||||
for orientation in supportedOrientaions {
|
||||
if let o = orientation as? String {
|
||||
if o == "UIInterfaceOrientationPortrait" {
|
||||
all.append(.portrait)
|
||||
}else if o == "UIInterfaceOrientationPortraitUpsideDown" {
|
||||
all.append(.portraitUpsideDown)
|
||||
}else if o == "UIInterfaceOrientationLandscapeLeft" {
|
||||
all.append(.landscapeLeft)
|
||||
}else if o == "UIInterfaceOrientationLandscapeRight" {
|
||||
all.append(.landscapeRight)
|
||||
}
|
||||
}
|
||||
}
|
||||
return all
|
||||
}
|
||||
}
|
||||
return all
|
||||
}
|
||||
//returns the application's delegate to check if the current UIViewController contains an ARView
|
||||
fileprivate static var delegate = UIApplication.shared.delegate
|
||||
//variable for the setter in `mask`
|
||||
fileprivate static var m: UIInterfaceOrientationMask = .portrait
|
||||
//returns the most appropriate orientation based on the content of the UIViewController.
|
||||
fileprivate static var mask: UIInterfaceOrientationMask {get {if let vc = delegate?.window??.inputViewController {if vc.hasARView {return .portrait}else{return UIInterfaceOrientationMask(orientations)}};return m;}set {m = newValue;}}
|
||||
}
|
||||
Reference in New Issue
Block a user