AVFoundationSimplePlayer: Version 2.0, 2016-09-13

Updated to Swift 3

Demonstrates how to create a simple movie playback app using only the AVPlayer and AVPlayerLayer classes from AVFoundation (not AVKit). You'll see how to open a movie file and then how to implement various functionality including play, pause, fast forward, rewind, volume adjustment, time slider updating, and scrubbing.
This commit is contained in:
Liu Lantao 2016-12-24 11:56:54 +08:00
parent 935094fb6e
commit 5c680bfc5b
55 changed files with 2338 additions and 0 deletions

Binary file not shown.

View File

@ -0,0 +1,42 @@
Sample code project: AVFoundationSimplePlayer-iOS: Using AVFoundation to Play Media
Version: 2.0
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,330 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
D2010EFB1B0E8A8D00BD0BDF /* ElephantSeals.mov in Resources */ = {isa = PBXBuildFile; fileRef = D2010EFA1B0E8A8D00BD0BDF /* ElephantSeals.mov */; };
D20EB3C21AF94FDE0059CD72 /* AAPLPlayerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D20EB3C11AF94FDE0059CD72 /* AAPLPlayerViewController.m */; };
D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2385B491AF5181400DC8ADE /* Main.storyboard */; };
D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2385B4C1AF5181400DC8ADE /* Images.xcassets */; };
D23E499F1B0D30AF0013F574 /* AAPLAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D23E499E1B0D30AF0013F574 /* AAPLAppDelegate.m */; };
D27489A01B1CD92F0020D82A /* AAPLPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D274899F1B1CD92F0020D82A /* AAPLPlayerView.m */; };
D27489A31B1CD9370020D82A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D27489A11B1CD9370020D82A /* Localizable.strings */; };
D2C148E51B0FA7AD004F41DA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D2C148E41B0FA7AD004F41DA /* main.m */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
D2010EFA1B0E8A8D00BD0BDF /* ElephantSeals.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; name = ElephantSeals.mov; path = ../Common/ElephantSeals.mov; sourceTree = SOURCE_ROOT; };
D20EB3C11AF94FDE0059CD72 /* AAPLPlayerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLPlayerViewController.m; sourceTree = "<group>"; };
D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-ObjC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AVFoundationSimplePlayer-ObjC.app"; sourceTree = BUILT_PRODUCTS_DIR; };
D2385B4A1AF5181400DC8ADE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
D2385B4C1AF5181400DC8ADE /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
D2385B511AF5181400DC8ADE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D23E499D1B0D2DF90013F574 /* AAPLAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLAppDelegate.h; sourceTree = "<group>"; };
D23E499E1B0D30AF0013F574 /* AAPLAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLAppDelegate.m; sourceTree = "<group>"; };
D23E49A01B0D325D0013F574 /* AAPLPlayerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLPlayerViewController.h; sourceTree = "<group>"; };
D274899E1B1CD92F0020D82A /* AAPLPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLPlayerView.h; sourceTree = "<group>"; };
D274899F1B1CD92F0020D82A /* AAPLPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLPlayerView.m; sourceTree = "<group>"; };
D27489A21B1CD9370020D82A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
D2C148E41B0FA7AD004F41DA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
D2CF22161B1E5EAD0006C864 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
D2385B3F1AF5181400DC8ADE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
D2010EFD1B0E8AD600BD0BDF /* Common */ = {
isa = PBXGroup;
children = (
D2010EFA1B0E8A8D00BD0BDF /* ElephantSeals.mov */,
);
name = Common;
sourceTree = "<group>";
};
D2385B391AF5181400DC8ADE = {
isa = PBXGroup;
children = (
D2CF22161B1E5EAD0006C864 /* README.md */,
D2385B441AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */,
D2385B431AF5181400DC8ADE /* Products */,
);
sourceTree = "<group>";
};
D2385B431AF5181400DC8ADE /* Products */ = {
isa = PBXGroup;
children = (
D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-ObjC.app */,
);
name = Products;
sourceTree = "<group>";
};
D2385B441AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */ = {
isa = PBXGroup;
children = (
D23E499E1B0D30AF0013F574 /* AAPLAppDelegate.m */,
D23E499D1B0D2DF90013F574 /* AAPLAppDelegate.h */,
D20EB3C11AF94FDE0059CD72 /* AAPLPlayerViewController.m */,
D23E49A01B0D325D0013F574 /* AAPLPlayerViewController.h */,
D274899E1B1CD92F0020D82A /* AAPLPlayerView.h */,
D274899F1B1CD92F0020D82A /* AAPLPlayerView.m */,
D2385B491AF5181400DC8ADE /* Main.storyboard */,
D2385B4C1AF5181400DC8ADE /* Images.xcassets */,
D2385B511AF5181400DC8ADE /* Info.plist */,
D27489A11B1CD9370020D82A /* Localizable.strings */,
D2C148E31B0FA76B004F41DA /* Supporting Files */,
D2010EFD1B0E8AD600BD0BDF /* Common */,
);
path = "AVFoundationSimplePlayer-iOS";
sourceTree = "<group>";
};
D2C148E31B0FA76B004F41DA /* Supporting Files */ = {
isa = PBXGroup;
children = (
D2C148E41B0FA7AD004F41DA /* main.m */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
D2385B411AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationSimplePlayer-iOS" */;
buildPhases = (
D2385B3E1AF5181400DC8ADE /* Sources */,
D2385B3F1AF5181400DC8ADE /* Frameworks */,
D2385B401AF5181400DC8ADE /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "AVFoundationSimplePlayer-iOS";
productName = "AVFoundationSimplePlayer-iOS";
productReference = D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-ObjC.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
D2385B3A1AF5181400DC8ADE /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0700;
ORGANIZATIONNAME = "Apple Inc.";
TargetAttributes = {
D2385B411AF5181400DC8ADE = {
CreatedOnToolsVersion = 7.0;
};
};
};
buildConfigurationList = D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationSimplePlayer-iOS" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = D2385B391AF5181400DC8ADE;
productRefGroup = D2385B431AF5181400DC8ADE /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
D2385B411AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
D2385B401AF5181400DC8ADE /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D27489A31B1CD9370020D82A /* Localizable.strings in Resources */,
D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */,
D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */,
D2010EFB1B0E8A8D00BD0BDF /* ElephantSeals.mov in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
D2385B3E1AF5181400DC8ADE /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D23E499F1B0D30AF0013F574 /* AAPLAppDelegate.m in Sources */,
D27489A01B1CD92F0020D82A /* AAPLPlayerView.m in Sources */,
D2C148E51B0FA7AD004F41DA /* main.m in Sources */,
D20EB3C21AF94FDE0059CD72 /* AAPLPlayerViewController.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
D2385B491AF5181400DC8ADE /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
D2385B4A1AF5181400DC8ADE /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
D27489A11B1CD9370020D82A /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
D27489A21B1CD9370020D82A /* en */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
D2385B5D1AF5181400DC8ADE /* 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 = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
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 = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
D2385B5E1AF5181400DC8ADE /* 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 = "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 = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
D2385B601AF5181400DC8ADE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
INFOPLIST_FILE = "AVFoundationSimplePlayer-iOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_NAME = "AVFoundationSimplePlayer-ObjC";
};
name = Debug;
};
D2385B611AF5181400DC8ADE /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
INFOPLIST_FILE = "AVFoundationSimplePlayer-iOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_NAME = "AVFoundationSimplePlayer-ObjC";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationSimplePlayer-iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D2385B5D1AF5181400DC8ADE /* Debug */,
D2385B5E1AF5181400DC8ADE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationSimplePlayer-iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D2385B601AF5181400DC8ADE /* Debug */,
D2385B611AF5181400DC8ADE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = D2385B3A1AF5181400DC8ADE /* Project object */;
}

View File

@ -0,0 +1,17 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Application delegate.
*/
@import UIKit;
@interface AAPLAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, 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 "AAPLAppDelegate.h"
@implementation AAPLAppDelegate
@end

View File

@ -0,0 +1,16 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
View containing an AVPlayerLayer.
*/
@import UIKit;
@class AVPlayer;
@interface AAPLPlayerView : UIView
@property AVPlayer *player;
@property (readonly) AVPlayerLayer *playerLayer;
@end

View File

@ -0,0 +1,34 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
View containing an AVPlayerLayer.
*/
@import Foundation;
@import AVFoundation;
#import "AAPLPlayerView.h"
@implementation AAPLPlayerView
- (AVPlayer *)player {
return self.playerLayer.player;
}
- (void)setPlayer:(AVPlayer *)player {
self.playerLayer.player = player;
}
// override UIView
+ (Class)layerClass {
return [AVPlayerLayer class];
}
- (AVPlayerLayer *)playerLayer {
return (AVPlayerLayer *)self.layer;
}
@end

View File

@ -0,0 +1,32 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
View controller containing a player view and basic playback controls.
*/
@import UIKit;
@class AAPLPlayerView;
@interface AAPLPlayerViewController : UIViewController
@property (readonly) AVPlayer *player;
@property AVURLAsset *asset;
@property CMTime currentTime;
@property (readonly) CMTime duration;
@property float rate;
@property (weak) IBOutlet UISlider *timeSlider;
@property (weak) IBOutlet UILabel *startTimeLabel;
@property (weak) IBOutlet UILabel *durationLabel;
@property (weak) IBOutlet UIButton *rewindButton;
@property (weak) IBOutlet UIButton *playPauseButton;
@property (weak) IBOutlet UIButton *fastForwardButton;
@property (weak) IBOutlet AAPLPlayerView *playerView;
@end

View File

@ -0,0 +1,316 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
View controller containing a player view and basic playback controls.
*/
@import Foundation;
@import AVFoundation;
@import CoreMedia.CMTime;
#import "AAPLPlayerViewController.h"
#import "AAPLPlayerView.h"
// Private properties
@interface AAPLPlayerViewController ()
{
AVPlayer *_player;
AVURLAsset *_asset;
id<NSObject> _timeObserverToken;
AVPlayerItem *_playerItem;
}
@property AVPlayerItem *playerItem;
@property (readonly) AVPlayerLayer *playerLayer;
@end
@implementation AAPLPlayerViewController
// MARK: - View Handling
/*
KVO context used to differentiate KVO callbacks for this class versus other
classes in its class hierarchy.
*/
static int AAPLPlayerViewControllerKVOContext = 0;
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
/*
Update the UI when these player properties change.
Use the context parameter to distinguish KVO for our particular observers and not
those destined for a subclass that also happens to be observing these properties.
*/
[self addObserver:self forKeyPath:@"asset" options:NSKeyValueObservingOptionNew context:&AAPLPlayerViewControllerKVOContext];
[self addObserver:self forKeyPath:@"player.currentItem.duration" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext];
[self addObserver:self forKeyPath:@"player.rate" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext];
[self addObserver:self forKeyPath:@"player.currentItem.status" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext];
self.playerView.playerLayer.player = self.player;
NSURL *movieURL = [[NSBundle mainBundle] URLForResource:@"ElephantSeals" withExtension:@"mov"];
self.asset = [AVURLAsset assetWithURL:movieURL];
// Use a weak self variable to avoid a retain cycle in the block.
AAPLPlayerViewController __weak *weakSelf = self;
_timeObserverToken = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:
^(CMTime time) {
weakSelf.timeSlider.value = CMTimeGetSeconds(time);
}];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
if (_timeObserverToken) {
[self.player removeTimeObserver:_timeObserverToken];
_timeObserverToken = nil;
}
[self.player pause];
[self removeObserver:self forKeyPath:@"asset" context:&AAPLPlayerViewControllerKVOContext];
[self removeObserver:self forKeyPath:@"player.currentItem.duration" context:&AAPLPlayerViewControllerKVOContext];
[self removeObserver:self forKeyPath:@"player.rate" context:&AAPLPlayerViewControllerKVOContext];
[self removeObserver:self forKeyPath:@"player.currentItem.status" context:&AAPLPlayerViewControllerKVOContext];
}
// MARK: - Properties
// Will attempt load and test these asset keys before playing
+ (NSArray *)assetKeysRequiredToPlay {
return @[ @"playable", @"hasProtectedContent" ];
}
- (AVPlayer *)player {
if (!_player)
_player = [[AVPlayer alloc] init];
return _player;
}
- (CMTime)currentTime {
return self.player.currentTime;
}
- (void)setCurrentTime:(CMTime)newCurrentTime {
[self.player seekToTime:newCurrentTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
- (CMTime)duration {
return self.player.currentItem ? self.player.currentItem.duration : kCMTimeZero;
}
- (float)rate {
return self.player.rate;
}
- (void)setRate:(float)newRate {
self.player.rate = newRate;
}
- (AVPlayerLayer *)playerLayer {
return self.playerView.playerLayer;
}
- (AVPlayerItem *)playerItem {
return _playerItem;
}
- (void)setPlayerItem:(AVPlayerItem *)newPlayerItem {
if (_playerItem != newPlayerItem) {
_playerItem = newPlayerItem;
// If needed, configure player item here before associating it with a player
// (example: adding outputs, setting text style rules, selecting media options)
[self.player replaceCurrentItemWithPlayerItem:_playerItem];
}
}
// MARK: - Asset Loading
- (void)asynchronouslyLoadURLAsset:(AVURLAsset *)newAsset {
/*
Using AVAsset now runs the risk of blocking the current thread
(the main UI thread) whilst I/O happens to populate the
properties. It's prudent to defer our work until the properties
we need have been loaded.
*/
[newAsset loadValuesAsynchronouslyForKeys:AAPLPlayerViewController.assetKeysRequiredToPlay completionHandler:^{
/*
The asset invokes its completion handler on an arbitrary queue.
To avoid multiple threads using our internal state at the same time
we'll elect to use the main thread at all times, let's dispatch
our handler to the main queue.
*/
dispatch_async(dispatch_get_main_queue(), ^{
if (newAsset != self.asset) {
/*
self.asset has already changed! No point continuing because
another newAsset will come along in a moment.
*/
return;
}
/*
Test whether the values of each of the keys we need have been
successfully loaded.
*/
for (NSString *key in self.class.assetKeysRequiredToPlay) {
NSError *error = nil;
if ([newAsset statusOfValueForKey:key error:&error] == AVKeyValueStatusFailed) {
NSString *message = [NSString localizedStringWithFormat:NSLocalizedString(@"error.asset_key_%@_failed.description", @"Can't use this AVAsset because one of it's keys failed to load"), key];
[self handleErrorWithMessage:message error:error];
return;
}
}
// We can't play this asset.
if (!newAsset.playable || newAsset.hasProtectedContent) {
NSString *message = NSLocalizedString(@"error.asset_not_playable.description", @"Can't use this AVAsset because it isn't playable or has protected content");
[self handleErrorWithMessage:message error:nil];
return;
}
/*
We can play this asset. Create a new AVPlayerItem and make it
our player's current item.
*/
self.playerItem = [AVPlayerItem playerItemWithAsset:newAsset];
});
}];
}
// MARK: - IBActions
- (IBAction)playPauseButtonWasPressed:(UIButton *)sender {
if (self.player.rate != 1.0) {
// not playing foward so play
if (CMTIME_COMPARE_INLINE(self.currentTime, ==, self.duration)) {
// at end so got back to begining
self.currentTime = kCMTimeZero;
}
[self.player play];
} else {
// playing so pause
[self.player pause];
}
}
- (IBAction)rewindButtonWasPressed:(UIButton *)sender {
self.rate = MAX(self.player.rate - 2.0, -2.0); // rewind no faster than -2.0
}
- (IBAction)fastForwardButtonWasPressed:(UIButton *)sender {
self.rate = MIN(self.player.rate + 2.0, 2.0); // fast forward no faster than 2.0
}
- (IBAction)timeSliderDidChange:(UISlider *)sender {
self.currentTime = CMTimeMakeWithSeconds(sender.value, 1000);
}
// MARK: - KV Observation
// Update our UI when player or player.currentItem changes
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context != &AAPLPlayerViewControllerKVOContext) {
// KVO isn't for us.
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
if ([keyPath isEqualToString:@"asset"]) {
if (self.asset) {
[self asynchronouslyLoadURLAsset:self.asset];
}
}
else if ([keyPath isEqualToString:@"player.currentItem.duration"]) {
// Update timeSlider and enable/disable controls when duration > 0.0
// Handle NSNull value for NSKeyValueChangeNewKey, i.e. when player.currentItem is nil
NSValue *newDurationAsValue = change[NSKeyValueChangeNewKey];
CMTime newDuration = [newDurationAsValue isKindOfClass:[NSValue class]] ? newDurationAsValue.CMTimeValue : kCMTimeZero;
BOOL hasValidDuration = CMTIME_IS_NUMERIC(newDuration) && newDuration.value != 0;
double newDurationSeconds = hasValidDuration ? CMTimeGetSeconds(newDuration) : 0.0;
self.timeSlider.maximumValue = newDurationSeconds;
self.timeSlider.value = hasValidDuration ? CMTimeGetSeconds(self.currentTime) : 0.0;
self.rewindButton.enabled = hasValidDuration;
self.playPauseButton.enabled = hasValidDuration;
self.fastForwardButton.enabled = hasValidDuration;
self.timeSlider.enabled = hasValidDuration;
self.startTimeLabel.enabled = hasValidDuration;
self.durationLabel.enabled = hasValidDuration;
int wholeMinutes = (int)trunc(newDurationSeconds / 60);
self.durationLabel.text = [NSString stringWithFormat:@"%d:%02d", wholeMinutes, (int)trunc(newDurationSeconds) - wholeMinutes * 60];
}
else if ([keyPath isEqualToString:@"player.rate"]) {
// Update playPauseButton image
double newRate = [change[NSKeyValueChangeNewKey] doubleValue];
UIImage *buttonImage = (newRate == 1.0) ? [UIImage imageNamed:@"PauseButton"] : [UIImage imageNamed:@"PlayButton"];
[self.playPauseButton setImage:buttonImage forState:UIControlStateNormal];
}
else if ([keyPath isEqualToString:@"player.currentItem.status"]) {
// Display an error if status becomes Failed
// Handle NSNull value for NSKeyValueChangeNewKey, i.e. when player.currentItem is nil
NSNumber *newStatusAsNumber = change[NSKeyValueChangeNewKey];
AVPlayerItemStatus newStatus = [newStatusAsNumber isKindOfClass:[NSNumber class]] ? newStatusAsNumber.integerValue : AVPlayerItemStatusUnknown;
if (newStatus == AVPlayerItemStatusFailed) {
[self handleErrorWithMessage:self.player.currentItem.error.localizedDescription error:self.player.currentItem.error];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// Trigger KVO for anyone observing our properties affected by player and player.currentItem
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
if ([key isEqualToString:@"duration"]) {
return [NSSet setWithArray:@[ @"player.currentItem.duration" ]];
} else if ([key isEqualToString:@"currentTime"]) {
return [NSSet setWithArray:@[ @"player.currentItem.currentTime" ]];
} else if ([key isEqualToString:@"rate"]) {
return [NSSet setWithArray:@[ @"player.rate" ]];
} else {
return [super keyPathsForValuesAffectingValueForKey:key];
}
}
// MARK: - Error Handling
- (void)handleErrorWithMessage:(NSString *)message error:(NSError *)error {
NSLog(@"Error occured with message: %@, error: %@.", message, error);
NSString *alertTitle = NSLocalizedString(@"alert.error.title", @"Alert title for errors");
NSString *defaultAlertMesssage = NSLocalizedString(@"error.default.description", @"Default error message when no NSError provided");
UIAlertController *controller = [UIAlertController alertControllerWithTitle:alertTitle message:message ?: defaultAlertMesssage preferredStyle:UIAlertControllerStyleAlert];
NSString *alertActionTitle = NSLocalizedString(@"alert.error.actions.OK", @"OK on error alert");
UIAlertAction *action = [UIAlertAction actionWithTitle:alertActionTitle style:UIAlertActionStyleDefault handler:nil];
[controller addAction:action];
[self presentViewController:controller animated:YES completion:nil];
}
@end

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8121.17" systemVersion="14E8" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
</dependencies>
<scenes>
<!--Player View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="AAPLPlayerViewController" 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="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xrl-4x-Edh">
<rect key="frame" x="70" y="550" width="70" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="5cK-ah-alZ"/>
<constraint firstAttribute="width" constant="70" id="E4B-3O-1NJ"/>
</constraints>
<state key="normal" image="PlayButton"/>
<connections>
<action selector="playPauseButtonWasPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="FYC-AI-liU"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="AcP-gW-NjJ">
<rect key="frame" x="140" y="550" width="70" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="Ds3-V4-PI5"/>
<constraint firstAttribute="width" constant="70" id="cXK-QP-qNN"/>
</constraints>
<state key="normal" image="ScanForwardButton"/>
<connections>
<action selector="fastForwardButtonWasPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="9VR-W7-z4u"/>
</connections>
</button>
<slider opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="zs2-tO-O9K">
<rect key="frame" x="218" y="550" width="364" height="31"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="Wah-wD-K8s"/>
</constraints>
<connections>
<action selector="timeSliderDidChange:" destination="BYZ-38-t0r" eventType="valueChanged" id="iEm-kp-I1a"/>
</connections>
</slider>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="k2B-HR-6u1" customClass="AAPLPlayerView">
<rect key="frame" x="0.0" y="20" width="600" height="500"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
</view>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1Ph-2J-7JL">
<rect key="frame" x="0.0" y="550" width="70" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="70" id="0HV-El-Nhy"/>
<constraint firstAttribute="height" constant="30" id="Ju6-Bx-wC5"/>
</constraints>
<state key="normal" image="ScanBackwardButton"/>
<connections>
<action selector="rewindButtonWasPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="a5z-7z-Dbp"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0:00" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" enabled="NO" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lSE-mk-Glu" userLabel="StartTime">
<rect key="frame" x="220" y="530" width="50" height="20"/>
<constraints>
<constraint firstAttribute="height" constant="20" id="3FX-0e-O6P"/>
<constraint firstAttribute="width" constant="50" id="FxO-it-ceh"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="-:--" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" enabled="NO" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qOC-Kz-nZX" userLabel="Duration">
<rect key="frame" x="530" y="530" width="50" height="20"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="dHY-sI-1Zo"/>
<constraint firstAttribute="height" constant="20" id="vny-2p-5hq"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="zs2-tO-O9K" firstAttribute="leading" secondItem="lSE-mk-Glu" secondAttribute="leading" id="0Nl-Eq-0C9"/>
<constraint firstItem="AcP-gW-NjJ" firstAttribute="leading" secondItem="xrl-4x-Edh" secondAttribute="trailing" id="0Xi-o4-GPm"/>
<constraint firstItem="k2B-HR-6u1" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="8RV-Wu-cZ2"/>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="1Ph-2J-7JL" secondAttribute="bottom" constant="20" id="AN8-Ow-vgP"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="leading" secondItem="AcP-gW-NjJ" secondAttribute="trailing" constant="10" id="Djj-sA-9ve"/>
<constraint firstItem="qOC-Kz-nZX" firstAttribute="trailing" secondItem="zs2-tO-O9K" secondAttribute="trailing" id="Ny2-em-Hsb"/>
<constraint firstAttribute="trailingMargin" secondItem="k2B-HR-6u1" secondAttribute="trailing" constant="-20" id="SHT-LM-7fv"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="width" secondItem="lSE-mk-Glu" secondAttribute="width" id="UTA-oc-lgo"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="centerY" secondItem="AcP-gW-NjJ" secondAttribute="centerY" id="aUq-JI-udW"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="centerX" secondItem="lSE-mk-Glu" secondAttribute="centerX" id="eau-ex-HCT"/>
<constraint firstItem="k2B-HR-6u1" firstAttribute="top" secondItem="8bC-Xf-vdC" secondAttribute="topMargin" constant="70" id="fr7-hU-dZX"/>
<constraint firstItem="xrl-4x-Edh" firstAttribute="centerY" secondItem="1Ph-2J-7JL" secondAttribute="centerY" id="gSS-SM-9qW"/>
<constraint firstItem="k2B-HR-6u1" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="-20" id="hIA-sm-1WB"/>
<constraint firstItem="AcP-gW-NjJ" firstAttribute="centerY" secondItem="xrl-4x-Edh" secondAttribute="centerY" id="hnG-E0-mGw"/>
<constraint firstItem="1Ph-2J-7JL" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="-20" id="iAK-lW-ewa"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailingMargin" id="ill-9D-3bq"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="top" secondItem="lSE-mk-Glu" secondAttribute="bottom" id="jXv-uJ-vtR"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="top" secondItem="qOC-Kz-nZX" secondAttribute="bottom" id="mP2-9X-QOD"/>
<constraint firstItem="xrl-4x-Edh" firstAttribute="leading" secondItem="1Ph-2J-7JL" secondAttribute="trailing" id="nWe-M1-J3B"/>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="k2B-HR-6u1" secondAttribute="bottom" constant="80" id="zi6-Fl-Tea"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="fr7-hU-dZX"/>
<exclude reference="UTA-oc-lgo"/>
<exclude reference="eau-ex-HCT"/>
</mask>
</variation>
</view>
<connections>
<outlet property="durationLabel" destination="qOC-Kz-nZX" id="vdA-zn-17E"/>
<outlet property="fastForwardButton" destination="AcP-gW-NjJ" id="bVk-tK-Ap4"/>
<outlet property="playPauseButton" destination="xrl-4x-Edh" id="BeD-Na-Ocg"/>
<outlet property="playerView" destination="k2B-HR-6u1" id="w2i-er-gei"/>
<outlet property="rewindButton" destination="1Ph-2J-7JL" id="Fwp-1T-h4Q"/>
<outlet property="startTimeLabel" destination="lSE-mk-Glu" id="0k2-Ci-tDY"/>
<outlet property="timeSlider" destination="zs2-tO-O9K" id="BKu-N7-khJ"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="357" y="421"/>
</scene>
</scenes>
<resources>
<image name="PlayButton" width="21" height="22"/>
<image name="ScanBackwardButton" width="29" height="16"/>
<image name="ScanForwardButton" width="29" height="16"/>
</resources>
</document>

View File

@ -0,0 +1,68 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "PauseButton.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "PauseButton@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "PauseButton@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "PlayButton.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "PlayButton@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "PlayButton@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ScanBackwardButton.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ScanBackwardButton@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ScanBackwardButton@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ScanForwardButton.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ScanForwardButton@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ScanForwardButton@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,49 @@
<?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>com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)</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>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>Main</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</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>
</dict>
</plist>

View File

@ -0,0 +1,22 @@
/*
<samplecode>
<abstract>
Localizable strings.
</abstract>
</samplecode>
*/
// Alert title for errors.
"alert.error.title" = "Error";
// OK action on error alert
"alert.error.actions.OK" = "OK on error alert";
// Can't use this AVAsset because one of it's keys failed to load.
"error.asset_key_%@_failed.description" = "Media failed to load key \"%@\"";
// Can't use this AVAsset because it isn't playable or has protected content.
"error.asset_not_playable.description" = "Media isn't playable or has protected content";
// Default error message when no NSError provided.
"error.default.description" = "Unknown error";

View File

@ -0,0 +1,59 @@
/*
File: AAPLAppDelegate.m
Abstract: Sample code for AVFoundationSimplePlayer-ObjC-iOS
Version: 1.0
Disclaimer: 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) 2013-2015 Apple Inc. All Rights Reserved.
*/
#import <UIKit/UIKit.h>
#import "AAPLAppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AAPLAppDelegate class]));
}
}

