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.
This commit is contained in:
Liu Lantao 2016-12-24 11:58:25 +08:00
parent 5c680bfc5b
commit 2a92c00b6d
63 changed files with 4015 additions and 0 deletions

42
AVCam/LICENSE.txt Normal file
View File

@ -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.

View File

@ -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 */;
}

View File

@ -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>

View File

@ -0,0 +1,15 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Application delegate.
*/
@import UIKit;
@interface AVCamAppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic) UIWindow *window;
@end

View File

@ -0,0 +1,13 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Application delegate.
*/
#import "AVCamAppDelegate.h"
@implementation AVCamAppDelegate
@end

View File

@ -0,0 +1,13 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
View controller for camera interface.
*/
@import UIKit;
@interface AVCamCameraViewController : UIViewController
@end

View File

@ -0,0 +1,947 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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 AVCaptureVideoPreviewLayers 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

View File

@ -0,0 +1,17 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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

View File

@ -0,0 +1,130 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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

View File

@ -0,0 +1,19 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Application preview view.
*/
@import UIKit;
@class AVCaptureSession;
@interface AVCamPreviewView : UIView
@property (nonatomic, readonly) AVCaptureVideoPreviewLayer *videoPreviewLayer;
@property (nonatomic) AVCaptureSession *session;
@end

View File

@ -0,0 +1,35 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,17 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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] ) );
}
}

24
AVCam/README.md Normal file
View File

@ -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.

View File

@ -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 */;
}

View File

@ -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>

View File

@ -0,0 +1,14 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Application delegate.
*/
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
}

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,940 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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 AVCaptureVideoPreviewLayers 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
}
}

View File

@ -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>

View File

@ -0,0 +1,119 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}
}
}

View File

@ -0,0 +1,31 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}