AVCam: Version 6.1, 2016-09-15
Adopt AVCaptureDeviceDiscoverySession AVCam demonstrates how to use the AVFoundation capture API to record movies and capture still images. The sample has a record button for recording movies, a camera button for switching between front and back cameras (on supported devices), and a still button for capturing still images. AVCam runs only on an actual device, either an iPad or iPhone, and cannot be run in Simulator.
|
@ -0,0 +1,42 @@
|
|||
Sample code project: AVCam-iOS: Using AVFoundation to Capture Images and Movies
|
||||
Version: 6.1
|
||||
|
||||
IMPORTANT: This Apple software is supplied to you by Apple
|
||||
Inc. ("Apple") in consideration of your agreement to the following
|
||||
terms, and your use, installation, modification or redistribution of
|
||||
this Apple software constitutes acceptance of these terms. If you do
|
||||
not agree with these terms, please do not use, install, modify or
|
||||
redistribute this Apple software.
|
||||
|
||||
In consideration of your agreement to abide by the following terms, and
|
||||
subject to these terms, Apple grants you a personal, non-exclusive
|
||||
license, under Apple's copyrights in this original Apple software (the
|
||||
"Apple Software"), to use, reproduce, modify and redistribute the Apple
|
||||
Software, with or without modifications, in source and/or binary forms;
|
||||
provided that if you redistribute the Apple Software in its entirety and
|
||||
without modifications, you must retain this notice and the following
|
||||
text and disclaimers in all such redistributions of the Apple Software.
|
||||
Neither the name, trademarks, service marks or logos of Apple Inc. may
|
||||
be used to endorse or promote products derived from the Apple Software
|
||||
without specific prior written permission from Apple. Except as
|
||||
expressly stated in this notice, no other rights or licenses, express or
|
||||
implied, are granted by Apple herein, including but not limited to any
|
||||
patent rights that may be infringed by your derivative works or by other
|
||||
works in which the Apple Software may be incorporated.
|
||||
|
||||
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
|
||||
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
|
||||
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
|
||||
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
|
||||
|
||||
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
|
||||
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
|
||||
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
|
||||
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
|
@ -0,0 +1,310 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2206265F1A1E330400A45150 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 2206265E1A1E330400A45150 /* main.m */; };
|
||||
220626881A1E345E00A45150 /* AVCamAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 220626831A1E345E00A45150 /* AVCamAppDelegate.m */; };
|
||||
220626891A1E345E00A45150 /* AVCamPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = 220626851A1E345E00A45150 /* AVCamPreviewView.m */; };
|
||||
2206268A1A1E345E00A45150 /* AVCamCameraViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 220626871A1E345E00A45150 /* AVCamCameraViewController.m */; };
|
||||
22CA31B81B022D1300D2DE70 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 22CA31B61B022D1300D2DE70 /* LaunchScreen.storyboard */; };
|
||||
7A74447A1CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A7444791CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.m */; };
|
||||
7A74447C1CEE6B4B00C70C83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A74447B1CEE6B4B00C70C83 /* Assets.xcassets */; };
|
||||
7A74447E1CEE6B5900C70C83 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A74447D1CEE6B5900C70C83 /* Main.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
220626591A1E330400A45150 /* AVCam.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AVCam.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2206265D1A1E330400A45150 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
2206265E1A1E330400A45150 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
220626821A1E345E00A45150 /* AVCamAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AVCamAppDelegate.h; sourceTree = "<group>"; };
|
||||
220626831A1E345E00A45150 /* AVCamAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AVCamAppDelegate.m; sourceTree = "<group>"; };
|
||||
220626841A1E345E00A45150 /* AVCamPreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AVCamPreviewView.h; sourceTree = "<group>"; };
|
||||
220626851A1E345E00A45150 /* AVCamPreviewView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AVCamPreviewView.m; sourceTree = "<group>"; };
|
||||
220626861A1E345E00A45150 /* AVCamCameraViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AVCamCameraViewController.h; sourceTree = "<group>"; };
|
||||
220626871A1E345E00A45150 /* AVCamCameraViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AVCamCameraViewController.m; sourceTree = "<group>"; };
|
||||
22CA31B71B022D1300D2DE70 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = AVCam/Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
22CA31B91B0250C300D2DE70 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
|
||||
7A7444781CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AVCamPhotoCaptureDelegate.h; sourceTree = "<group>"; };
|
||||
7A7444791CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AVCamPhotoCaptureDelegate.m; sourceTree = "<group>"; };
|
||||
7A74447B1CEE6B4B00C70C83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
7A74447D1CEE6B5900C70C83 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
220626561A1E330400A45150 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
220626501A1E330400A45150 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
22CA31B91B0250C300D2DE70 /* README.md */,
|
||||
2206265B1A1E330400A45150 /* AVCam */,
|
||||
2206265A1A1E330400A45150 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2206265A1A1E330400A45150 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
220626591A1E330400A45150 /* AVCam.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2206265B1A1E330400A45150 /* AVCam */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
220626821A1E345E00A45150 /* AVCamAppDelegate.h */,
|
||||
220626831A1E345E00A45150 /* AVCamAppDelegate.m */,
|
||||
220626841A1E345E00A45150 /* AVCamPreviewView.h */,
|
||||
220626851A1E345E00A45150 /* AVCamPreviewView.m */,
|
||||
220626861A1E345E00A45150 /* AVCamCameraViewController.h */,
|
||||
220626871A1E345E00A45150 /* AVCamCameraViewController.m */,
|
||||
7A7444781CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.h */,
|
||||
7A7444791CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.m */,
|
||||
7A74447D1CEE6B5900C70C83 /* Main.storyboard */,
|
||||
7A74447B1CEE6B4B00C70C83 /* Assets.xcassets */,
|
||||
22CA31B61B022D1300D2DE70 /* LaunchScreen.storyboard */,
|
||||
2206265D1A1E330400A45150 /* Info.plist */,
|
||||
2206265E1A1E330400A45150 /* main.m */,
|
||||
);
|
||||
path = AVCam;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
220626581A1E330400A45150 /* AVCam */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 2206267C1A1E330400A45150 /* Build configuration list for PBXNativeTarget "AVCam" */;
|
||||
buildPhases = (
|
||||
220626551A1E330400A45150 /* Sources */,
|
||||
220626561A1E330400A45150 /* Frameworks */,
|
||||
220626571A1E330400A45150 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = AVCam;
|
||||
productName = AVCam;
|
||||
productReference = 220626591A1E330400A45150 /* AVCam.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
220626511A1E330400A45150 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0800;
|
||||
TargetAttributes = {
|
||||
220626581A1E330400A45150 = {
|
||||
CreatedOnToolsVersion = 6.1;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 220626541A1E330400A45150 /* Build configuration list for PBXProject "AVCam Objective-C" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 220626501A1E330400A45150;
|
||||
productRefGroup = 2206265A1A1E330400A45150 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
220626581A1E330400A45150 /* AVCam */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
220626571A1E330400A45150 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7A74447E1CEE6B5900C70C83 /* Main.storyboard in Resources */,
|
||||
7A74447C1CEE6B4B00C70C83 /* Assets.xcassets in Resources */,
|
||||
22CA31B81B022D1300D2DE70 /* LaunchScreen.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
220626551A1E330400A45150 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
220626881A1E345E00A45150 /* AVCamAppDelegate.m in Sources */,
|
||||
2206265F1A1E330400A45150 /* main.m in Sources */,
|
||||
220626891A1E345E00A45150 /* AVCamPreviewView.m in Sources */,
|
||||
2206268A1A1E345E00A45150 /* AVCamCameraViewController.m in Sources */,
|
||||
7A74447A1CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
22CA31B61B022D1300D2DE70 /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
22CA31B71B022D1300D2DE70 /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
path = ..;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
2206267A1A1E330400A45150 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
2206267B1A1E330400A45150 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
2206267D1A1E330400A45150 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_FILE = AVCam/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVCam";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
2206267E1A1E330400A45150 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_FILE = AVCam/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVCam";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
220626541A1E330400A45150 /* Build configuration list for PBXProject "AVCam Objective-C" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
2206267A1A1E330400A45150 /* Debug */,
|
||||
2206267B1A1E330400A45150 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
2206267C1A1E330400A45150 /* Build configuration list for PBXNativeTarget "AVCam" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
2206267D1A1E330400A45150 /* Debug */,
|
||||
2206267E1A1E330400A45150 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 220626511A1E330400A45150 /* Project object */;
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "220626581A1E330400A45150"
|
||||
BuildableName = "AVCam.app"
|
||||
BlueprintName = "AVCam"
|
||||
ReferencedContainer = "container:AVCam Objective-C.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "220626581A1E330400A45150"
|
||||
BuildableName = "AVCam.app"
|
||||
BlueprintName = "AVCam"
|
||||
ReferencedContainer = "container:AVCam Objective-C.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "220626581A1E330400A45150"
|
||||
BuildableName = "AVCam.app"
|
||||
BlueprintName = "AVCam"
|
||||
ReferencedContainer = "container:AVCam Objective-C.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "220626581A1E330400A45150"
|
||||
BuildableName = "AVCam.app"
|
||||
BlueprintName = "AVCam"
|
||||
ReferencedContainer = "container:AVCam Objective-C.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Application delegate.
|
||||
*/
|
||||
|
||||
@import UIKit;
|
||||
|
||||
@interface AVCamAppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
@property (nonatomic) UIWindow *window;
|
||||
|
||||
@end
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Application delegate.
|
||||
*/
|
||||
|
||||
#import "AVCamAppDelegate.h"
|
||||
|
||||
@implementation AVCamAppDelegate
|
||||
|
||||
@end
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
View controller for camera interface.
|
||||
*/
|
||||
|
||||
@import UIKit;
|
||||
|
||||
@interface AVCamCameraViewController : UIViewController
|
||||
|
||||
@end
|
|
@ -0,0 +1,947 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
View controller for camera interface.
|
||||
*/
|
||||
|
||||
@import AVFoundation;
|
||||
@import Photos;
|
||||
|
||||
#import "AVCamCameraViewController.h"
|
||||
#import "AVCamPreviewView.h"
|
||||
#import "AVCamPhotoCaptureDelegate.h"
|
||||
|
||||
static void * SessionRunningContext = &SessionRunningContext;
|
||||
|
||||
typedef NS_ENUM( NSInteger, AVCamSetupResult ) {
|
||||
AVCamSetupResultSuccess,
|
||||
AVCamSetupResultCameraNotAuthorized,
|
||||
AVCamSetupResultSessionConfigurationFailed
|
||||
};
|
||||
|
||||
typedef NS_ENUM( NSInteger, AVCamCaptureMode ) {
|
||||
AVCamCaptureModePhoto = 0,
|
||||
AVCamCaptureModeMovie = 1
|
||||
};
|
||||
|
||||
typedef NS_ENUM( NSInteger, AVCamLivePhotoMode ) {
|
||||
AVCamLivePhotoModeOn,
|
||||
AVCamLivePhotoModeOff
|
||||
};
|
||||
|
||||
@interface AVCaptureDeviceDiscoverySession (Utilities)
|
||||
|
||||
- (NSInteger)uniqueDevicePositionsCount;
|
||||
|
||||
@end
|
||||
|
||||
@implementation AVCaptureDeviceDiscoverySession (Utilities)
|
||||
|
||||
- (NSInteger)uniqueDevicePositionsCount
|
||||
{
|
||||
NSMutableArray<NSNumber *> *uniqueDevicePositions = [NSMutableArray array];
|
||||
|
||||
for ( AVCaptureDevice *device in self.devices ) {
|
||||
if ( ! [uniqueDevicePositions containsObject:@(device.position)] ) {
|
||||
[uniqueDevicePositions addObject:@(device.position)];
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueDevicePositions.count;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface AVCamCameraViewController () <AVCaptureFileOutputRecordingDelegate>
|
||||
|
||||
// Session management.
|
||||
@property (nonatomic, weak) IBOutlet AVCamPreviewView *previewView;
|
||||
@property (nonatomic, weak) IBOutlet UISegmentedControl *captureModeControl;
|
||||
|
||||
@property (nonatomic) AVCamSetupResult setupResult;
|
||||
@property (nonatomic) dispatch_queue_t sessionQueue;
|
||||
@property (nonatomic) AVCaptureSession *session;
|
||||
@property (nonatomic, getter=isSessionRunning) BOOL sessionRunning;
|
||||
@property (nonatomic) AVCaptureDeviceInput *videoDeviceInput;
|
||||
|
||||
// Device configuration.
|
||||
@property (nonatomic, weak) IBOutlet UIButton *cameraButton;
|
||||
@property (nonatomic, weak) IBOutlet UILabel *cameraUnavailableLabel;
|
||||
@property (nonatomic) AVCaptureDeviceDiscoverySession *videoDeviceDiscoverySession;
|
||||
|
||||
// Capturing photos.
|
||||
@property (nonatomic, weak) IBOutlet UIButton *photoButton;
|
||||
@property (nonatomic, weak) IBOutlet UIButton *livePhotoModeButton;
|
||||
@property (nonatomic) AVCamLivePhotoMode livePhotoMode;
|
||||
@property (nonatomic, weak) IBOutlet UILabel *capturingLivePhotoLabel;
|
||||
|
||||
@property (nonatomic) AVCapturePhotoOutput *photoOutput;
|
||||
@property (nonatomic) NSMutableDictionary<NSNumber *, AVCamPhotoCaptureDelegate *> *inProgressPhotoCaptureDelegates;
|
||||
@property (nonatomic) NSInteger inProgressLivePhotoCapturesCount;
|
||||
|
||||
// Recording movies.
|
||||
@property (nonatomic, weak) IBOutlet UIButton *recordButton;
|
||||
@property (nonatomic, weak) IBOutlet UIButton *resumeButton;
|
||||
|
||||
@property (nonatomic, strong) AVCaptureMovieFileOutput *movieFileOutput;
|
||||
@property (nonatomic) UIBackgroundTaskIdentifier backgroundRecordingID;
|
||||
|
||||
@end
|
||||
|
||||
@implementation AVCamCameraViewController
|
||||
|
||||
#pragma mark View Controller Life Cycle
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
// Disable UI. The UI is enabled if and only if the session starts running.
|
||||
self.cameraButton.enabled = NO;
|
||||
self.recordButton.enabled = NO;
|
||||
self.photoButton.enabled = NO;
|
||||
self.livePhotoModeButton.enabled = NO;
|
||||
self.captureModeControl.enabled = NO;
|
||||
|
||||
// Create the AVCaptureSession.
|
||||
self.session = [[AVCaptureSession alloc] init];
|
||||
|
||||
// Create a device discovery session.
|
||||
NSArray<AVCaptureDeviceType> *deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInDuoCamera];
|
||||
self.videoDeviceDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionUnspecified];
|
||||
|
||||
// Set up the preview view.
|
||||
self.previewView.session = self.session;
|
||||
|
||||
// Communicate with the session and other session objects on this queue.
|
||||
self.sessionQueue = dispatch_queue_create( "session queue", DISPATCH_QUEUE_SERIAL );
|
||||
|
||||
self.setupResult = AVCamSetupResultSuccess;
|
||||
|
||||
/*
|
||||
Check video authorization status. Video access is required and audio
|
||||
access is optional. If audio access is denied, audio is not recorded
|
||||
during movie recording.
|
||||
*/
|
||||
switch ( [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] )
|
||||
{
|
||||
case AVAuthorizationStatusAuthorized:
|
||||
{
|
||||
// The user has previously granted access to the camera.
|
||||
break;
|
||||
}
|
||||
case AVAuthorizationStatusNotDetermined:
|
||||
{
|
||||
/*
|
||||
The user has not yet been presented with the option to grant
|
||||
video access. We suspend the session queue to delay session
|
||||
setup until the access request has completed.
|
||||
|
||||
Note that audio access will be implicitly requested when we
|
||||
create an AVCaptureDeviceInput for audio during session setup.
|
||||
*/
|
||||
dispatch_suspend( self.sessionQueue );
|
||||
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted ) {
|
||||
if ( ! granted ) {
|
||||
self.setupResult = AVCamSetupResultCameraNotAuthorized;
|
||||
}
|
||||
dispatch_resume( self.sessionQueue );
|
||||
}];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// The user has previously denied access.
|
||||
self.setupResult = AVCamSetupResultCameraNotAuthorized;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Setup the capture session.
|
||||
In general it is not safe to mutate an AVCaptureSession or any of its
|
||||
inputs, outputs, or connections from multiple threads at the same time.
|
||||
|
||||
Why not do all of this on the main queue?
|
||||
Because -[AVCaptureSession startRunning] is a blocking call which can
|
||||
take a long time. We dispatch session setup to the sessionQueue so
|
||||
that the main queue isn't blocked, which keeps the UI responsive.
|
||||
*/
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
[self configureSession];
|
||||
} );
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
switch ( self.setupResult )
|
||||
{
|
||||
case AVCamSetupResultSuccess:
|
||||
{
|
||||
// Only setup observers and start the session running if setup succeeded.
|
||||
[self addObservers];
|
||||
[self.session startRunning];
|
||||
self.sessionRunning = self.session.isRunning;
|
||||
break;
|
||||
}
|
||||
case AVCamSetupResultCameraNotAuthorized:
|
||||
{
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
NSString *message = NSLocalizedString( @"AVCam doesn't have permission to use the camera, please change privacy settings", @"Alert message when the user has denied access to the camera" );
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"AVCam" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString( @"OK", @"Alert OK button" ) style:UIAlertActionStyleCancel handler:nil];
|
||||
[alertController addAction:cancelAction];
|
||||
// Provide quick access to Settings.
|
||||
UIAlertAction *settingsAction = [UIAlertAction actionWithTitle:NSLocalizedString( @"Settings", @"Alert button to open Settings" ) style:UIAlertActionStyleDefault handler:^( UIAlertAction *action ) {
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
|
||||
}];
|
||||
[alertController addAction:settingsAction];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
} );
|
||||
break;
|
||||
}
|
||||
case AVCamSetupResultSessionConfigurationFailed:
|
||||
{
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
NSString *message = NSLocalizedString( @"Unable to capture media", @"Alert message when something goes wrong during capture session configuration" );
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"AVCam" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString( @"OK", @"Alert OK button" ) style:UIAlertActionStyleCancel handler:nil];
|
||||
[alertController addAction:cancelAction];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
} );
|
||||
break;
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
if ( self.setupResult == AVCamSetupResultSuccess ) {
|
||||
[self.session stopRunning];
|
||||
[self removeObservers];
|
||||
}
|
||||
} );
|
||||
|
||||
[super viewDidDisappear:animated];
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotate
|
||||
{
|
||||
// Disable autorotation of the interface when recording is in progress.
|
||||
return ! self.movieFileOutput.isRecording;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
||||
{
|
||||
return UIInterfaceOrientationMaskAll;
|
||||
}
|
||||
|
||||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
|
||||
{
|
||||
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||||
|
||||
UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
|
||||
|
||||
if ( UIDeviceOrientationIsPortrait( deviceOrientation ) || UIDeviceOrientationIsLandscape( deviceOrientation ) ) {
|
||||
self.previewView.videoPreviewLayer.connection.videoOrientation = (AVCaptureVideoOrientation)deviceOrientation;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Session Management
|
||||
|
||||
// Call this on the session queue.
|
||||
- (void)configureSession
|
||||
{
|
||||
if ( self.setupResult != AVCamSetupResultSuccess ) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
|
||||
[self.session beginConfiguration];
|
||||
|
||||
/*
|
||||
We do not create an AVCaptureMovieFileOutput when setting up the session because the
|
||||
AVCaptureMovieFileOutput does not support movie recording with AVCaptureSessionPresetPhoto.
|
||||
*/
|
||||
self.session.sessionPreset = AVCaptureSessionPresetPhoto;
|
||||
|
||||
// Add video input.
|
||||
|
||||
// Choose the back dual camera if available, otherwise default to a wide angle camera.
|
||||
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInDuoCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack];
|
||||
if ( ! videoDevice ) {
|
||||
// If the back dual camera is not available, default to the back wide angle camera.
|
||||
videoDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack];
|
||||
|
||||
// In some cases where users break their phones, the back wide angle camera is not available. In this case, we should default to the front wide angle camera.
|
||||
if ( ! videoDevice ) {
|
||||
videoDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
|
||||
}
|
||||
}
|
||||
AVCaptureDeviceInput *videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
|
||||
if ( ! videoDeviceInput ) {
|
||||
NSLog( @"Could not create video device input: %@", error );
|
||||
self.setupResult = AVCamSetupResultSessionConfigurationFailed;
|
||||
[self.session commitConfiguration];
|
||||
return;
|
||||
}
|
||||
if ( [self.session canAddInput:videoDeviceInput] ) {
|
||||
[self.session addInput:videoDeviceInput];
|
||||
self.videoDeviceInput = videoDeviceInput;
|
||||
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
/*
|
||||
Why are we dispatching this to the main queue?
|
||||
Because AVCaptureVideoPreviewLayer is the backing layer for AVCamPreviewView and UIView
|
||||
can only be manipulated on the main thread.
|
||||
Note: As an exception to the above rule, it is not necessary to serialize video orientation changes
|
||||
on the AVCaptureVideoPreviewLayer’s connection with other session manipulation.
|
||||
|
||||
Use the status bar orientation as the initial video orientation. Subsequent orientation changes are
|
||||
handled by -[AVCamCameraViewController viewWillTransitionToSize:withTransitionCoordinator:].
|
||||
*/
|
||||
UIInterfaceOrientation statusBarOrientation = [UIApplication sharedApplication].statusBarOrientation;
|
||||
AVCaptureVideoOrientation initialVideoOrientation = AVCaptureVideoOrientationPortrait;
|
||||
if ( statusBarOrientation != UIInterfaceOrientationUnknown ) {
|
||||
initialVideoOrientation = (AVCaptureVideoOrientation)statusBarOrientation;
|
||||
}
|
||||
|
||||
self.previewView.videoPreviewLayer.connection.videoOrientation = initialVideoOrientation;
|
||||
} );
|
||||
}
|
||||
else {
|
||||
NSLog( @"Could not add video device input to the session" );
|
||||
self.setupResult = AVCamSetupResultSessionConfigurationFailed;
|
||||
[self.session commitConfiguration];
|
||||
return;
|
||||
}
|
||||
|
||||
// Add audio input.
|
||||
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
||||
AVCaptureDeviceInput *audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
|
||||
if ( ! audioDeviceInput ) {
|
||||
NSLog( @"Could not create audio device input: %@", error );
|
||||
}
|
||||
if ( [self.session canAddInput:audioDeviceInput] ) {
|
||||
[self.session addInput:audioDeviceInput];
|
||||
}
|
||||
else {
|
||||
NSLog( @"Could not add audio device input to the session" );
|
||||
}
|
||||
|
||||
// Add photo output.
|
||||
AVCapturePhotoOutput *photoOutput = [[AVCapturePhotoOutput alloc] init];
|
||||
if ( [self.session canAddOutput:photoOutput] ) {
|
||||
[self.session addOutput:photoOutput];
|
||||
self.photoOutput = photoOutput;
|
||||
|
||||
self.photoOutput.highResolutionCaptureEnabled = YES;
|
||||
self.photoOutput.livePhotoCaptureEnabled = self.photoOutput.livePhotoCaptureSupported;
|
||||
self.livePhotoMode = self.photoOutput.livePhotoCaptureSupported ? AVCamLivePhotoModeOn : AVCamLivePhotoModeOff;
|
||||
|
||||
self.inProgressPhotoCaptureDelegates = [NSMutableDictionary dictionary];
|
||||
self.inProgressLivePhotoCapturesCount = 0;
|
||||
}
|
||||
else {
|
||||
NSLog( @"Could not add photo output to the session" );
|
||||
self.setupResult = AVCamSetupResultSessionConfigurationFailed;
|
||||
[self.session commitConfiguration];
|
||||
return;
|
||||
}
|
||||
|
||||
self.backgroundRecordingID = UIBackgroundTaskInvalid;
|
||||
|
||||
[self.session commitConfiguration];
|
||||
}
|
||||
|
||||
- (IBAction)resumeInterruptedSession:(id)sender
|
||||
{
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
/*
|
||||
The session might fail to start running, e.g., if a phone or FaceTime call is still
|
||||
using audio or video. A failure to start the session running will be communicated via
|
||||
a session runtime error notification. To avoid repeatedly failing to start the session
|
||||
running, we only try to restart the session running in the session runtime error handler
|
||||
if we aren't trying to resume the session running.
|
||||
*/
|
||||
[self.session startRunning];
|
||||
self.sessionRunning = self.session.isRunning;
|
||||
if ( ! self.session.isRunning ) {
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
NSString *message = NSLocalizedString( @"Unable to resume", @"Alert message when unable to resume the session running" );
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"AVCam" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString( @"OK", @"Alert OK button" ) style:UIAlertActionStyleCancel handler:nil];
|
||||
[alertController addAction:cancelAction];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
} );
|
||||
}
|
||||
else {
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
self.resumeButton.hidden = YES;
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
- (IBAction)toggleCaptureMode:(UISegmentedControl *)captureModeControl
|
||||
{
|
||||
if ( captureModeControl.selectedSegmentIndex == AVCamCaptureModePhoto ) {
|
||||
self.recordButton.enabled = NO;
|
||||
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
/*
|
||||
Remove the AVCaptureMovieFileOutput from the session because movie recording is
|
||||
not supported with AVCaptureSessionPresetPhoto. Additionally, Live Photo
|
||||
capture is not supported when an AVCaptureMovieFileOutput is connected to the session.
|
||||
*/
|
||||
[self.session beginConfiguration];
|
||||
[self.session removeOutput:self.movieFileOutput];
|
||||
self.session.sessionPreset = AVCaptureSessionPresetPhoto;
|
||||
[self.session commitConfiguration];
|
||||
|
||||
self.movieFileOutput = nil;
|
||||
|
||||
if ( self.photoOutput.livePhotoCaptureSupported ) {
|
||||
self.photoOutput.livePhotoCaptureEnabled = YES;
|
||||
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
self.livePhotoModeButton.enabled = YES;
|
||||
self.livePhotoModeButton.hidden = NO;
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
else if ( captureModeControl.selectedSegmentIndex == AVCamCaptureModeMovie ) {
|
||||
self.livePhotoModeButton.hidden = YES;
|
||||
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
AVCaptureMovieFileOutput *movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
|
||||
|
||||
if ( [self.session canAddOutput:movieFileOutput] )
|
||||
{
|
||||
[self.session beginConfiguration];
|
||||
[self.session addOutput:movieFileOutput];
|
||||
self.session.sessionPreset = AVCaptureSessionPresetHigh;
|
||||
AVCaptureConnection *connection = [movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
|
||||
if ( connection.isVideoStabilizationSupported ) {
|
||||
connection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
|
||||
}
|
||||
[self.session commitConfiguration];
|
||||
|
||||
self.movieFileOutput = movieFileOutput;
|
||||
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
self.recordButton.enabled = YES;
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Device Configuration
|
||||
|
||||
- (IBAction)changeCamera:(id)sender
|
||||
{
|
||||
self.cameraButton.enabled = NO;
|
||||
self.recordButton.enabled = NO;
|
||||
self.photoButton.enabled = NO;
|
||||
self.livePhotoModeButton.enabled = NO;
|
||||
self.captureModeControl.enabled = NO;
|
||||
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
AVCaptureDevice *currentVideoDevice = self.videoDeviceInput.device;
|
||||
AVCaptureDevicePosition currentPosition = currentVideoDevice.position;
|
||||
|
||||
AVCaptureDevicePosition preferredPosition;
|
||||
AVCaptureDeviceType preferredDeviceType;
|
||||
|
||||
switch ( currentPosition )
|
||||
{
|
||||
case AVCaptureDevicePositionUnspecified:
|
||||
case AVCaptureDevicePositionFront:
|
||||
preferredPosition = AVCaptureDevicePositionBack;
|
||||
preferredDeviceType = AVCaptureDeviceTypeBuiltInDuoCamera;
|
||||
break;
|
||||
case AVCaptureDevicePositionBack:
|
||||
preferredPosition = AVCaptureDevicePositionFront;
|
||||
preferredDeviceType = AVCaptureDeviceTypeBuiltInWideAngleCamera;
|
||||
break;
|
||||
}
|
||||
|
||||
NSArray<AVCaptureDevice *> *devices = self.videoDeviceDiscoverySession.devices;
|
||||
AVCaptureDevice *newVideoDevice = nil;
|
||||
|
||||
// First, look for a device with both the preferred position and device type.
|
||||
for ( AVCaptureDevice *device in devices ) {
|
||||
if ( device.position == preferredPosition && [device.deviceType isEqualToString:preferredDeviceType] ) {
|
||||
newVideoDevice = device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, look for a device with only the preferred position.
|
||||
if ( ! newVideoDevice ) {
|
||||
for ( AVCaptureDevice *device in devices ) {
|
||||
if ( device.position == preferredPosition ) {
|
||||
newVideoDevice = device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( newVideoDevice ) {
|
||||
AVCaptureDeviceInput *videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:newVideoDevice error:NULL];
|
||||
|
||||
[self.session beginConfiguration];
|
||||
|
||||
// Remove the existing device input first, since using the front and back camera simultaneously is not supported.
|
||||
[self.session removeInput:self.videoDeviceInput];
|
||||
|
||||
if ( [self.session canAddInput:videoDeviceInput] ) {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:currentVideoDevice];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(subjectAreaDidChange:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:newVideoDevice];
|
||||
|
||||
[self.session addInput:videoDeviceInput];
|
||||
self.videoDeviceInput = videoDeviceInput;
|
||||
}
|
||||
else {
|
||||
[self.session addInput:self.videoDeviceInput];
|
||||
}
|
||||
|
||||
AVCaptureConnection *movieFileOutputConnection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
|
||||
if ( movieFileOutputConnection.isVideoStabilizationSupported ) {
|
||||
movieFileOutputConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
|
||||
}
|
||||
|
||||
/*
|
||||
Set Live Photo capture enabled if it is supported. When changing cameras, the
|
||||
`livePhotoCaptureEnabled` property of the AVCapturePhotoOutput gets set to NO when
|
||||
a video device is disconnected from the session. After the new video device is
|
||||
added to the session, re-enable Live Photo capture on the AVCapturePhotoOutput if it is supported.
|
||||
*/
|
||||
self.photoOutput.livePhotoCaptureEnabled = self.photoOutput.livePhotoCaptureSupported;
|
||||
|
||||
[self.session commitConfiguration];
|
||||
}
|
||||
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
self.cameraButton.enabled = YES;
|
||||
self.recordButton.enabled = self.captureModeControl.selectedSegmentIndex == AVCamCaptureModeMovie;
|
||||
self.photoButton.enabled = YES;
|
||||
self.livePhotoModeButton.enabled = YES;
|
||||
self.captureModeControl.enabled = YES;
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
- (IBAction)focusAndExposeTap:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
CGPoint devicePoint = [self.previewView.videoPreviewLayer captureDevicePointOfInterestForPoint:[gestureRecognizer locationInView:gestureRecognizer.view]];
|
||||
[self focusWithMode:AVCaptureFocusModeAutoFocus exposeWithMode:AVCaptureExposureModeAutoExpose atDevicePoint:devicePoint monitorSubjectAreaChange:YES];
|
||||
}
|
||||
|
||||
- (void)focusWithMode:(AVCaptureFocusMode)focusMode exposeWithMode:(AVCaptureExposureMode)exposureMode atDevicePoint:(CGPoint)point monitorSubjectAreaChange:(BOOL)monitorSubjectAreaChange
|
||||
{
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
AVCaptureDevice *device = self.videoDeviceInput.device;
|
||||
NSError *error = nil;
|
||||
if ( [device lockForConfiguration:&error] ) {
|
||||
/*
|
||||
Setting (focus/exposure)PointOfInterest alone does not initiate a (focus/exposure) operation.
|
||||
Call set(Focus/Exposure)Mode() to apply the new point of interest.
|
||||
*/
|
||||
if ( device.isFocusPointOfInterestSupported && [device isFocusModeSupported:focusMode] ) {
|
||||
device.focusPointOfInterest = point;
|
||||
device.focusMode = focusMode;
|
||||
}
|
||||
|
||||
if ( device.isExposurePointOfInterestSupported && [device isExposureModeSupported:exposureMode] ) {
|
||||
device.exposurePointOfInterest = point;
|
||||
device.exposureMode = exposureMode;
|
||||
}
|
||||
|
||||
device.subjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange;
|
||||
[device unlockForConfiguration];
|
||||
}
|
||||
else {
|
||||
NSLog( @"Could not lock device for configuration: %@", error );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
#pragma mark Capturing Photos
|
||||
|
||||
- (IBAction)capturePhoto:(id)sender
|
||||
{
|
||||
/*
|
||||
Retrieve the video preview layer's video orientation on the main queue before
|
||||
entering the session queue. We do this to ensure UI elements are accessed on
|
||||
the main thread and session configuration is done on the session queue.
|
||||
*/
|
||||
AVCaptureVideoOrientation videoPreviewLayerVideoOrientation = self.previewView.videoPreviewLayer.connection.videoOrientation;
|
||||
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
|
||||
// Update the photo output's connection to match the video orientation of the video preview layer.
|
||||
AVCaptureConnection *photoOutputConnection = [self.photoOutput connectionWithMediaType:AVMediaTypeVideo];
|
||||
photoOutputConnection.videoOrientation = videoPreviewLayerVideoOrientation;
|
||||
|
||||
// Capture a JPEG photo with flash set to auto and high resolution photo enabled.
|
||||
AVCapturePhotoSettings *photoSettings = [AVCapturePhotoSettings photoSettings];
|
||||
photoSettings.flashMode = AVCaptureFlashModeAuto;
|
||||
photoSettings.highResolutionPhotoEnabled = YES;
|
||||
if ( photoSettings.availablePreviewPhotoPixelFormatTypes.count > 0 ) {
|
||||
photoSettings.previewPhotoFormat = @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : photoSettings.availablePreviewPhotoPixelFormatTypes.firstObject };
|
||||
}
|
||||
if ( self.livePhotoMode == AVCamLivePhotoModeOn && self.photoOutput.livePhotoCaptureSupported ) { // Live Photo capture is not supported in movie mode.
|
||||
NSString *livePhotoMovieFileName = [NSUUID UUID].UUIDString;
|
||||
NSString *livePhotoMovieFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[livePhotoMovieFileName stringByAppendingPathExtension:@"mov"]];
|
||||
photoSettings.livePhotoMovieFileURL = [NSURL fileURLWithPath:livePhotoMovieFilePath];
|
||||
}
|
||||
|
||||
// Use a separate object for the photo capture delegate to isolate each capture life cycle.
|
||||
AVCamPhotoCaptureDelegate *photoCaptureDelegate = [[AVCamPhotoCaptureDelegate alloc] initWithRequestedPhotoSettings:photoSettings willCapturePhotoAnimation:^{
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
self.previewView.videoPreviewLayer.opacity = 0.0;
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
self.previewView.videoPreviewLayer.opacity = 1.0;
|
||||
}];
|
||||
} );
|
||||
} capturingLivePhoto:^( BOOL capturing ) {
|
||||
/*
|
||||
Because Live Photo captures can overlap, we need to keep track of the
|
||||
number of in progress Live Photo captures to ensure that the
|
||||
Live Photo label stays visible during these captures.
|
||||
*/
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
if ( capturing ) {
|
||||
self.inProgressLivePhotoCapturesCount++;
|
||||
}
|
||||
else {
|
||||
self.inProgressLivePhotoCapturesCount--;
|
||||
}
|
||||
|
||||
NSInteger inProgressLivePhotoCapturesCount = self.inProgressLivePhotoCapturesCount;
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
if ( inProgressLivePhotoCapturesCount > 0 ) {
|
||||
self.capturingLivePhotoLabel.hidden = NO;
|
||||
}
|
||||
else if ( inProgressLivePhotoCapturesCount == 0 ) {
|
||||
self.capturingLivePhotoLabel.hidden = YES;
|
||||
}
|
||||
else {
|
||||
NSLog( @"Error: In progress live photo capture count is less than 0" );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
} completed:^( AVCamPhotoCaptureDelegate *photoCaptureDelegate ) {
|
||||
// When the capture is complete, remove a reference to the photo capture delegate so it can be deallocated.
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
self.inProgressPhotoCaptureDelegates[@(photoCaptureDelegate.requestedPhotoSettings.uniqueID)] = nil;
|
||||
} );
|
||||
}];
|
||||
|
||||
/*
|
||||
The Photo Output keeps a weak reference to the photo capture delegate so
|
||||
we store it in an array to maintain a strong reference to this object
|
||||
until the capture is completed.
|
||||
*/
|
||||
self.inProgressPhotoCaptureDelegates[@(photoCaptureDelegate.requestedPhotoSettings.uniqueID)] = photoCaptureDelegate;
|
||||
[self.photoOutput capturePhotoWithSettings:photoSettings delegate:photoCaptureDelegate];
|
||||
} );
|
||||
}
|
||||
|
||||
- (IBAction)toggleLivePhotoMode:(UIButton *)livePhotoModeButton
|
||||
{
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
self.livePhotoMode = ( self.livePhotoMode == AVCamLivePhotoModeOn ) ? AVCamLivePhotoModeOff : AVCamLivePhotoModeOn;
|
||||
AVCamLivePhotoMode livePhotoMode = self.livePhotoMode;
|
||||
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
if ( livePhotoMode == AVCamLivePhotoModeOn ) {
|
||||
[self.livePhotoModeButton setTitle:NSLocalizedString( @"Live Photo Mode: On", @"Live photo mode button on title" ) forState:UIControlStateNormal];
|
||||
}
|
||||
else {
|
||||
[self.livePhotoModeButton setTitle:NSLocalizedString( @"Live Photo Mode: Off", @"Live photo mode button off title" ) forState:UIControlStateNormal];
|
||||
}
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
#pragma mark Recording Movies
|
||||
|
||||
- (IBAction)toggleMovieRecording:(id)sender
|
||||
{
|
||||
/*
|
||||
Disable the Camera button until recording finishes, and disable
|
||||
the Record button until recording starts or finishes.
|
||||
|
||||
See the AVCaptureFileOutputRecordingDelegate methods.
|
||||
*/
|
||||
self.cameraButton.enabled = NO;
|
||||
self.recordButton.enabled = NO;
|
||||
self.captureModeControl.enabled = NO;
|
||||
|
||||
/*
|
||||
Retrieve the video preview layer's video orientation on the main queue
|
||||
before entering the session queue. We do this to ensure UI elements are
|
||||
accessed on the main thread and session configuration is done on the session queue.
|
||||
*/
|
||||
AVCaptureVideoOrientation videoPreviewLayerVideoOrientation = self.previewView.videoPreviewLayer.connection.videoOrientation;
|
||||
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
if ( ! self.movieFileOutput.isRecording ) {
|
||||
if ( [UIDevice currentDevice].isMultitaskingSupported ) {
|
||||
/*
|
||||
Setup background task.
|
||||
This is needed because the -[captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error:]
|
||||
callback is not received until AVCam returns to the foreground unless you request background execution time.
|
||||
This also ensures that there will be time to write the file to the photo library when AVCam is backgrounded.
|
||||
To conclude this background execution, -[endBackgroundTask:] is called in
|
||||
-[captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error:] after the recorded file has been saved.
|
||||
*/
|
||||
self.backgroundRecordingID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
|
||||
}
|
||||
|
||||
// Update the orientation on the movie file output video connection before starting recording.
|
||||
AVCaptureConnection *movieFileOutputConnection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
|
||||
movieFileOutputConnection.videoOrientation = videoPreviewLayerVideoOrientation;
|
||||
|
||||
// Start recording to a temporary file.
|
||||
NSString *outputFileName = [NSUUID UUID].UUIDString;
|
||||
NSString *outputFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[outputFileName stringByAppendingPathExtension:@"mov"]];
|
||||
[self.movieFileOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:outputFilePath] recordingDelegate:self];
|
||||
}
|
||||
else {
|
||||
[self.movieFileOutput stopRecording];
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections
|
||||
{
|
||||
// Enable the Record button to let the user stop the recording.
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
self.recordButton.enabled = YES;
|
||||
[self.recordButton setTitle:NSLocalizedString( @"Stop", @"Recording button stop title" ) forState:UIControlStateNormal];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error
|
||||
{
|
||||
/*
|
||||
Note that currentBackgroundRecordingID is used to end the background task
|
||||
associated with this recording. This allows a new recording to be started,
|
||||
associated with a new UIBackgroundTaskIdentifier, once the movie file output's
|
||||
`recording` property is back to NO — which happens sometime after this method
|
||||
returns.
|
||||
|
||||
Note: Since we use a unique file path for each recording, a new recording will
|
||||
not overwrite a recording currently being saved.
|
||||
*/
|
||||
UIBackgroundTaskIdentifier currentBackgroundRecordingID = self.backgroundRecordingID;
|
||||
self.backgroundRecordingID = UIBackgroundTaskInvalid;
|
||||
|
||||
dispatch_block_t cleanup = ^{
|
||||
if ( [[NSFileManager defaultManager] fileExistsAtPath:outputFileURL.path] ) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:outputFileURL.path error:NULL];
|
||||
}
|
||||
|
||||
if ( currentBackgroundRecordingID != UIBackgroundTaskInvalid ) {
|
||||
[[UIApplication sharedApplication] endBackgroundTask:currentBackgroundRecordingID];
|
||||
}
|
||||
};
|
||||
|
||||
BOOL success = YES;
|
||||
|
||||
if ( error ) {
|
||||
NSLog( @"Movie file finishing error: %@", error );
|
||||
success = [error.userInfo[AVErrorRecordingSuccessfullyFinishedKey] boolValue];
|
||||
}
|
||||
if ( success ) {
|
||||
// Check authorization status.
|
||||
[PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status ) {
|
||||
if ( status == PHAuthorizationStatusAuthorized ) {
|
||||
// Save the movie file to the photo library and cleanup.
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||
PHAssetResourceCreationOptions *options = [[PHAssetResourceCreationOptions alloc] init];
|
||||
options.shouldMoveFile = YES;
|
||||
PHAssetCreationRequest *creationRequest = [PHAssetCreationRequest creationRequestForAsset];
|
||||
[creationRequest addResourceWithType:PHAssetResourceTypeVideo fileURL:outputFileURL options:options];
|
||||
} completionHandler:^( BOOL success, NSError *error ) {
|
||||
if ( ! success ) {
|
||||
NSLog( @"Could not save movie to photo library: %@", error );
|
||||
}
|
||||
cleanup();
|
||||
}];
|
||||
}
|
||||
else {
|
||||
cleanup();
|
||||
}
|
||||
}];
|
||||
}
|
||||
else {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
// Enable the Camera and Record buttons to let the user switch camera and start another recording.
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
// Only enable the ability to change camera if the device has more than one camera.
|
||||
self.cameraButton.enabled = ( self.videoDeviceDiscoverySession.uniqueDevicePositionsCount > 1 );
|
||||
self.recordButton.enabled = YES;
|
||||
self.captureModeControl.enabled = YES;
|
||||
[self.recordButton setTitle:NSLocalizedString( @"Record", @"Recording button record title" ) forState:UIControlStateNormal];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark KVO and Notifications
|
||||
|
||||
- (void)addObservers
|
||||
{
|
||||
[self.session addObserver:self forKeyPath:@"running" options:NSKeyValueObservingOptionNew context:SessionRunningContext];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(subjectAreaDidChange:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:self.videoDeviceInput.device];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:self.session];
|
||||
|
||||
/*
|
||||
A session can only run when the app is full screen. It will be interrupted
|
||||
in a multi-app layout, introduced in iOS 9, see also the documentation of
|
||||
AVCaptureSessionInterruptionReason. Add observers to handle these session
|
||||
interruptions and show a preview is paused message. See the documentation
|
||||
of AVCaptureSessionWasInterruptedNotification for other interruption reasons.
|
||||
*/
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionWasInterrupted:) name:AVCaptureSessionWasInterruptedNotification object:self.session];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionInterruptionEnded:) name:AVCaptureSessionInterruptionEndedNotification object:self.session];
|
||||
}
|
||||
|
||||
- (void)removeObservers
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
|
||||
[self.session removeObserver:self forKeyPath:@"running" context:SessionRunningContext];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
||||
{
|
||||
if ( context == SessionRunningContext ) {
|
||||
BOOL isSessionRunning = [change[NSKeyValueChangeNewKey] boolValue];
|
||||
BOOL livePhotoCaptureSupported = self.photoOutput.livePhotoCaptureSupported;
|
||||
BOOL livePhotoCaptureEnabled = self.photoOutput.livePhotoCaptureEnabled;
|
||||
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
// Only enable the ability to change camera if the device has more than one camera.
|
||||
self.cameraButton.enabled = isSessionRunning && ( self.videoDeviceDiscoverySession.uniqueDevicePositionsCount > 1 );
|
||||
self.recordButton.enabled = isSessionRunning && ( self.captureModeControl.selectedSegmentIndex == AVCamCaptureModeMovie );
|
||||
self.photoButton.enabled = isSessionRunning;
|
||||
self.captureModeControl.enabled = isSessionRunning;
|
||||
self.livePhotoModeButton.enabled = isSessionRunning && livePhotoCaptureEnabled;
|
||||
self.livePhotoModeButton.hidden = ! ( isSessionRunning && livePhotoCaptureSupported );
|
||||
} );
|
||||
}
|
||||
else {
|
||||
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)subjectAreaDidChange:(NSNotification *)notification
|
||||
{
|
||||
CGPoint devicePoint = CGPointMake( 0.5, 0.5 );
|
||||
[self focusWithMode:AVCaptureFocusModeContinuousAutoFocus exposeWithMode:AVCaptureExposureModeContinuousAutoExposure atDevicePoint:devicePoint monitorSubjectAreaChange:NO];
|
||||
}
|
||||
|
||||
- (void)sessionRuntimeError:(NSNotification *)notification
|
||||
{
|
||||
NSError *error = notification.userInfo[AVCaptureSessionErrorKey];
|
||||
NSLog( @"Capture session runtime error: %@", error );
|
||||
|
||||
/*
|
||||
Automatically try to restart the session running if media services were
|
||||
reset and the last start running succeeded. Otherwise, enable the user
|
||||
to try to resume the session running.
|
||||
*/
|
||||
if ( error.code == AVErrorMediaServicesWereReset ) {
|
||||
dispatch_async( self.sessionQueue, ^{
|
||||
if ( self.isSessionRunning ) {
|
||||
[self.session startRunning];
|
||||
self.sessionRunning = self.session.isRunning;
|
||||
}
|
||||
else {
|
||||
dispatch_async( dispatch_get_main_queue(), ^{
|
||||
self.resumeButton.hidden = NO;
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
else {
|
||||
self.resumeButton.hidden = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)sessionWasInterrupted:(NSNotification *)notification
|
||||
{
|
||||
/*
|
||||
In some scenarios we want to enable the user to resume the session running.
|
||||
For example, if music playback is initiated via control center while
|
||||
using AVCam, then the user can let AVCam resume
|
||||
the session running, which will stop music playback. Note that stopping
|
||||
music playback in control center will not automatically resume the session
|
||||
running. Also note that it is not always possible to resume, see -[resumeInterruptedSession:].
|
||||
*/
|
||||
BOOL showResumeButton = NO;
|
||||
|
||||
AVCaptureSessionInterruptionReason reason = [notification.userInfo[AVCaptureSessionInterruptionReasonKey] integerValue];
|
||||
NSLog( @"Capture session was interrupted with reason %ld", (long)reason );
|
||||
|
||||
if ( reason == AVCaptureSessionInterruptionReasonAudioDeviceInUseByAnotherClient ||
|
||||
reason == AVCaptureSessionInterruptionReasonVideoDeviceInUseByAnotherClient ) {
|
||||
showResumeButton = YES;
|
||||
}
|
||||
else if ( reason == AVCaptureSessionInterruptionReasonVideoDeviceNotAvailableWithMultipleForegroundApps ) {
|
||||
// Simply fade-in a label to inform the user that the camera is unavailable.
|
||||
self.cameraUnavailableLabel.alpha = 0.0;
|
||||
self.cameraUnavailableLabel.hidden = NO;
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
self.cameraUnavailableLabel.alpha = 1.0;
|
||||
}];
|
||||
}
|
||||
|
||||
if ( showResumeButton ) {
|
||||
// Simply fade-in a button to enable the user to try to resume the session running.
|
||||
self.resumeButton.alpha = 0.0;
|
||||
self.resumeButton.hidden = NO;
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
self.resumeButton.alpha = 1.0;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)sessionInterruptionEnded:(NSNotification *)notification
|
||||
{
|
||||
NSLog( @"Capture session interruption ended" );
|
||||
|
||||
if ( ! self.resumeButton.hidden ) {
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
self.resumeButton.alpha = 0.0;
|
||||
} completion:^( BOOL finished ) {
|
||||
self.resumeButton.hidden = YES;
|
||||
}];
|
||||
}
|
||||
if ( ! self.cameraUnavailableLabel.hidden ) {
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
self.cameraUnavailableLabel.alpha = 0.0;
|
||||
} completion:^( BOOL finished ) {
|
||||
self.cameraUnavailableLabel.hidden = YES;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Photo capture delegate.
|
||||
*/
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
@interface AVCamPhotoCaptureDelegate : NSObject<AVCapturePhotoCaptureDelegate>
|
||||
|
||||
- (instancetype)initWithRequestedPhotoSettings:(AVCapturePhotoSettings *)requestedPhotoSettings willCapturePhotoAnimation:(void (^)())willCapturePhotoAnimation capturingLivePhoto:(void (^)( BOOL capturing ))capturingLivePhoto completed:(void (^)( AVCamPhotoCaptureDelegate *photoCaptureDelegate ))completed;
|
||||
|
||||
@property (nonatomic, readonly) AVCapturePhotoSettings *requestedPhotoSettings;
|
||||
|
||||
@end
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Photo capture delegate.
|
||||
*/
|
||||
|
||||
#import "AVCamPhotoCaptureDelegate.h"
|
||||
|
||||
@import Photos;
|
||||
|
||||
@interface AVCamPhotoCaptureDelegate ()
|
||||
|
||||
@property (nonatomic, readwrite) AVCapturePhotoSettings *requestedPhotoSettings;
|
||||
@property (nonatomic) void (^willCapturePhotoAnimation)();
|
||||
@property (nonatomic) void (^capturingLivePhoto)(BOOL capturing);
|
||||
@property (nonatomic) void (^completed)(AVCamPhotoCaptureDelegate *photoCaptureDelegate);
|
||||
|
||||
@property (nonatomic) NSData *photoData;
|
||||
@property (nonatomic) NSURL *livePhotoCompanionMovieURL;
|
||||
|
||||
@end
|
||||
|
||||
@implementation AVCamPhotoCaptureDelegate
|
||||
|
||||
- (instancetype)initWithRequestedPhotoSettings:(AVCapturePhotoSettings *)requestedPhotoSettings willCapturePhotoAnimation:(void (^)())willCapturePhotoAnimation capturingLivePhoto:(void (^)(BOOL))capturingLivePhoto completed:(void (^)(AVCamPhotoCaptureDelegate *))completed
|
||||
{
|
||||
self = [super init];
|
||||
if ( self ) {
|
||||
self.requestedPhotoSettings = requestedPhotoSettings;
|
||||
self.willCapturePhotoAnimation = willCapturePhotoAnimation;
|
||||
self.capturingLivePhoto = capturingLivePhoto;
|
||||
self.completed = completed;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)didFinish
|
||||
{
|
||||
if ( [[NSFileManager defaultManager] fileExistsAtPath:self.livePhotoCompanionMovieURL.path] ) {
|
||||
NSError *error = nil;
|
||||
[[NSFileManager defaultManager] removeItemAtPath:self.livePhotoCompanionMovieURL.path error:&error];
|
||||
|
||||
if ( error ) {
|
||||
NSLog( @"Could not remove file at url: %@", self.livePhotoCompanionMovieURL.path );
|
||||
}
|
||||
}
|
||||
|
||||
self.completed( self );
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput willBeginCaptureForResolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings
|
||||
{
|
||||
if ( ( resolvedSettings.livePhotoMovieDimensions.width > 0 ) && ( resolvedSettings.livePhotoMovieDimensions.height > 0 ) ) {
|
||||
self.capturingLivePhoto( YES );
|
||||
}
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput willCapturePhotoForResolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings
|
||||
{
|
||||
self.willCapturePhotoAnimation();
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings error:(NSError *)error
|
||||
{
|
||||
if ( error != nil ) {
|
||||
NSLog( @"Error capturing photo: %@", error );
|
||||
return;
|
||||
}
|
||||
|
||||
self.photoData = [AVCapturePhotoOutput JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer previewPhotoSampleBuffer:previewPhotoSampleBuffer];
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishRecordingLivePhotoMovieForEventualFileAtURL:(NSURL *)outputFileURL resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings
|
||||
{
|
||||
self.capturingLivePhoto(NO);
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishProcessingLivePhotoToMovieFileAtURL:(NSURL *)outputFileURL duration:(CMTime)duration photoDisplayTime:(CMTime)photoDisplayTime resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings error:(NSError *)error
|
||||
{
|
||||
if ( error != nil ) {
|
||||
NSLog( @"Error processing live photo companion movie: %@", error );
|
||||
return;
|
||||
}
|
||||
|
||||
self.livePhotoCompanionMovieURL = outputFileURL;
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishCaptureForResolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings error:(NSError *)error
|
||||
{
|
||||
if ( error != nil ) {
|
||||
NSLog( @"Error capturing photo: %@", error );
|
||||
[self didFinish];
|
||||
return;
|
||||
}
|
||||
|
||||
if ( self.photoData == nil ) {
|
||||
NSLog( @"No photo data resource" );
|
||||
[self didFinish];
|
||||
return;
|
||||
}
|
||||
|
||||
[PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status ) {
|
||||
if ( status == PHAuthorizationStatusAuthorized ) {
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||
PHAssetCreationRequest *creationRequest = [PHAssetCreationRequest creationRequestForAsset];
|
||||
[creationRequest addResourceWithType:PHAssetResourceTypePhoto data:self.photoData options:nil];
|
||||
|
||||
if ( self.livePhotoCompanionMovieURL ) {
|
||||
PHAssetResourceCreationOptions *livePhotoCompanionMovieResourceOptions = [[PHAssetResourceCreationOptions alloc] init];
|
||||
livePhotoCompanionMovieResourceOptions.shouldMoveFile = YES;
|
||||
[creationRequest addResourceWithType:PHAssetResourceTypePairedVideo fileURL:self.livePhotoCompanionMovieURL options:livePhotoCompanionMovieResourceOptions];
|
||||
}
|
||||
} completionHandler:^( BOOL success, NSError * _Nullable error ) {
|
||||
if ( ! success ) {
|
||||
NSLog( @"Error occurred while saving photo to photo library: %@", error );
|
||||
}
|
||||
|
||||
[self didFinish];
|
||||
}];
|
||||
}
|
||||
else {
|
||||
NSLog( @"Not authorized to save photo" );
|
||||
[self didFinish];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Application preview view.
|
||||
*/
|
||||
|
||||
@import UIKit;
|
||||
|
||||
@class AVCaptureSession;
|
||||
|
||||
@interface AVCamPreviewView : UIView
|
||||
|
||||
@property (nonatomic, readonly) AVCaptureVideoPreviewLayer *videoPreviewLayer;
|
||||
|
||||
@property (nonatomic) AVCaptureSession *session;
|
||||
|
||||
@end
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Application preview view.
|
||||
*/
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
#import "AVCamPreviewView.h"
|
||||
|
||||
@implementation AVCamPreviewView
|
||||
|
||||
+ (Class)layerClass
|
||||
{
|
||||
return [AVCaptureVideoPreviewLayer class];
|
||||
}
|
||||
|
||||
- (AVCaptureVideoPreviewLayer *)videoPreviewLayer
|
||||
{
|
||||
return (AVCaptureVideoPreviewLayer *)self.layer;
|
||||
}
|
||||
|
||||
- (AVCaptureSession *)session
|
||||
{
|
||||
return self.videoPreviewLayer.session;
|
||||
}
|
||||
|
||||
- (void)setSession:(AVCaptureSession *)session
|
||||
{
|
||||
self.videoPreviewLayer.session = session;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,128 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-Small.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-Small@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-Small@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "57x57",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "57x57",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-40.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "50x50",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small-50.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "50x50",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small-50@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "72x72",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-72.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "72x72",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-72@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11129.12" systemVersion="15F24" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11103.9"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<color key="tintColor" red="1" green="1" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</document>
|
|
@ -0,0 +1,193 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11129.12" systemVersion="15F24" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11103.9"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Cam Camera View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="AVCamCameraViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3eR-Rn-XpZ" userLabel="Preview" customClass="AVCamPreviewView">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<gestureRecognizers/>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="fY6-qX-ntV" appends="YES" id="G6D-dx-xU8"/>
|
||||
</connections>
|
||||
</view>
|
||||
<label hidden="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Camera Unavailable" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zf0-db-esM" userLabel="Camera Unavailable">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="24"/>
|
||||
<color key="textColor" red="1" green="1" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button hidden="YES" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FZr-Ip-7WL" userLabel="Resume">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="24"/>
|
||||
<inset key="contentEdgeInsets" minX="10" minY="5" maxX="10" maxY="5"/>
|
||||
<state key="normal" title="Tap to resume">
|
||||
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="resumeInterruptedSession:" destination="BYZ-38-t0r" eventType="touchUpInside" id="42K-1B-qJd"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eRT-dK-6dM" userLabel="Record">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<state key="normal" title="Record">
|
||||
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="toggleMovieRecording:" destination="BYZ-38-t0r" eventType="touchUpInside" id="9R7-Ok-FpB"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uCj-6P-mHF" userLabel="Still">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="NtC-UN-gTs"/>
|
||||
<constraint firstAttribute="width" constant="80" id="dxU-UP-4Ae"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<state key="normal" title="Photo">
|
||||
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="capturePhoto:" destination="BYZ-38-t0r" eventType="touchUpInside" id="o5K-SC-fYn"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rUJ-G6-RPv" userLabel="Camera">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<state key="normal" title="Camera">
|
||||
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="changeCamera:" destination="BYZ-38-t0r" eventType="touchUpInside" id="3W0-h3-6fc"/>
|
||||
</connections>
|
||||
</button>
|
||||
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="FAC-co-10c">
|
||||
<segments>
|
||||
<segment title="Photo"/>
|
||||
<segment title="Movie"/>
|
||||
</segments>
|
||||
<connections>
|
||||
<action selector="toggleCaptureMode:" destination="BYZ-38-t0r" eventType="valueChanged" id="SKd-67-ZHh"/>
|
||||
</connections>
|
||||
</segmentedControl>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eI6-gV-W7d">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="200" id="heR-zX-F6K"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<state key="normal" title="Live Photo Mode: On"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="toggleLivePhotoMode:" destination="BYZ-38-t0r" eventType="touchUpInside" id="JqX-wJ-Xf1"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label hidden="YES" opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Live" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pii-2r-R2l">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="25" id="Kxo-zf-Fe1"/>
|
||||
<constraint firstAttribute="width" constant="40" id="eRd-mj-8Du"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="1" green="1" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="125-kC-WZF"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="height" secondItem="eRT-dK-6dM" secondAttribute="height" id="AEV-ew-H4g"/>
|
||||
<constraint firstItem="eI6-gV-W7d" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="8" id="Aao-6b-vLN"/>
|
||||
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="height" secondItem="8bC-Xf-vdC" secondAttribute="height" id="Ice-47-M9N"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="top" secondItem="rUJ-G6-RPv" secondAttribute="top" id="NFm-e8-abT"/>
|
||||
<constraint firstItem="FZr-Ip-7WL" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="OaZ-uO-vXY"/>
|
||||
<constraint firstItem="FAC-co-10c" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="Oow-A6-mDp"/>
|
||||
<constraint firstItem="zf0-db-esM" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="Ris-mI-8lA"/>
|
||||
<constraint firstItem="Pii-2r-R2l" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="Upd-h8-1dL"/>
|
||||
<constraint firstItem="zf0-db-esM" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="W6q-xJ-jfF"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="height" secondItem="rUJ-G6-RPv" secondAttribute="height" id="aQi-F7-E2b"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="top" secondItem="FAC-co-10c" secondAttribute="bottom" constant="20" id="aSR-Je-0lW"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="top" secondItem="eRT-dK-6dM" secondAttribute="top" id="bQd-ro-0Hw"/>
|
||||
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="uCj-6P-mHF" secondAttribute="bottom" constant="20" id="eWs-co-Aaz"/>
|
||||
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="igk-MQ-CGt"/>
|
||||
<constraint firstItem="rUJ-G6-RPv" firstAttribute="leading" secondItem="uCj-6P-mHF" secondAttribute="trailing" constant="20" id="lsk-Hm-rTd"/>
|
||||
<constraint firstAttribute="centerX" secondItem="uCj-6P-mHF" secondAttribute="centerX" id="m8a-cF-Rf0"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="width" secondItem="rUJ-G6-RPv" secondAttribute="width" id="o8j-gw-35B"/>
|
||||
<constraint firstItem="Pii-2r-R2l" firstAttribute="top" secondItem="eI6-gV-W7d" secondAttribute="bottom" constant="8" id="oDE-jY-ryC"/>
|
||||
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="width" secondItem="8bC-Xf-vdC" secondAttribute="width" id="pSC-xP-dl0"/>
|
||||
<constraint firstItem="eI6-gV-W7d" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="rqt-bn-mSt"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="width" secondItem="eRT-dK-6dM" secondAttribute="width" id="s8u-Y8-n27"/>
|
||||
<constraint firstItem="FZr-Ip-7WL" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="sTY-i6-czN"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="leading" secondItem="eRT-dK-6dM" secondAttribute="trailing" constant="20" id="zwj-TX-t6O"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<extendedEdge key="edgesForExtendedLayout"/>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="cameraButton" destination="rUJ-G6-RPv" id="dAV-WS-N1p"/>
|
||||
<outlet property="cameraUnavailableLabel" destination="zf0-db-esM" id="P9W-lb-Pb8"/>
|
||||
<outlet property="captureModeControl" destination="FAC-co-10c" id="KXj-wg-BvS"/>
|
||||
<outlet property="capturingLivePhotoLabel" destination="Pii-2r-R2l" id="JAa-4l-5SD"/>
|
||||
<outlet property="livePhotoModeButton" destination="eI6-gV-W7d" id="r9f-cN-YSH"/>
|
||||
<outlet property="photoButton" destination="uCj-6P-mHF" id="Ha8-ua-hxy"/>
|
||||
<outlet property="previewView" destination="3eR-Rn-XpZ" id="e7I-nu-L6j"/>
|
||||
<outlet property="recordButton" destination="eRT-dK-6dM" id="iqk-en-NsW"/>
|
||||
<outlet property="resumeButton" destination="FZr-Ip-7WL" id="tX5-Sx-rQK"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
<tapGestureRecognizer id="fY6-qX-ntV">
|
||||
<connections>
|
||||
<action selector="focusAndExposeTap:" destination="BYZ-38-t0r" id="65g-8k-5pv"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-656" y="-630"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<color key="tintColor" red="1" green="1" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</document>
|
|
@ -0,0 +1,60 @@
|
|||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>to take photos and videos</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>to record Live Photos and movies</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>to save photos and videos to your Photo Library</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Main application entry point.
|
||||
*/
|
||||
|
||||
@import UIKit;
|
||||
|
||||
#import "AVCamAppDelegate.h"
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
@autoreleasepool {
|
||||
return UIApplicationMain( argc, argv, nil, NSStringFromClass( [AVCamAppDelegate class] ) );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
# AVCam-iOS: Using AVFoundation to Capture Photos and Movies
|
||||
|
||||
AVCam demonstrates how to use the AVFoundation capture API to record movies and capture photos. The sample has a record button for recording movies, a photo button for capturing photos, a Live Photo mode button for enabling Live Photo capture, a capture mode control for toggling between photo and movie capture modes, and a camera button for switching between front and back cameras (on supported devices). AVCam runs only on an actual device, either an iPad or iPhone, and cannot be run in Simulator.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Build
|
||||
|
||||
Xcode 8.0, iOS 10.0 SDK
|
||||
|
||||
### Runtime
|
||||
|
||||
iOS 10.0 or later
|
||||
|
||||
## Changes from Previous Version
|
||||
|
||||
- Adopt AVCapturePhotoOutput
|
||||
- Capture Live Photos
|
||||
- Add privacy keys to Info.plist
|
||||
- Add a version of AVCam in Swift 3
|
||||
- Remove support for AVCaptureStillImageOutput
|
||||
- Bug fixes
|
||||
|
||||
Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
@ -0,0 +1,317 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
7AA677151CFF765600B353FB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA677141CFF765600B353FB /* AppDelegate.swift */; };
|
||||
7AA677171CFF765600B353FB /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA677161CFF765600B353FB /* CameraViewController.swift */; };
|
||||
7AA6771A1CFF765600B353FB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7AA677181CFF765600B353FB /* Main.storyboard */; };
|
||||
7AA6771C1CFF765600B353FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7AA6771B1CFF765600B353FB /* Assets.xcassets */; };
|
||||
7AA6771F1CFF765600B353FB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7AA6771D1CFF765600B353FB /* LaunchScreen.storyboard */; };
|
||||
7AA677271CFF774800B353FB /* PhotoCaptureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA677261CFF774800B353FB /* PhotoCaptureDelegate.swift */; };
|
||||
7AA677291CFF7B5C00B353FB /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA677281CFF7B5C00B353FB /* PreviewView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
7AA677111CFF765600B353FB /* AVCam.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AVCam.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7AA677141CFF765600B353FB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AA677161CFF765600B353FB /* CameraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = "<group>"; };
|
||||
7AA677191CFF765600B353FB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
7AA6771B1CFF765600B353FB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
7AA6771E1CFF765600B353FB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
7AA677201CFF765600B353FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
7AA677261CFF774800B353FB /* PhotoCaptureDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureDelegate.swift; sourceTree = "<group>"; };
|
||||
7AA677281CFF7B5C00B353FB /* PreviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = "<group>"; };
|
||||
7AE4754E1D00FFA900C2CB9E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
7AA6770E1CFF765500B353FB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
7AA677081CFF765500B353FB = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7AE4754E1D00FFA900C2CB9E /* README.md */,
|
||||
7AA677131CFF765600B353FB /* AVCam */,
|
||||
7AA677121CFF765600B353FB /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7AA677121CFF765600B353FB /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7AA677111CFF765600B353FB /* AVCam.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7AA677131CFF765600B353FB /* AVCam */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7AA677141CFF765600B353FB /* AppDelegate.swift */,
|
||||
7AA677281CFF7B5C00B353FB /* PreviewView.swift */,
|
||||
7AA677161CFF765600B353FB /* CameraViewController.swift */,
|
||||
7AA677261CFF774800B353FB /* PhotoCaptureDelegate.swift */,
|
||||
7AA677181CFF765600B353FB /* Main.storyboard */,
|
||||
7AA6771B1CFF765600B353FB /* Assets.xcassets */,
|
||||
7AA6771D1CFF765600B353FB /* LaunchScreen.storyboard */,
|
||||
7AA677201CFF765600B353FB /* Info.plist */,
|
||||
);
|
||||
path = AVCam;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
7AA677101CFF765500B353FB /* AVCam */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7AA677231CFF765600B353FB /* Build configuration list for PBXNativeTarget "AVCam" */;
|
||||
buildPhases = (
|
||||
7AA6770D1CFF765500B353FB /* Sources */,
|
||||
7AA6770E1CFF765500B353FB /* Frameworks */,
|
||||
7AA6770F1CFF765500B353FB /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = AVCam;
|
||||
productName = AVCam;
|
||||
productReference = 7AA677111CFF765600B353FB /* AVCam.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
7AA677091CFF765500B353FB /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0800;
|
||||
LastUpgradeCheck = 0800;
|
||||
ORGANIZATIONNAME = "Apple, Inc.";
|
||||
TargetAttributes = {
|
||||
7AA677101CFF765500B353FB = {
|
||||
CreatedOnToolsVersion = 8.0;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 7AA6770C1CFF765500B353FB /* Build configuration list for PBXProject "AVCam Swift" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 7AA677081CFF765500B353FB;
|
||||
productRefGroup = 7AA677121CFF765600B353FB /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
7AA677101CFF765500B353FB /* AVCam */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
7AA6770F1CFF765500B353FB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7AA6771F1CFF765600B353FB /* LaunchScreen.storyboard in Resources */,
|
||||
7AA6771C1CFF765600B353FB /* Assets.xcassets in Resources */,
|
||||
7AA6771A1CFF765600B353FB /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
7AA6770D1CFF765500B353FB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7AA677271CFF774800B353FB /* PhotoCaptureDelegate.swift in Sources */,
|
||||
7AA677291CFF7B5C00B353FB /* PreviewView.swift in Sources */,
|
||||
7AA677171CFF765600B353FB /* CameraViewController.swift in Sources */,
|
||||
7AA677151CFF765600B353FB /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
7AA677181CFF765600B353FB /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
7AA677191CFF765600B353FB /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7AA6771D1CFF765600B353FB /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
7AA6771E1CFF765600B353FB /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
7AA677211CFF765600B353FB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7AA677221CFF765600B353FB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
7AA677241CFF765600B353FB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_FILE = AVCam/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVCam";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 3.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7AA677251CFF765600B353FB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_FILE = AVCam/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVCam";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 3.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
7AA6770C1CFF765500B353FB /* Build configuration list for PBXProject "AVCam Swift" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7AA677211CFF765600B353FB /* Debug */,
|
||||
7AA677221CFF765600B353FB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
7AA677231CFF765600B353FB /* Build configuration list for PBXNativeTarget "AVCam" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7AA677241CFF765600B353FB /* Debug */,
|
||||
7AA677251CFF765600B353FB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 7AA677091CFF765500B353FB /* Project object */;
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7AA677101CFF765500B353FB"
|
||||
BuildableName = "AVCam.app"
|
||||
BlueprintName = "AVCam"
|
||||
ReferencedContainer = "container:AVCam Swift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7AA677101CFF765500B353FB"
|
||||
BuildableName = "AVCam.app"
|
||||
BlueprintName = "AVCam"
|
||||
ReferencedContainer = "container:AVCam Swift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7AA677101CFF765500B353FB"
|
||||
BuildableName = "AVCam.app"
|
||||
BlueprintName = "AVCam"
|
||||
ReferencedContainer = "container:AVCam Swift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "7AA677101CFF765500B353FB"
|
||||
BuildableName = "AVCam.app"
|
||||
BlueprintName = "AVCam"
|
||||
ReferencedContainer = "container:AVCam Swift.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Application delegate.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-Small.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-Small@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-Small@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "57x57",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "57x57",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-40.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "50x50",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small-50.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "50x50",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-Small-50@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "72x72",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-72.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "72x72",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-72@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11129.12" systemVersion="15F24" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11103.9"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<color key="tintColor" red="1" green="1" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</document>
|
|
@ -0,0 +1,193 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11129.12" systemVersion="15F24" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11103.9"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Camera View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="CameraViewController" customModule="AVCam" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3eR-Rn-XpZ" userLabel="Preview" customClass="PreviewView" customModule="AVCam" customModuleProvider="target">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<gestureRecognizers/>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="fY6-qX-ntV" appends="YES" id="G6D-dx-xU8"/>
|
||||
</connections>
|
||||
</view>
|
||||
<label hidden="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Camera Unavailable" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zf0-db-esM" userLabel="Camera Unavailable">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="24"/>
|
||||
<color key="textColor" red="1" green="1" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button hidden="YES" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FZr-Ip-7WL" userLabel="Resume">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="24"/>
|
||||
<inset key="contentEdgeInsets" minX="10" minY="5" maxX="10" maxY="5"/>
|
||||
<state key="normal" title="Tap to resume">
|
||||
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="resumeInterruptedSession:" destination="BYZ-38-t0r" eventType="touchUpInside" id="o7T-5Z-tfn"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eRT-dK-6dM" userLabel="Record">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<state key="normal" title="Record">
|
||||
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="toggleMovieRecording:" destination="BYZ-38-t0r" eventType="touchUpInside" id="9R7-Ok-FpB"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uCj-6P-mHF" userLabel="Still">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="NtC-UN-gTs"/>
|
||||
<constraint firstAttribute="width" constant="80" id="dxU-UP-4Ae"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<state key="normal" title="Photo">
|
||||
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="capturePhoto:" destination="BYZ-38-t0r" eventType="touchUpInside" id="o5K-SC-fYn"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rUJ-G6-RPv" userLabel="Camera">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<state key="normal" title="Camera">
|
||||
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="changeCamera:" destination="BYZ-38-t0r" eventType="touchUpInside" id="3W0-h3-6fc"/>
|
||||
</connections>
|
||||
</button>
|
||||
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="FAC-co-10c">
|
||||
<segments>
|
||||
<segment title="Photo"/>
|
||||
<segment title="Movie"/>
|
||||
</segments>
|
||||
<connections>
|
||||
<action selector="toggleCaptureMode:" destination="BYZ-38-t0r" eventType="valueChanged" id="SKd-67-ZHh"/>
|
||||
</connections>
|
||||
</segmentedControl>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eI6-gV-W7d">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="200" id="heR-zX-F6K"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<state key="normal" title="Live Photo Mode: On"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="toggleLivePhotoMode:" destination="BYZ-38-t0r" eventType="touchUpInside" id="JqX-wJ-Xf1"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label hidden="YES" opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Live" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pii-2r-R2l">
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="25" id="Kxo-zf-Fe1"/>
|
||||
<constraint firstAttribute="width" constant="40" id="eRd-mj-8Du"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="1" green="1" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="4"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="125-kC-WZF"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="height" secondItem="eRT-dK-6dM" secondAttribute="height" id="AEV-ew-H4g"/>
|
||||
<constraint firstItem="eI6-gV-W7d" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="8" id="Aao-6b-vLN"/>
|
||||
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="height" secondItem="8bC-Xf-vdC" secondAttribute="height" id="Ice-47-M9N"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="top" secondItem="rUJ-G6-RPv" secondAttribute="top" id="NFm-e8-abT"/>
|
||||
<constraint firstItem="FZr-Ip-7WL" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="OaZ-uO-vXY"/>
|
||||
<constraint firstItem="FAC-co-10c" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="Oow-A6-mDp"/>
|
||||
<constraint firstItem="zf0-db-esM" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="Ris-mI-8lA"/>
|
||||
<constraint firstItem="Pii-2r-R2l" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="Upd-h8-1dL"/>
|
||||
<constraint firstItem="zf0-db-esM" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="W6q-xJ-jfF"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="height" secondItem="rUJ-G6-RPv" secondAttribute="height" id="aQi-F7-E2b"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="top" secondItem="FAC-co-10c" secondAttribute="bottom" constant="20" id="aSR-Je-0lW"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="top" secondItem="eRT-dK-6dM" secondAttribute="top" id="bQd-ro-0Hw"/>
|
||||
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="uCj-6P-mHF" secondAttribute="bottom" constant="20" id="eWs-co-Aaz"/>
|
||||
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="igk-MQ-CGt"/>
|
||||
<constraint firstItem="rUJ-G6-RPv" firstAttribute="leading" secondItem="uCj-6P-mHF" secondAttribute="trailing" constant="20" id="lsk-Hm-rTd"/>
|
||||
<constraint firstAttribute="centerX" secondItem="uCj-6P-mHF" secondAttribute="centerX" id="m8a-cF-Rf0"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="width" secondItem="rUJ-G6-RPv" secondAttribute="width" id="o8j-gw-35B"/>
|
||||
<constraint firstItem="Pii-2r-R2l" firstAttribute="top" secondItem="eI6-gV-W7d" secondAttribute="bottom" constant="8" id="oDE-jY-ryC"/>
|
||||
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="width" secondItem="8bC-Xf-vdC" secondAttribute="width" id="pSC-xP-dl0"/>
|
||||
<constraint firstItem="eI6-gV-W7d" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="rqt-bn-mSt"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="width" secondItem="eRT-dK-6dM" secondAttribute="width" id="s8u-Y8-n27"/>
|
||||
<constraint firstItem="FZr-Ip-7WL" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="sTY-i6-czN"/>
|
||||
<constraint firstItem="uCj-6P-mHF" firstAttribute="leading" secondItem="eRT-dK-6dM" secondAttribute="trailing" constant="20" id="zwj-TX-t6O"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<extendedEdge key="edgesForExtendedLayout"/>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="cameraButton" destination="rUJ-G6-RPv" id="dAV-WS-N1p"/>
|
||||
<outlet property="cameraUnavailableLabel" destination="zf0-db-esM" id="P9W-lb-Pb8"/>
|
||||
<outlet property="captureModeControl" destination="FAC-co-10c" id="KXj-wg-BvS"/>
|
||||
<outlet property="capturingLivePhotoLabel" destination="Pii-2r-R2l" id="JAa-4l-5SD"/>
|
||||
<outlet property="livePhotoModeButton" destination="eI6-gV-W7d" id="r9f-cN-YSH"/>
|
||||
<outlet property="photoButton" destination="uCj-6P-mHF" id="Ha8-ua-hxy"/>
|
||||
<outlet property="previewView" destination="3eR-Rn-XpZ" id="e7I-nu-L6j"/>
|
||||
<outlet property="recordButton" destination="eRT-dK-6dM" id="iqk-en-NsW"/>
|
||||
<outlet property="resumeButton" destination="FZr-Ip-7WL" id="tX5-Sx-rQK"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
<tapGestureRecognizer id="fY6-qX-ntV">
|
||||
<connections>
|
||||
<action selector="focusAndExposeTap:" destination="BYZ-38-t0r" id="65g-8k-5pv"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-656" y="-630"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<color key="tintColor" red="1" green="1" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</document>
|
|
@ -0,0 +1,940 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
View controller for camera interface.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
import Photos
|
||||
|
||||
class CameraViewController: UIViewController, AVCaptureFileOutputRecordingDelegate {
|
||||
// MARK: View Controller Life Cycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Disable UI. The UI is enabled if and only if the session starts running.
|
||||
cameraButton.isEnabled = false
|
||||
recordButton.isEnabled = false
|
||||
photoButton.isEnabled = false
|
||||
livePhotoModeButton.isEnabled = false
|
||||
captureModeControl.isEnabled = false
|
||||
|
||||
// Set up the video preview view.
|
||||
previewView.session = session
|
||||
|
||||
/*
|
||||
Check video authorization status. Video access is required and audio
|
||||
access is optional. If audio access is denied, audio is not recorded
|
||||
during movie recording.
|
||||
*/
|
||||
switch AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo) {
|
||||
case .authorized:
|
||||
// The user has previously granted access to the camera.
|
||||
break
|
||||
|
||||
case .notDetermined:
|
||||
/*
|
||||
The user has not yet been presented with the option to grant
|
||||
video access. We suspend the session queue to delay session
|
||||
setup until the access request has completed.
|
||||
|
||||
Note that audio access will be implicitly requested when we
|
||||
create an AVCaptureDeviceInput for audio during session setup.
|
||||
*/
|
||||
sessionQueue.suspend()
|
||||
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo, completionHandler: { [unowned self] granted in
|
||||
if !granted {
|
||||
self.setupResult = .notAuthorized
|
||||
}
|
||||
self.sessionQueue.resume()
|
||||
})
|
||||
|
||||
default:
|
||||
// The user has previously denied access.
|
||||
setupResult = .notAuthorized
|
||||
}
|
||||
|
||||
/*
|
||||
Setup the capture session.
|
||||
In general it is not safe to mutate an AVCaptureSession or any of its
|
||||
inputs, outputs, or connections from multiple threads at the same time.
|
||||
|
||||
Why not do all of this on the main queue?
|
||||
Because AVCaptureSession.startRunning() is a blocking call which can
|
||||
take a long time. We dispatch session setup to the sessionQueue so
|
||||
that the main queue isn't blocked, which keeps the UI responsive.
|
||||
*/
|
||||
sessionQueue.async { [unowned self] in
|
||||
self.configureSession()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
sessionQueue.async {
|
||||
switch self.setupResult {
|
||||
case .success:
|
||||
// Only setup observers and start the session running if setup succeeded.
|
||||
self.addObservers()
|
||||
self.session.startRunning()
|
||||
self.isSessionRunning = self.session.isRunning
|
||||
|
||||
case .notAuthorized:
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
let message = NSLocalizedString("AVCam doesn't have permission to use the camera, please change privacy settings", comment: "Alert message when the user has denied access to the camera")
|
||||
let alertController = UIAlertController(title: "AVCam", message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Settings", comment: "Alert button to open Settings"), style: .`default`, handler: { action in
|
||||
UIApplication.shared.open(URL(string: UIApplicationOpenSettingsURLString)!, options: [:], completionHandler: nil)
|
||||
}))
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
case .configurationFailed:
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
let message = NSLocalizedString("Unable to capture media", comment: "Alert message when something goes wrong during capture session configuration")
|
||||
let alertController = UIAlertController(title: "AVCam", message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil))
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
sessionQueue.async { [unowned self] in
|
||||
if self.setupResult == .success {
|
||||
self.session.stopRunning()
|
||||
self.isSessionRunning = self.session.isRunning
|
||||
self.removeObservers()
|
||||
}
|
||||
}
|
||||
|
||||
super.viewWillDisappear(animated)
|
||||
}
|
||||
|
||||
override var shouldAutorotate: Bool {
|
||||
// Disable autorotation of the interface when recording is in progress.
|
||||
if let movieFileOutput = movieFileOutput {
|
||||
return !movieFileOutput.isRecording
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .all
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
if let videoPreviewLayerConnection = previewView.videoPreviewLayer.connection {
|
||||
let deviceOrientation = UIDevice.current.orientation
|
||||
guard let newVideoOrientation = deviceOrientation.videoOrientation, deviceOrientation.isPortrait || deviceOrientation.isLandscape else {
|
||||
return
|
||||
}
|
||||
|
||||
videoPreviewLayerConnection.videoOrientation = newVideoOrientation
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Session Management
|
||||
|
||||
private enum SessionSetupResult {
|
||||
case success
|
||||
case notAuthorized
|
||||
case configurationFailed
|
||||
}
|
||||
|
||||
private let session = AVCaptureSession()
|
||||
|
||||
private var isSessionRunning = false
|
||||
|
||||
private let sessionQueue = DispatchQueue(label: "session queue", attributes: [], target: nil) // Communicate with the session and other session objects on this queue.
|
||||
|
||||
private var setupResult: SessionSetupResult = .success
|
||||
|
||||
var videoDeviceInput: AVCaptureDeviceInput!
|
||||
|
||||
@IBOutlet private weak var previewView: PreviewView!
|
||||
|
||||
// Call this on the session queue.
|
||||
private func configureSession() {
|
||||
if setupResult != .success {
|
||||
return
|
||||
}
|
||||
|
||||
session.beginConfiguration()
|
||||
|
||||
/*
|
||||
We do not create an AVCaptureMovieFileOutput when setting up the session because the
|
||||
AVCaptureMovieFileOutput does not support movie recording with AVCaptureSessionPresetPhoto.
|
||||
*/
|
||||
session.sessionPreset = AVCaptureSessionPresetPhoto
|
||||
|
||||
// Add video input.
|
||||
do {
|
||||
var defaultVideoDevice: AVCaptureDevice?
|
||||
|
||||
// Choose the back dual camera if available, otherwise default to a wide angle camera.
|
||||
if let dualCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInDuoCamera, mediaType: AVMediaTypeVideo, position: .back) {
|
||||
defaultVideoDevice = dualCameraDevice
|
||||
}
|
||||
else if let backCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .back) {
|
||||
// If the back dual camera is not available, default to the back wide angle camera.
|
||||
defaultVideoDevice = backCameraDevice
|
||||
}
|
||||
else if let frontCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .front) {
|
||||
// In some cases where users break their phones, the back wide angle camera is not available. In this case, we should default to the front wide angle camera.
|
||||
defaultVideoDevice = frontCameraDevice
|
||||
}
|
||||
|
||||
let videoDeviceInput = try AVCaptureDeviceInput(device: defaultVideoDevice)
|
||||
|
||||
if session.canAddInput(videoDeviceInput) {
|
||||
session.addInput(videoDeviceInput)
|
||||
self.videoDeviceInput = videoDeviceInput
|
||||
|
||||
DispatchQueue.main.async {
|
||||
/*
|
||||
Why are we dispatching this to the main queue?
|
||||
Because AVCaptureVideoPreviewLayer is the backing layer for PreviewView and UIView
|
||||
can only be manipulated on the main thread.
|
||||
Note: As an exception to the above rule, it is not necessary to serialize video orientation changes
|
||||
on the AVCaptureVideoPreviewLayer’s connection with other session manipulation.
|
||||
|
||||
Use the status bar orientation as the initial video orientation. Subsequent orientation changes are
|
||||
handled by CameraViewController.viewWillTransition(to:with:).
|
||||
*/
|
||||
let statusBarOrientation = UIApplication.shared.statusBarOrientation
|
||||
var initialVideoOrientation: AVCaptureVideoOrientation = .portrait
|
||||
if statusBarOrientation != .unknown {
|
||||
if let videoOrientation = statusBarOrientation.videoOrientation {
|
||||
initialVideoOrientation = videoOrientation
|
||||
}
|
||||
}
|
||||
|
||||
self.previewView.videoPreviewLayer.connection.videoOrientation = initialVideoOrientation
|
||||
}
|
||||
}
|
||||
else {
|
||||
print("Could not add video device input to the session")
|
||||
setupResult = .configurationFailed
|
||||
session.commitConfiguration()
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
print("Could not create video device input: \(error)")
|
||||
setupResult = .configurationFailed
|
||||
session.commitConfiguration()
|
||||
return
|
||||
}
|
||||
|
||||
// Add audio input.
|
||||
do {
|
||||
let audioDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio)
|
||||
let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice)
|
||||
|
||||
if session.canAddInput(audioDeviceInput) {
|
||||
session.addInput(audioDeviceInput)
|
||||
}
|
||||
else {
|
||||
print("Could not add audio device input to the session")
|
||||
}
|
||||
}
|
||||
catch {
|
||||
print("Could not create audio device input: \(error)")
|
||||
}
|
||||
|
||||
// Add photo output.
|
||||
if session.canAddOutput(photoOutput)
|
||||
{
|
||||
session.addOutput(photoOutput)
|
||||
|
||||
photoOutput.isHighResolutionCaptureEnabled = true
|
||||
photoOutput.isLivePhotoCaptureEnabled = photoOutput.isLivePhotoCaptureSupported
|
||||
livePhotoMode = photoOutput.isLivePhotoCaptureSupported ? .on : .off
|
||||
}
|
||||
else {
|
||||
print("Could not add photo output to the session")
|
||||
setupResult = .configurationFailed
|
||||
session.commitConfiguration()
|
||||
return
|
||||
}
|
||||
|
||||
session.commitConfiguration()
|
||||
}
|
||||
|
||||
@IBAction private func resumeInterruptedSession(_ resumeButton: UIButton)
|
||||
{
|
||||
sessionQueue.async { [unowned self] in
|
||||
/*
|
||||
The session might fail to start running, e.g., if a phone or FaceTime call is still
|
||||
using audio or video. A failure to start the session running will be communicated via
|
||||
a session runtime error notification. To avoid repeatedly failing to start the session
|
||||
running, we only try to restart the session running in the session runtime error handler
|
||||
if we aren't trying to resume the session running.
|
||||
*/
|
||||
self.session.startRunning()
|
||||
self.isSessionRunning = self.session.isRunning
|
||||
if !self.session.isRunning {
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
let message = NSLocalizedString("Unable to resume", comment: "Alert message when unable to resume the session running")
|
||||
let alertController = UIAlertController(title: "AVCam", message: message, preferredStyle: .alert)
|
||||
let cancelAction = UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
else {
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
self.resumeButton.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum CaptureMode: Int {
|
||||
case photo = 0
|
||||
case movie = 1
|
||||
}
|
||||
|
||||
@IBOutlet private weak var captureModeControl: UISegmentedControl!
|
||||
|
||||
@IBAction private func toggleCaptureMode(_ captureModeControl: UISegmentedControl) {
|
||||
if captureModeControl.selectedSegmentIndex == CaptureMode.photo.rawValue {
|
||||
recordButton.isEnabled = false
|
||||
|
||||
sessionQueue.async { [unowned self] in
|
||||
/*
|
||||
Remove the AVCaptureMovieFileOutput from the session because movie recording is
|
||||
not supported with AVCaptureSessionPresetPhoto. Additionally, Live Photo
|
||||
capture is not supported when an AVCaptureMovieFileOutput is connected to the session.
|
||||
*/
|
||||
self.session.beginConfiguration()
|
||||
self.session.removeOutput(self.movieFileOutput)
|
||||
self.session.sessionPreset = AVCaptureSessionPresetPhoto
|
||||
self.session.commitConfiguration()
|
||||
|
||||
self.movieFileOutput = nil
|
||||
|
||||
if self.photoOutput.isLivePhotoCaptureSupported {
|
||||
self.photoOutput.isLivePhotoCaptureEnabled = true
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.livePhotoModeButton.isEnabled = true
|
||||
self.livePhotoModeButton.isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if captureModeControl.selectedSegmentIndex == CaptureMode.movie.rawValue
|
||||
{
|
||||
livePhotoModeButton.isHidden = true
|
||||
|
||||
sessionQueue.async { [unowned self] in
|
||||
let movieFileOutput = AVCaptureMovieFileOutput()
|
||||
|
||||
if self.session.canAddOutput(movieFileOutput) {
|
||||
self.session.beginConfiguration()
|
||||
self.session.addOutput(movieFileOutput)
|
||||
self.session.sessionPreset = AVCaptureSessionPresetHigh
|
||||
if let connection = movieFileOutput.connection(withMediaType: AVMediaTypeVideo) {
|
||||
if connection.isVideoStabilizationSupported {
|
||||
connection.preferredVideoStabilizationMode = .auto
|
||||
}
|
||||
}
|
||||
self.session.commitConfiguration()
|
||||
|
||||
self.movieFileOutput = movieFileOutput
|
||||
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
self.recordButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Device Configuration
|
||||
|
||||
@IBOutlet private weak var cameraButton: UIButton!
|
||||
|
||||
@IBOutlet private weak var cameraUnavailableLabel: UILabel!
|
||||
|
||||
private let videoDeviceDiscoverySession = AVCaptureDeviceDiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDuoCamera], mediaType: AVMediaTypeVideo, position: .unspecified)!
|
||||
|
||||
@IBAction private func changeCamera(_ cameraButton: UIButton) {
|
||||
cameraButton.isEnabled = false
|
||||
recordButton.isEnabled = false
|
||||
photoButton.isEnabled = false
|
||||
livePhotoModeButton.isEnabled = false
|
||||
captureModeControl.isEnabled = false
|
||||
|
||||
sessionQueue.async { [unowned self] in
|
||||
let currentVideoDevice = self.videoDeviceInput.device
|
||||
let currentPosition = currentVideoDevice!.position
|
||||
|
||||
let preferredPosition: AVCaptureDevicePosition
|
||||
let preferredDeviceType: AVCaptureDeviceType
|
||||
|
||||
switch currentPosition {
|
||||
case .unspecified, .front:
|
||||
preferredPosition = .back
|
||||
preferredDeviceType = .builtInDuoCamera
|
||||
|
||||
case .back:
|
||||
preferredPosition = .front
|
||||
preferredDeviceType = .builtInWideAngleCamera
|
||||
}
|
||||
|
||||
let devices = self.videoDeviceDiscoverySession.devices!
|
||||
var newVideoDevice: AVCaptureDevice? = nil
|
||||
|
||||
// First, look for a device with both the preferred position and device type. Otherwise, look for a device with only the preferred position.
|
||||
if let device = devices.filter({ $0.position == preferredPosition && $0.deviceType == preferredDeviceType }).first {
|
||||
newVideoDevice = device
|
||||
}
|
||||
else if let device = devices.filter({ $0.position == preferredPosition }).first {
|
||||
newVideoDevice = device
|
||||
}
|
||||
|
||||
if let videoDevice = newVideoDevice {
|
||||
do {
|
||||
let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
|
||||
|
||||
self.session.beginConfiguration()
|
||||
|
||||
// Remove the existing device input first, since using the front and back camera simultaneously is not supported.
|
||||
self.session.removeInput(self.videoDeviceInput)
|
||||
|
||||
if self.session.canAddInput(videoDeviceInput) {
|
||||
NotificationCenter.default.removeObserver(self, name: Notification.Name("AVCaptureDeviceSubjectAreaDidChangeNotification"), object: currentVideoDevice!)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaDidChange), name: Notification.Name("AVCaptureDeviceSubjectAreaDidChangeNotification"), object: videoDeviceInput.device)
|
||||
|
||||
self.session.addInput(videoDeviceInput)
|
||||
self.videoDeviceInput = videoDeviceInput
|
||||
}
|
||||
else {
|
||||
self.session.addInput(self.videoDeviceInput);
|
||||
}
|
||||
|
||||
if let connection = self.movieFileOutput?.connection(withMediaType: AVMediaTypeVideo) {
|
||||
if connection.isVideoStabilizationSupported {
|
||||
connection.preferredVideoStabilizationMode = .auto
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Set Live Photo capture enabled if it is supported. When changing cameras, the
|
||||
`isLivePhotoCaptureEnabled` property of the AVCapturePhotoOutput gets set to NO when
|
||||
a video device is disconnected from the session. After the new video device is
|
||||
added to the session, re-enable Live Photo capture on the AVCapturePhotoOutput if it is supported.
|
||||
*/
|
||||
self.photoOutput.isLivePhotoCaptureEnabled = self.photoOutput.isLivePhotoCaptureSupported;
|
||||
|
||||
self.session.commitConfiguration()
|
||||
}
|
||||
catch {
|
||||
print("Error occured while creating video device input: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
self.cameraButton.isEnabled = true
|
||||
self.recordButton.isEnabled = self.movieFileOutput != nil
|
||||
self.photoButton.isEnabled = true
|
||||
self.livePhotoModeButton.isEnabled = true
|
||||
self.captureModeControl.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction private func focusAndExposeTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
let devicePoint = self.previewView.videoPreviewLayer.captureDevicePointOfInterest(for: gestureRecognizer.location(in: gestureRecognizer.view))
|
||||
focus(with: .autoFocus, exposureMode: .autoExpose, at: devicePoint, monitorSubjectAreaChange: true)
|
||||
}
|
||||
|
||||
private func focus(with focusMode: AVCaptureFocusMode, exposureMode: AVCaptureExposureMode, at devicePoint: CGPoint, monitorSubjectAreaChange: Bool) {
|
||||
sessionQueue.async { [unowned self] in
|
||||
if let device = self.videoDeviceInput.device {
|
||||
do {
|
||||
try device.lockForConfiguration()
|
||||
|
||||
/*
|
||||
Setting (focus/exposure)PointOfInterest alone does not initiate a (focus/exposure) operation.
|
||||
Call set(Focus/Exposure)Mode() to apply the new point of interest.
|
||||
*/
|
||||
if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(focusMode) {
|
||||
device.focusPointOfInterest = devicePoint
|
||||
device.focusMode = focusMode
|
||||
}
|
||||
|
||||
if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) {
|
||||
device.exposurePointOfInterest = devicePoint
|
||||
device.exposureMode = exposureMode
|
||||
}
|
||||
|
||||
device.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange
|
||||
device.unlockForConfiguration()
|
||||
}
|
||||
catch {
|
||||
print("Could not lock device for configuration: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Capturing Photos
|
||||
|
||||
private let photoOutput = AVCapturePhotoOutput()
|
||||
|
||||
private var inProgressPhotoCaptureDelegates = [Int64 : PhotoCaptureDelegate]()
|
||||
|
||||
@IBOutlet private weak var photoButton: UIButton!
|
||||
|
||||
@IBAction private func capturePhoto(_ photoButton: UIButton) {
|
||||
/*
|
||||
Retrieve the video preview layer's video orientation on the main queue before
|
||||
entering the session queue. We do this to ensure UI elements are accessed on
|
||||
the main thread and session configuration is done on the session queue.
|
||||
*/
|
||||
let videoPreviewLayerOrientation = previewView.videoPreviewLayer.connection.videoOrientation
|
||||
|
||||
sessionQueue.async {
|
||||
// Update the photo output's connection to match the video orientation of the video preview layer.
|
||||
if let photoOutputConnection = self.photoOutput.connection(withMediaType: AVMediaTypeVideo) {
|
||||
photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
|
||||
}
|
||||
|
||||
// Capture a JPEG photo with flash set to auto and high resolution photo enabled.
|
||||
let photoSettings = AVCapturePhotoSettings()
|
||||
photoSettings.flashMode = .auto
|
||||
photoSettings.isHighResolutionPhotoEnabled = true
|
||||
if photoSettings.availablePreviewPhotoPixelFormatTypes.count > 0 {
|
||||
photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String : photoSettings.availablePreviewPhotoPixelFormatTypes.first!]
|
||||
}
|
||||
if self.livePhotoMode == .on && self.photoOutput.isLivePhotoCaptureSupported { // Live Photo capture is not supported in movie mode.
|
||||
let livePhotoMovieFileName = NSUUID().uuidString
|
||||
let livePhotoMovieFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent((livePhotoMovieFileName as NSString).appendingPathExtension("mov")!)
|
||||
photoSettings.livePhotoMovieFileURL = URL(fileURLWithPath: livePhotoMovieFilePath)
|
||||
}
|
||||
|
||||
// Use a separate object for the photo capture delegate to isolate each capture life cycle.
|
||||
let photoCaptureDelegate = PhotoCaptureDelegate(with: photoSettings, willCapturePhotoAnimation: {
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
self.previewView.videoPreviewLayer.opacity = 0
|
||||
UIView.animate(withDuration: 0.25) { [unowned self] in
|
||||
self.previewView.videoPreviewLayer.opacity = 1
|
||||
}
|
||||
}
|
||||
}, capturingLivePhoto: { capturing in
|
||||
/*
|
||||
Because Live Photo captures can overlap, we need to keep track of the
|
||||
number of in progress Live Photo captures to ensure that the
|
||||
Live Photo label stays visible during these captures.
|
||||
*/
|
||||
self.sessionQueue.async { [unowned self] in
|
||||
if capturing {
|
||||
self.inProgressLivePhotoCapturesCount += 1
|
||||
}
|
||||
else {
|
||||
self.inProgressLivePhotoCapturesCount -= 1
|
||||
}
|
||||
|
||||
let inProgressLivePhotoCapturesCount = self.inProgressLivePhotoCapturesCount
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
if inProgressLivePhotoCapturesCount > 0 {
|
||||
self.capturingLivePhotoLabel.isHidden = false
|
||||
}
|
||||
else if inProgressLivePhotoCapturesCount == 0 {
|
||||
self.capturingLivePhotoLabel.isHidden = true
|
||||
}
|
||||
else {
|
||||
print("Error: In progress live photo capture count is less than 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, completed: { [unowned self] photoCaptureDelegate in
|
||||
// When the capture is complete, remove a reference to the photo capture delegate so it can be deallocated.
|
||||
self.sessionQueue.async { [unowned self] in
|
||||
self.inProgressPhotoCaptureDelegates[photoCaptureDelegate.requestedPhotoSettings.uniqueID] = nil
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/*
|
||||
The Photo Output keeps a weak reference to the photo capture delegate so
|
||||
we store it in an array to maintain a strong reference to this object
|
||||
until the capture is completed.
|
||||
*/
|
||||
self.inProgressPhotoCaptureDelegates[photoCaptureDelegate.requestedPhotoSettings.uniqueID] = photoCaptureDelegate
|
||||
self.photoOutput.capturePhoto(with: photoSettings, delegate: photoCaptureDelegate)
|
||||
}
|
||||
}
|
||||
|
||||
private enum LivePhotoMode {
|
||||
case on
|
||||
case off
|
||||
}
|
||||
|
||||
private var livePhotoMode: LivePhotoMode = .off
|
||||
|
||||
@IBOutlet private weak var livePhotoModeButton: UIButton!
|
||||
|
||||
@IBAction private func toggleLivePhotoMode(_ livePhotoModeButton: UIButton) {
|
||||
sessionQueue.async { [unowned self] in
|
||||
self.livePhotoMode = (self.livePhotoMode == .on) ? .off : .on
|
||||
let livePhotoMode = self.livePhotoMode
|
||||
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
if livePhotoMode == .on {
|
||||
self.livePhotoModeButton.setTitle(NSLocalizedString("Live Photo Mode: On", comment: "Live photo mode button on title"), for: [])
|
||||
}
|
||||
else {
|
||||
self.livePhotoModeButton.setTitle(NSLocalizedString("Live Photo Mode: Off", comment: "Live photo mode button off title"), for: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var inProgressLivePhotoCapturesCount = 0
|
||||
|
||||
@IBOutlet var capturingLivePhotoLabel: UILabel!
|
||||
|
||||
// MARK: Recording Movies
|
||||
|
||||
private var movieFileOutput: AVCaptureMovieFileOutput? = nil
|
||||
|
||||
private var backgroundRecordingID: UIBackgroundTaskIdentifier? = nil
|
||||
|
||||
@IBOutlet private weak var recordButton: UIButton!
|
||||
|
||||
@IBOutlet private weak var resumeButton: UIButton!
|
||||
|
||||
@IBAction private func toggleMovieRecording(_ recordButton: UIButton) {
|
||||
guard let movieFileOutput = self.movieFileOutput else {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Disable the Camera button until recording finishes, and disable
|
||||
the Record button until recording starts or finishes.
|
||||
|
||||
See the AVCaptureFileOutputRecordingDelegate methods.
|
||||
*/
|
||||
cameraButton.isEnabled = false
|
||||
recordButton.isEnabled = false
|
||||
captureModeControl.isEnabled = false
|
||||
|
||||
/*
|
||||
Retrieve the video preview layer's video orientation on the main queue
|
||||
before entering the session queue. We do this to ensure UI elements are
|
||||
accessed on the main thread and session configuration is done on the session queue.
|
||||
*/
|
||||
let videoPreviewLayerOrientation = previewView.videoPreviewLayer.connection.videoOrientation
|
||||
|
||||
sessionQueue.async { [unowned self] in
|
||||
if !movieFileOutput.isRecording {
|
||||
if UIDevice.current.isMultitaskingSupported {
|
||||
/*
|
||||
Setup background task.
|
||||
This is needed because the `capture(_:, didFinishRecordingToOutputFileAt:, fromConnections:, error:)`
|
||||
callback is not received until AVCam returns to the foreground unless you request background execution time.
|
||||
This also ensures that there will be time to write the file to the photo library when AVCam is backgrounded.
|
||||
To conclude this background execution, endBackgroundTask(_:) is called in
|
||||
`capture(_:, didFinishRecordingToOutputFileAt:, fromConnections:, error:)` after the recorded file has been saved.
|
||||
*/
|
||||
self.backgroundRecordingID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
|
||||
}
|
||||
|
||||
// Update the orientation on the movie file output video connection before starting recording.
|
||||
let movieFileOutputConnection = self.movieFileOutput?.connection(withMediaType: AVMediaTypeVideo)
|
||||
movieFileOutputConnection?.videoOrientation = videoPreviewLayerOrientation
|
||||
|
||||
// Start recording to a temporary file.
|
||||
let outputFileName = NSUUID().uuidString
|
||||
let outputFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent((outputFileName as NSString).appendingPathExtension("mov")!)
|
||||
movieFileOutput.startRecording(toOutputFileURL: URL(fileURLWithPath: outputFilePath), recordingDelegate: self)
|
||||
}
|
||||
else {
|
||||
movieFileOutput.stopRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func capture(_ captureOutput: AVCaptureFileOutput!, didStartRecordingToOutputFileAt fileURL: URL!, fromConnections connections: [Any]!) {
|
||||
// Enable the Record button to let the user stop the recording.
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
self.recordButton.isEnabled = true
|
||||
self.recordButton.setTitle(NSLocalizedString("Stop", comment: "Recording button stop title"), for: [])
|
||||
}
|
||||
}
|
||||
|
||||
func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!) {
|
||||
/*
|
||||
Note that currentBackgroundRecordingID is used to end the background task
|
||||
associated with this recording. This allows a new recording to be started,
|
||||
associated with a new UIBackgroundTaskIdentifier, once the movie file output's
|
||||
`isRecording` property is back to false — which happens sometime after this method
|
||||
returns.
|
||||
|
||||
Note: Since we use a unique file path for each recording, a new recording will
|
||||
not overwrite a recording currently being saved.
|
||||
*/
|
||||
func cleanup() {
|
||||
let path = outputFileURL.path
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: path)
|
||||
}
|
||||
catch {
|
||||
print("Could not remove file at url: \(outputFileURL)")
|
||||
}
|
||||
}
|
||||
|
||||
if let currentBackgroundRecordingID = backgroundRecordingID {
|
||||
backgroundRecordingID = UIBackgroundTaskInvalid
|
||||
|
||||
if currentBackgroundRecordingID != UIBackgroundTaskInvalid {
|
||||
UIApplication.shared.endBackgroundTask(currentBackgroundRecordingID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var success = true
|
||||
|
||||
if error != nil {
|
||||
print("Movie file finishing error: \(error)")
|
||||
success = (((error as NSError).userInfo[AVErrorRecordingSuccessfullyFinishedKey] as AnyObject).boolValue)!
|
||||
}
|
||||
|
||||
if success {
|
||||
// Check authorization status.
|
||||
PHPhotoLibrary.requestAuthorization { status in
|
||||
if status == .authorized {
|
||||
// Save the movie file to the photo library and cleanup.
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let options = PHAssetResourceCreationOptions()
|
||||
options.shouldMoveFile = true
|
||||
let creationRequest = PHAssetCreationRequest.forAsset()
|
||||
creationRequest.addResource(with: .video, fileURL: outputFileURL, options: options)
|
||||
}, completionHandler: { success, error in
|
||||
if !success {
|
||||
print("Could not save movie to photo library: \(error)")
|
||||
}
|
||||
cleanup()
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
// Enable the Camera and Record buttons to let the user switch camera and start another recording.
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
// Only enable the ability to change camera if the device has more than one camera.
|
||||
self.cameraButton.isEnabled = self.videoDeviceDiscoverySession.uniqueDevicePositionsCount() > 1
|
||||
self.recordButton.isEnabled = true
|
||||
self.captureModeControl.isEnabled = true
|
||||
self.recordButton.setTitle(NSLocalizedString("Record", comment: "Recording button record title"), for: [])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: KVO and Notifications
|
||||
|
||||
private var sessionRunningObserveContext = 0
|
||||
|
||||
private func addObservers() {
|
||||
session.addObserver(self, forKeyPath: "running", options: .new, context: &sessionRunningObserveContext)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(subjectAreaDidChange), name: Notification.Name("AVCaptureDeviceSubjectAreaDidChangeNotification"), object: videoDeviceInput.device)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sessionRuntimeError), name: Notification.Name("AVCaptureSessionRuntimeErrorNotification"), object: session)
|
||||
|
||||
/*
|
||||
A session can only run when the app is full screen. It will be interrupted
|
||||
in a multi-app layout, introduced in iOS 9, see also the documentation of
|
||||
AVCaptureSessionInterruptionReason. Add observers to handle these session
|
||||
interruptions and show a preview is paused message. See the documentation
|
||||
of AVCaptureSessionWasInterruptedNotification for other interruption reasons.
|
||||
*/
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sessionWasInterrupted), name: Notification.Name("AVCaptureSessionWasInterruptedNotification"), object: session)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sessionInterruptionEnded), name: Notification.Name("AVCaptureSessionInterruptionEndedNotification"), object: session)
|
||||
}
|
||||
|
||||
private func removeObservers() {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
session.removeObserver(self, forKeyPath: "running", context: &sessionRunningObserveContext)
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if context == &sessionRunningObserveContext {
|
||||
let newValue = change?[.newKey] as AnyObject?
|
||||
guard let isSessionRunning = newValue?.boolValue else { return }
|
||||
let isLivePhotoCaptureSupported = photoOutput.isLivePhotoCaptureSupported
|
||||
let isLivePhotoCaptureEnabled = photoOutput.isLivePhotoCaptureEnabled
|
||||
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
// Only enable the ability to change camera if the device has more than one camera.
|
||||
self.cameraButton.isEnabled = isSessionRunning && self.videoDeviceDiscoverySession.uniqueDevicePositionsCount() > 1
|
||||
self.recordButton.isEnabled = isSessionRunning && self.movieFileOutput != nil
|
||||
self.photoButton.isEnabled = isSessionRunning
|
||||
self.captureModeControl.isEnabled = isSessionRunning
|
||||
self.livePhotoModeButton.isEnabled = isSessionRunning && isLivePhotoCaptureEnabled
|
||||
self.livePhotoModeButton.isHidden = !(isSessionRunning && isLivePhotoCaptureSupported)
|
||||
}
|
||||
}
|
||||
else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
func subjectAreaDidChange(notification: NSNotification) {
|
||||
let devicePoint = CGPoint(x: 0.5, y: 0.5)
|
||||
focus(with: .autoFocus, exposureMode: .continuousAutoExposure, at: devicePoint, monitorSubjectAreaChange: false)
|
||||
}
|
||||
|
||||
func sessionRuntimeError(notification: NSNotification) {
|
||||
guard let errorValue = notification.userInfo?[AVCaptureSessionErrorKey] as? NSError else {
|
||||
return
|
||||
}
|
||||
|
||||
let error = AVError(_nsError: errorValue)
|
||||
print("Capture session runtime error: \(error)")
|
||||
|
||||
/*
|
||||
Automatically try to restart the session running if media services were
|
||||
reset and the last start running succeeded. Otherwise, enable the user
|
||||
to try to resume the session running.
|
||||
*/
|
||||
if error.code == .mediaServicesWereReset {
|
||||
sessionQueue.async { [unowned self] in
|
||||
if self.isSessionRunning {
|
||||
self.session.startRunning()
|
||||
self.isSessionRunning = self.session.isRunning
|
||||
}
|
||||
else {
|
||||
DispatchQueue.main.async { [unowned self] in
|
||||
self.resumeButton.isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
resumeButton.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func sessionWasInterrupted(notification: NSNotification) {
|
||||
/*
|
||||
In some scenarios we want to enable the user to resume the session running.
|
||||
For example, if music playback is initiated via control center while
|
||||
using AVCam, then the user can let AVCam resume
|
||||
the session running, which will stop music playback. Note that stopping
|
||||
music playback in control center will not automatically resume the session
|
||||
running. Also note that it is not always possible to resume, see `resumeInterruptedSession(_:)`.
|
||||
*/
|
||||
if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?, let reasonIntegerValue = userInfoValue.integerValue, let reason = AVCaptureSessionInterruptionReason(rawValue: reasonIntegerValue) {
|
||||
print("Capture session was interrupted with reason \(reason)")
|
||||
|
||||
var showResumeButton = false
|
||||
|
||||
if reason == AVCaptureSessionInterruptionReason.audioDeviceInUseByAnotherClient || reason == AVCaptureSessionInterruptionReason.videoDeviceInUseByAnotherClient {
|
||||
showResumeButton = true
|
||||
}
|
||||
else if reason == AVCaptureSessionInterruptionReason.videoDeviceNotAvailableWithMultipleForegroundApps {
|
||||
// Simply fade-in a label to inform the user that the camera is unavailable.
|
||||
cameraUnavailableLabel.alpha = 0
|
||||
cameraUnavailableLabel.isHidden = false
|
||||
UIView.animate(withDuration: 0.25) { [unowned self] in
|
||||
self.cameraUnavailableLabel.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
if showResumeButton {
|
||||
// Simply fade-in a button to enable the user to try to resume the session running.
|
||||
resumeButton.alpha = 0
|
||||
resumeButton.isHidden = false
|
||||
UIView.animate(withDuration: 0.25) { [unowned self] in
|
||||
self.resumeButton.alpha = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sessionInterruptionEnded(notification: NSNotification) {
|
||||
print("Capture session interruption ended")
|
||||
|
||||
if !resumeButton.isHidden {
|
||||
UIView.animate(withDuration: 0.25,
|
||||
animations: { [unowned self] in
|
||||
self.resumeButton.alpha = 0
|
||||
}, completion: { [unowned self] finished in
|
||||
self.resumeButton.isHidden = true
|
||||
}
|
||||
)
|
||||
}
|
||||
if !cameraUnavailableLabel.isHidden {
|
||||
UIView.animate(withDuration: 0.25,
|
||||
animations: { [unowned self] in
|
||||
self.cameraUnavailableLabel.alpha = 0
|
||||
}, completion: { [unowned self] finished in
|
||||
self.cameraUnavailableLabel.isHidden = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIDeviceOrientation {
|
||||
var videoOrientation: AVCaptureVideoOrientation? {
|
||||
switch self {
|
||||
case .portrait: return .portrait
|
||||
case .portraitUpsideDown: return .portraitUpsideDown
|
||||
case .landscapeLeft: return .landscapeRight
|
||||
case .landscapeRight: return .landscapeLeft
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIInterfaceOrientation {
|
||||
var videoOrientation: AVCaptureVideoOrientation? {
|
||||
switch self {
|
||||
case .portrait: return .portrait
|
||||
case .portraitUpsideDown: return .portraitUpsideDown
|
||||
case .landscapeLeft: return .landscapeLeft
|
||||
case .landscapeRight: return .landscapeRight
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AVCaptureDeviceDiscoverySession {
|
||||
func uniqueDevicePositionsCount() -> Int {
|
||||
var uniqueDevicePositions = [AVCaptureDevicePosition]()
|
||||
|
||||
for device in devices {
|
||||
if !uniqueDevicePositions.contains(device.position) {
|
||||
uniqueDevicePositions.append(device.position)
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueDevicePositions.count
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>to take photos and video</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>to record Live Photos and movies</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>to save photos and videos to your Photo Library</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>Launch Screen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Photo capture delegate.
|
||||
*/
|
||||
|
||||
import AVFoundation
|
||||
import Photos
|
||||
|
||||
class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
|
||||
private(set) var requestedPhotoSettings: AVCapturePhotoSettings
|
||||
|
||||
private let willCapturePhotoAnimation: () -> ()
|
||||
|
||||
private let capturingLivePhoto: (Bool) -> ()
|
||||
|
||||
private let completed: (PhotoCaptureDelegate) -> ()
|
||||
|
||||
private var photoData: Data? = nil
|
||||
|
||||
private var livePhotoCompanionMovieURL: URL? = nil
|
||||
|
||||
init(with requestedPhotoSettings: AVCapturePhotoSettings, willCapturePhotoAnimation: @escaping () -> (), capturingLivePhoto: @escaping (Bool) -> (), completed: @escaping (PhotoCaptureDelegate) -> ()) {
|
||||
self.requestedPhotoSettings = requestedPhotoSettings
|
||||
self.willCapturePhotoAnimation = willCapturePhotoAnimation
|
||||
self.capturingLivePhoto = capturingLivePhoto
|
||||
self.completed = completed
|
||||
}
|
||||
|
||||
private func didFinish() {
|
||||
if let livePhotoCompanionMoviePath = livePhotoCompanionMovieURL?.path {
|
||||
if FileManager.default.fileExists(atPath: livePhotoCompanionMoviePath) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: livePhotoCompanionMoviePath)
|
||||
}
|
||||
catch {
|
||||
print("Could not remove file at url: \(livePhotoCompanionMoviePath)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completed(self)
|
||||
}
|
||||
|
||||
func capture(_ captureOutput: AVCapturePhotoOutput, willBeginCaptureForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings) {
|
||||
if resolvedSettings.livePhotoMovieDimensions.width > 0 && resolvedSettings.livePhotoMovieDimensions.height > 0 {
|
||||
capturingLivePhoto(true)
|
||||
}
|
||||
}
|
||||
|
||||
func capture(_ captureOutput: AVCapturePhotoOutput, willCapturePhotoForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings) {
|
||||
willCapturePhotoAnimation()
|
||||
}
|
||||
|
||||
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?, previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) {
|
||||
if let photoSampleBuffer = photoSampleBuffer {
|
||||
photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: previewPhotoSampleBuffer)
|
||||
}
|
||||
else {
|
||||
print("Error capturing photo: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishRecordingLivePhotoMovieForEventualFileAt outputFileURL: URL, resolvedSettings: AVCaptureResolvedPhotoSettings) {
|
||||
capturingLivePhoto(false)
|
||||
}
|
||||
|
||||
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingLivePhotoToMovieFileAt outputFileURL: URL, duration: CMTime, photoDisplay photoDisplayTime: CMTime, resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
|
||||
if let _ = error {
|
||||
print("Error processing live photo companion movie: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
livePhotoCompanionMovieURL = outputFileURL
|
||||
}
|
||||
|
||||
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishCaptureForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
|
||||
if let error = error {
|
||||
print("Error capturing photo: \(error)")
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
guard let photoData = photoData else {
|
||||
print("No photo data resource")
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
PHPhotoLibrary.requestAuthorization { [unowned self] status in
|
||||
if status == .authorized {
|
||||
PHPhotoLibrary.shared().performChanges({ [unowned self] in
|
||||
let creationRequest = PHAssetCreationRequest.forAsset()
|
||||
creationRequest.addResource(with: .photo, data: photoData, options: nil)
|
||||
|
||||
if let livePhotoCompanionMovieURL = self.livePhotoCompanionMovieURL {
|
||||
let livePhotoCompanionMovieFileResourceOptions = PHAssetResourceCreationOptions()
|
||||
livePhotoCompanionMovieFileResourceOptions.shouldMoveFile = true
|
||||
creationRequest.addResource(with: .pairedVideo, fileURL: livePhotoCompanionMovieURL, options: livePhotoCompanionMovieFileResourceOptions)
|
||||
}
|
||||
|
||||
}, completionHandler: { [unowned self] success, error in
|
||||
if let error = error {
|
||||
print("Error occurered while saving photo to photo library: \(error)")
|
||||
}
|
||||
|
||||
self.didFinish()
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
self.didFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Application preview view.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
class PreviewView: UIView {
|
||||
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
|
||||
return layer as! AVCaptureVideoPreviewLayer
|
||||
}
|
||||
|
||||
var session: AVCaptureSession? {
|
||||
get {
|
||||
return videoPreviewLayer.session
|
||||
}
|
||||
set {
|
||||
videoPreviewLayer.session = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UIView
|
||||
|
||||
override class var layerClass: AnyClass {
|
||||
return AVCaptureVideoPreviewLayer.self
|
||||
}
|
||||
}
|