View File

@ -0,0 +1,15 @@
# AVFoundationSimplePlayer-iOS: Using AVFoundation to Play Media
Demonstrates how to create a simple movie playback app using only the AVPlayer and AVPlayerLayer classes from AVFoundation (not AVKit). You'll see how to load a movie file as an AVAsset and then how to implement various functionality including play, pause, fast forward, rewind, time slider updating, and scrubbing.
## Requirements
### Build
Xcode 8.0, iOS 9.0 SDK
### Runtime
iOS 9.0 or later
Copyright (C) 2016 Apple Inc. All rights reserved.

View File

@ -0,0 +1,322 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
D2010EF91B0E8A8800BD0BDF /* ElephantSeals.mov in Resources */ = {isa = PBXBuildFile; fileRef = D2010EF81B0E8A8800BD0BDF /* ElephantSeals.mov */; };
D20EB3C21AF94FDE0059CD72 /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20EB3C11AF94FDE0059CD72 /* PlayerViewController.swift */; };
D2385B461AF5181400DC8ADE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2385B451AF5181400DC8ADE /* AppDelegate.swift */; };
D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2385B491AF5181400DC8ADE /* Main.storyboard */; };
D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2385B4C1AF5181400DC8ADE /* Images.xcassets */; };
D27489971B1CD4E00020D82A /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27489961B1CD4E00020D82A /* PlayerView.swift */; };
D274899C1B1CD4EB0020D82A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D27489981B1CD4EB0020D82A /* Localizable.strings */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
D2010EF81B0E8A8800BD0BDF /* ElephantSeals.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; name = ElephantSeals.mov; path = ../Common/ElephantSeals.mov; sourceTree = SOURCE_ROOT; };
D20EB3C11AF94FDE0059CD72 /* PlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AVFoundationSimplePlayer-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; };
D2385B451AF5181400DC8ADE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
D2385B4A1AF5181400DC8ADE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
D2385B4C1AF5181400DC8ADE /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
D2385B511AF5181400DC8ADE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D27489961B1CD4E00020D82A /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
D27489991B1CD4EB0020D82A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
D2CF22151B1E5E970006C864 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
D2385B3F1AF5181400DC8ADE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
D2010EFC1B0E8ACB00BD0BDF /* Common */ = {
isa = PBXGroup;
children = (
D2010EF81B0E8A8800BD0BDF /* ElephantSeals.mov */,
);
name = Common;
sourceTree = "<group>";
};
D2385B391AF5181400DC8ADE = {
isa = PBXGroup;
children = (
D2CF22151B1E5E970006C864 /* README.md */,
D2385B441AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */,
D2385B431AF5181400DC8ADE /* Products */,
);
sourceTree = "<group>";
};
D2385B431AF5181400DC8ADE /* Products */ = {
isa = PBXGroup;
children = (
D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-Swift.app */,
);
name = Products;
sourceTree = "<group>";
};
D2385B441AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */ = {
isa = PBXGroup;
children = (
D2385B451AF5181400DC8ADE /* AppDelegate.swift */,
D20EB3C11AF94FDE0059CD72 /* PlayerViewController.swift */,
D27489961B1CD4E00020D82A /* PlayerView.swift */,
D2385B491AF5181400DC8ADE /* Main.storyboard */,
D2385B4C1AF5181400DC8ADE /* Images.xcassets */,
D2385B511AF5181400DC8ADE /* Info.plist */,
D27489981B1CD4EB0020D82A /* Localizable.strings */,
D2010EFC1B0E8ACB00BD0BDF /* Common */,
);
path = "AVFoundationSimplePlayer-iOS";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
D2385B411AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationSimplePlayer-iOS" */;
buildPhases = (
D2385B3E1AF5181400DC8ADE /* Sources */,
D2385B3F1AF5181400DC8ADE /* Frameworks */,
D2385B401AF5181400DC8ADE /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "AVFoundationSimplePlayer-iOS";
productName = "AVFoundationSimplePlayer-iOS";
productReference = D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-Swift.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
D2385B3A1AF5181400DC8ADE /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0800;
ORGANIZATIONNAME = "Apple Inc.";
TargetAttributes = {
D2385B411AF5181400DC8ADE = {
CreatedOnToolsVersion = 7.0;
LastSwiftMigration = 0800;
};
};
};
buildConfigurationList = D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationSimplePlayer-iOS" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = D2385B391AF5181400DC8ADE;
productRefGroup = D2385B431AF5181400DC8ADE /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
D2385B411AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
D2385B401AF5181400DC8ADE /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D274899C1B1CD4EB0020D82A /* Localizable.strings in Resources */,
D2010EF91B0E8A8800BD0BDF /* ElephantSeals.mov in Resources */,
D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */,
D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
D2385B3E1AF5181400DC8ADE /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D27489971B1CD4E00020D82A /* PlayerView.swift in Sources */,
D2385B461AF5181400DC8ADE /* AppDelegate.swift in Sources */,
D20EB3C21AF94FDE0059CD72 /* PlayerViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
D2385B491AF5181400DC8ADE /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
D2385B4A1AF5181400DC8ADE /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
D27489981B1CD4EB0020D82A /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
D27489991B1CD4EB0020D82A /* en */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
D2385B5D1AF5181400DC8ADE /* 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 = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
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 = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
D2385B5E1AF5181400DC8ADE /* 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 = "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 = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
D2385B601AF5181400DC8ADE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = "AVFoundationSimplePlayer-iOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "AVFoundationSimplePlayer-Swift";
PROVISIONING_PROFILE = "";
SWIFT_VERSION = 3.0;
};
name = Debug;
};
D2385B611AF5181400DC8ADE /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = "AVFoundationSimplePlayer-iOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "AVFoundationSimplePlayer-Swift";
PROVISIONING_PROFILE = "";
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 3.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationSimplePlayer-iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D2385B5D1AF5181400DC8ADE /* Debug */,
D2385B5E1AF5181400DC8ADE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationSimplePlayer-iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D2385B601AF5181400DC8ADE /* Debug */,
D2385B611AF5181400DC8ADE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = D2385B3A1AF5181400DC8ADE /* Project object */;
}

View File

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

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8121.17" systemVersion="14E8" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
</dependencies>
<scenes>
<!--Player View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="PlayerViewController" customModule="AVFoundationSimplePlayer_Swift" 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="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xrl-4x-Edh">
<rect key="frame" x="70" y="550" width="70" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="5cK-ah-alZ"/>
<constraint firstAttribute="width" constant="70" id="E4B-3O-1NJ"/>
</constraints>
<state key="normal" image="PlayButton"/>
<connections>
<action selector="playPauseButtonWasPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="FYC-AI-liU"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="AcP-gW-NjJ">
<rect key="frame" x="140" y="550" width="70" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="Ds3-V4-PI5"/>
<constraint firstAttribute="width" constant="70" id="cXK-QP-qNN"/>
</constraints>
<state key="normal" image="ScanForwardButton"/>
<connections>
<action selector="fastForwardButtonWasPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="9VR-W7-z4u"/>
</connections>
</button>
<slider opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="zs2-tO-O9K">
<rect key="frame" x="218" y="550" width="364" height="31"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="Wah-wD-K8s"/>
</constraints>
<connections>
<action selector="timeSliderDidChange:" destination="BYZ-38-t0r" eventType="valueChanged" id="iEm-kp-I1a"/>
</connections>
</slider>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="k2B-HR-6u1" customClass="PlayerView" customModule="AVFoundationSimplePlayer_Swift" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="600" height="500"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
</view>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1Ph-2J-7JL">
<rect key="frame" x="0.0" y="550" width="70" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="70" id="0HV-El-Nhy"/>
<constraint firstAttribute="height" constant="30" id="Ju6-Bx-wC5"/>
</constraints>
<state key="normal" image="ScanBackwardButton"/>
<connections>
<action selector="rewindButtonWasPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="a5z-7z-Dbp"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0:00" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" enabled="NO" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lSE-mk-Glu" userLabel="StartTime">
<rect key="frame" x="220" y="530" width="50" height="20"/>
<constraints>
<constraint firstAttribute="height" constant="20" id="3FX-0e-O6P"/>
<constraint firstAttribute="width" constant="50" id="FxO-it-ceh"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="-:--" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" enabled="NO" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qOC-Kz-nZX" userLabel="Duration">
<rect key="frame" x="530" y="530" width="50" height="20"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="dHY-sI-1Zo"/>
<constraint firstAttribute="height" constant="20" id="vny-2p-5hq"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="zs2-tO-O9K" firstAttribute="leading" secondItem="lSE-mk-Glu" secondAttribute="leading" id="0Nl-Eq-0C9"/>
<constraint firstItem="AcP-gW-NjJ" firstAttribute="leading" secondItem="xrl-4x-Edh" secondAttribute="trailing" id="0Xi-o4-GPm"/>
<constraint firstItem="k2B-HR-6u1" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="8RV-Wu-cZ2"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="leading" secondItem="AcP-gW-NjJ" secondAttribute="trailing" constant="10" id="Djj-sA-9ve"/>
<constraint firstItem="qOC-Kz-nZX" firstAttribute="trailing" secondItem="zs2-tO-O9K" secondAttribute="trailing" id="Ny2-em-Hsb"/>
<constraint firstAttribute="trailingMargin" secondItem="k2B-HR-6u1" secondAttribute="trailing" constant="-20" id="SHT-LM-7fv"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="width" secondItem="lSE-mk-Glu" secondAttribute="width" id="UTA-oc-lgo"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="centerY" secondItem="AcP-gW-NjJ" secondAttribute="centerY" id="aUq-JI-udW"/>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="1Ph-2J-7JL" secondAttribute="bottom" constant="20" id="dYL-aD-qru"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="centerX" secondItem="lSE-mk-Glu" secondAttribute="centerX" id="eau-ex-HCT"/>
<constraint firstItem="k2B-HR-6u1" firstAttribute="top" secondItem="8bC-Xf-vdC" secondAttribute="topMargin" constant="70" id="fr7-hU-dZX"/>
<constraint firstItem="xrl-4x-Edh" firstAttribute="centerY" secondItem="1Ph-2J-7JL" secondAttribute="centerY" id="gSS-SM-9qW"/>
<constraint firstItem="k2B-HR-6u1" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="-20" id="hIA-sm-1WB"/>
<constraint firstItem="AcP-gW-NjJ" firstAttribute="centerY" secondItem="xrl-4x-Edh" secondAttribute="centerY" id="hnG-E0-mGw"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="trailing" secondItem="8bC-Xf-vdC" secondAttribute="trailingMargin" id="ill-9D-3bq"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="top" secondItem="lSE-mk-Glu" secondAttribute="bottom" id="jXv-uJ-vtR"/>
<constraint firstItem="1Ph-2J-7JL" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="-20" id="kCK-L8-VXv"/>
<constraint firstItem="zs2-tO-O9K" firstAttribute="top" secondItem="qOC-Kz-nZX" secondAttribute="bottom" id="mP2-9X-QOD"/>
<constraint firstItem="xrl-4x-Edh" firstAttribute="leading" secondItem="1Ph-2J-7JL" secondAttribute="trailing" id="nWe-M1-J3B"/>
<constraint firstItem="1Ph-2J-7JL" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="20" id="pTa-hD-fe7"/>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="k2B-HR-6u1" secondAttribute="bottom" constant="80" id="zi6-Fl-Tea"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="fr7-hU-dZX"/>
<exclude reference="pTa-hD-fe7"/>
<exclude reference="UTA-oc-lgo"/>
<exclude reference="eau-ex-HCT"/>
</mask>
</variation>
</view>
<connections>
<outlet property="durationLabel" destination="qOC-Kz-nZX" id="vdA-zn-17E"/>
<outlet property="fastForwardButton" destination="AcP-gW-NjJ" id="bVk-tK-Ap4"/>
<outlet property="playPauseButton" destination="xrl-4x-Edh" id="BeD-Na-Ocg"/>
<outlet property="playerView" destination="k2B-HR-6u1" id="w2i-er-gei"/>
<outlet property="rewindButton" destination="1Ph-2J-7JL" id="Fwp-1T-h4Q"/>
<outlet property="startTimeLabel" destination="lSE-mk-Glu" id="0k2-Ci-tDY"/>
<outlet property="timeSlider" destination="zs2-tO-O9K" id="BKu-N7-khJ"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="357" y="421"/>
</scene>
</scenes>
<resources>
<image name="PlayButton" width="21" height="22"/>
<image name="ScanBackwardButton" width="29" height="16"/>
<image name="ScanForwardButton" width="29" height="16"/>
</resources>
</document>

View File

@ -0,0 +1,68 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "PauseButton.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "PauseButton@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "PauseButton@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "PlayButton.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "PlayButton@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "PlayButton@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ScanBackwardButton.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ScanBackwardButton@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ScanBackwardButton@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ScanForwardButton.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ScanForwardButton@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ScanForwardButton@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,49 @@
<?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>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>Main</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</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>
</dict>
</plist>

View File

@ -0,0 +1,31 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Player view backed by an AVPlayerLayer.
*/
import UIKit
import AVFoundation
/// A simple `UIView` subclass that is backed by an `AVPlayerLayer` layer.
class PlayerView: UIView {
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
}

View File

@ -0,0 +1,359 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
View controller containing a player view and basic playback controls.
*/
import Foundation
import AVFoundation
import UIKit
/*
KVO context used to differentiate KVO callbacks for this class versus other
classes in its class hierarchy.
*/
private var playerViewControllerKVOContext = 0
class PlayerViewController: UIViewController {
// MARK: Properties
// Attempt load and test these asset keys before playing.
static let assetKeysRequiredToPlay = [
"playable",
"hasProtectedContent"
]
let player = AVPlayer()
var currentTime: Double {
get {
return CMTimeGetSeconds(player.currentTime())
}
set {
let newTime = CMTimeMakeWithSeconds(newValue, 1)
player.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
}
}
var duration: Double {
guard let currentItem = player.currentItem else { return 0.0 }
return CMTimeGetSeconds(currentItem.duration)
}
var rate: Float {
get {
return player.rate
}
set {
player.rate = newValue
}
}
var asset: AVURLAsset? {
didSet {
guard let newAsset = asset else { return }
asynchronouslyLoadURLAsset(newAsset)
}
}
private var playerLayer: AVPlayerLayer? {
return playerView.playerLayer
}
/*
A formatter for individual date components used to provide an appropriate
value for the `startTimeLabel` and `durationLabel`.
*/
let timeRemainingFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.zeroFormattingBehavior = .pad
formatter.allowedUnits = [.minute, .second]
return formatter
}()
/*
A token obtained from calling `player`'s `addPeriodicTimeObserverForInterval(_:queue:usingBlock:)`
method.
*/
private var timeObserverToken: Any?
private var playerItem: AVPlayerItem? = nil {
didSet {
/*
If needed, configure player item here before associating it with a player.
(example: adding outputs, setting text style rules, selecting media options)
*/
player.replaceCurrentItem(with: self.playerItem)
}
}
// MARK: - IBOutlets
@IBOutlet weak var timeSlider: UISlider!
@IBOutlet weak var startTimeLabel: UILabel!
@IBOutlet weak var durationLabel: UILabel!
@IBOutlet weak var rewindButton: UIButton!
@IBOutlet weak var playPauseButton: UIButton!
@IBOutlet weak var fastForwardButton: UIButton!
@IBOutlet weak var playerView: PlayerView!
// MARK: - View Controller
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
/*
Update the UI when these player properties change.
Use the context parameter to distinguish KVO for our particular observers
and not those destined for a subclass that also happens to be observing
these properties.
*/
addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.duration), options: [.new, .initial], context: &playerViewControllerKVOContext)
addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.rate), options: [.new, .initial], context: &playerViewControllerKVOContext)
addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.status), options: [.new, .initial], context: &playerViewControllerKVOContext)
playerView.playerLayer.player = player
let movieURL = Bundle.main.url(forResource: "ElephantSeals", withExtension: "mov")!
asset = AVURLAsset(url: movieURL, options: nil)
// Make sure we don't have a strong reference cycle by only capturing self as weak.
let interval = CMTimeMake(1, 1)
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main) { [unowned self] time in
let timeElapsed = Float(CMTimeGetSeconds(time))
self.timeSlider.value = Float(timeElapsed)
self.startTimeLabel.text = self.createTimeString(time: timeElapsed)
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if let timeObserverToken = timeObserverToken {
player.removeTimeObserver(timeObserverToken)
self.timeObserverToken = nil
}
player.pause()
removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.duration), context: &playerViewControllerKVOContext)
removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.rate), context: &playerViewControllerKVOContext)
removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.status), context: &playerViewControllerKVOContext)
}
// MARK: - Asset Loading
func asynchronouslyLoadURLAsset(_ newAsset: AVURLAsset) {
/*
Using AVAsset now runs the risk of blocking the current thread (the
main UI thread) whilst I/O happens to populate the properties. It's
prudent to defer our work until the properties we need have been loaded.
*/
newAsset.loadValuesAsynchronously(forKeys: PlayerViewController.assetKeysRequiredToPlay) {
/*
The asset invokes its completion handler on an arbitrary queue.
To avoid multiple threads using our internal state at the same time
we'll elect to use the main thread at all times, let's dispatch
our handler to the main queue.
*/
DispatchQueue.main.async {
/*
`self.asset` has already changed! No point continuing because
another `newAsset` will come along in a moment.
*/
guard newAsset == self.asset else { return }
/*
Test whether the values of each of the keys we need have been
successfully loaded.
*/
for key in PlayerViewController.assetKeysRequiredToPlay {
var error: NSError?
if newAsset.statusOfValue(forKey: key, error: &error) == .failed {
let stringFormat = NSLocalizedString("error.asset_key_%@_failed.description", comment: "Can't use this AVAsset because one of it's keys failed to load")
let message = String.localizedStringWithFormat(stringFormat, key)
self.handleErrorWithMessage(message, error: error)
return
}
}
// We can't play this asset.
if !newAsset.isPlayable || newAsset.hasProtectedContent {
let message = NSLocalizedString("error.asset_not_playable.description", comment: "Can't use this AVAsset because it isn't playable or has protected content")
self.handleErrorWithMessage(message)
return
}
/*
We can play this asset. Create a new `AVPlayerItem` and make
it our player's current item.
*/
self.playerItem = AVPlayerItem(asset: newAsset)
}
}
}
// MARK: - IBActions
@IBAction func playPauseButtonWasPressed(_ sender: UIButton) {
if player.rate != 1.0 {
// Not playing forward, so play.
if currentTime == duration {
// At end, so got back to begining.
currentTime = 0.0
}
player.play()
}
else {
// Playing, so pause.
player.pause()
}
}
@IBAction func rewindButtonWasPressed(_ sender: UIButton) {
// Rewind no faster than -2.0.
rate = max(player.rate - 2.0, -2.0)
}
@IBAction func fastForwardButtonWasPressed(_ sender: UIButton) {
// Fast forward no faster than 2.0.
rate = min(player.rate + 2.0, 2.0)
}
@IBAction func timeSliderDidChange(_ sender: UISlider) {
currentTime = Double(sender.value)
}
// MARK: - KVO Observation
// Update our UI when player or `player.currentItem` changes.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
// Make sure the this KVO callback was intended for this view controller.
guard context == &playerViewControllerKVOContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
if keyPath == #keyPath(PlayerViewController.player.currentItem.duration) {
// Update timeSlider and enable/disable controls when duration > 0.0
/*
Handle `NSNull` value for `NSKeyValueChangeNewKey`, i.e. when
`player.currentItem` is nil.
*/
let newDuration: CMTime
if let newDurationAsValue = change?[NSKeyValueChangeKey.newKey] as? NSValue {
newDuration = newDurationAsValue.timeValue
}
else {
newDuration = kCMTimeZero
}
let hasValidDuration = newDuration.isNumeric && newDuration.value != 0
let newDurationSeconds = hasValidDuration ? CMTimeGetSeconds(newDuration) : 0.0
let currentTime = hasValidDuration ? Float(CMTimeGetSeconds(player.currentTime())) : 0.0
timeSlider.maximumValue = Float(newDurationSeconds)
timeSlider.value = currentTime
rewindButton.isEnabled = hasValidDuration
playPauseButton.isEnabled = hasValidDuration
fastForwardButton.isEnabled = hasValidDuration
timeSlider.isEnabled = hasValidDuration
startTimeLabel.isEnabled = hasValidDuration
startTimeLabel.text = createTimeString(time: currentTime)
durationLabel.isEnabled = hasValidDuration
durationLabel.text = createTimeString(time: Float(newDurationSeconds))
}
else if keyPath == #keyPath(PlayerViewController.player.rate) {
// Update `playPauseButton` image.
let newRate = (change?[NSKeyValueChangeKey.newKey] as! NSNumber).doubleValue
let buttonImageName = newRate == 1.0 ? "PauseButton" : "PlayButton"
let buttonImage = UIImage(named: buttonImageName)
playPauseButton.setImage(buttonImage, for: UIControlState())
}
else if keyPath == #keyPath(PlayerViewController.player.currentItem.status) {
// Display an error if status becomes `.Failed`.
/*
Handle `NSNull` value for `NSKeyValueChangeNewKey`, i.e. when
`player.currentItem` is nil.
*/
let newStatus: AVPlayerItemStatus
if let newStatusAsNumber = change?[NSKeyValueChangeKey.newKey] as? NSNumber {
newStatus = AVPlayerItemStatus(rawValue: newStatusAsNumber.intValue)!
}
else {
newStatus = .unknown
}
if newStatus == .failed {
handleErrorWithMessage(player.currentItem?.error?.localizedDescription, error:player.currentItem?.error)
}
}
}
// Trigger KVO for anyone observing our properties affected by player and player.currentItem
override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
let affectedKeyPathsMappingByKey: [String: Set<String>] = [
"duration": [#keyPath(PlayerViewController.player.currentItem.duration)],
"rate": [#keyPath(PlayerViewController.player.rate)]
]
return affectedKeyPathsMappingByKey[key] ?? super.keyPathsForValuesAffectingValue(forKey: key)
}
// MARK: - Error Handling
func handleErrorWithMessage(_ message: String?, error: Error? = nil) {
NSLog("Error occured with message: \(message), error: \(error).")
let alertTitle = NSLocalizedString("alert.error.title", comment: "Alert title for errors")
let defaultAlertMessage = NSLocalizedString("error.default.description", comment: "Default error message when no NSError provided")
let alert = UIAlertController(title: alertTitle, message: message == nil ? defaultAlertMessage : message, preferredStyle: UIAlertControllerStyle.alert)
let alertActionTitle = NSLocalizedString("alert.error.actions.OK", comment: "OK on error alert")
let alertAction = UIAlertAction(title: alertActionTitle, style: .default, handler: nil)
alert.addAction(alertAction)
present(alert, animated: true, completion: nil)
}
// MARK: Convenience
func createTimeString(time: Float) -> String {
let components = NSDateComponents()
components.second = Int(max(0.0, time))
return timeRemainingFormatter.string(from: components as DateComponents)!
}
}

View File

@ -0,0 +1,22 @@
/*
<samplecode>
<abstract>
Localizable strings.
</abstract>
</samplecode>
*/
// Alert title for errors.
"alert.error.title" = "Error";
// OK action on error alert
"alert.error.actions.OK" = "OK on error alert";
// Can't use this AVAsset because one of it's keys failed to load.
"error.asset_key_%@_failed.description" = "Media failed to load key \"%@\"";
// Can't use this AVAsset because it isn't playable or has protected content.
"error.asset_not_playable.description" = "Media isn't playable or has protected content";
// Default error message when no NSError provided.
"error.default.description" = "Unknown error";