1 Commits

Author SHA1 Message Date
Freddy Kellison-Linn 56bef1c115 Release 1.3.0 2018-07-18 15:39:05 -04:00
22 changed files with 300 additions and 294 deletions
-50
View File
@@ -1,50 +0,0 @@
version: 2
jobs:
build-and-test:
environment:
BUNDLE_PATH: vendor/bundle
FL_OUTPUT_DIR: output
macos:
xcode: "10.1.0"
working_directory: ~/PlayerKit
shell: /bin/bash --login -o pipefail
steps:
- checkout
- restore_cache:
key: v1-gems-{{ checksum "Gemfile.lock" }}
- run:
name: Set Ruby version
command: echo "ruby-2.4" > ~/.ruby-version
- run:
name: Install bundler dependencies
command: bundle install --path vendor/bundle
- run:
name: Build and run tests
command: bundle exec fastlane ios test
environment:
SCAN_DEVICE: iPhone 8
SCAN_SCHEME: PlayerKit-Example
- save_cache:
paths:
- vendor/bundle
key: v1-gems-{{ checksum "Gemfile.lock" }}
- store_artifacts:
path: output
- store_test_results:
path: output/scan
workflows:
version: 2
build:
jobs:
- build-and-test
+1
View File
@@ -0,0 +1 @@
4.0
-42
View File
@@ -1,42 +0,0 @@
# Copyright © 2018 Vimeo. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# Run `swiftlint rules` for a list of available rules.
# Only the rules specified here will be enabled.
whitelist_rules:
- trailing_newline
- trailing_whitespace
- opening_brace
# Paths to include during linting.
included:
- ../PlayerKit/Classes
- ../Example
# Paths to exclude during linting.
excluded:
- Pods
# Configurable rules can be customized from this configuration file,
# binary rules can set their severity level.
trailing_whitespace:
ignores_empty_lines: true
+14 -19
View File
@@ -172,7 +172,6 @@
39DBB93200760D1BB486C4CC /* [CP] Check Pods Manifest.lock */,
607FACCC1AFB9204008FA782 /* Sources */,
607FACCD1AFB9204008FA782 /* Frameworks */,
0C1455B6219DDFB7006442F3 /* ShellScript */,
607FACCE1AFB9204008FA782 /* Resources */,
AEEB7FDEAB18BB5B2C3B7E51 /* [CP] Embed Pods Frameworks */,
);
@@ -217,12 +216,12 @@
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = 35R365FS4Q;
LastSwiftMigration = 1010;
LastSwiftMigration = 0900;
};
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = 35R365FS4Q;
LastSwiftMigration = 1010;
LastSwiftMigration = 0900;
TestTargetID = 607FACCF1AFB9204008FA782;
};
};
@@ -232,7 +231,6 @@
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
Base,
);
@@ -269,19 +267,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0C1455B6219DDFB7006442F3 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "WORKSPACE_ROOT=$( cd \"$(dirname \"${SRCROOT[0]}\")\" ; pwd -P )\n\nif which \"$PODS_ROOT\"/SwiftLint/swiftlint > /dev/null; then\n \"$PODS_ROOT\"/SwiftLint/swiftlint autocorrect --config \"$WORKSPACE_ROOT\"/.swiftlint.yml\nelse\n echo \"Warning: SwiftLint is not installed. Make sure you've run `bundle exec pod install`.\"\nfi\n";
};
39DBB93200760D1BB486C4CC /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -422,7 +407,7 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 3.0;
};
name = Debug;
};
@@ -462,7 +447,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 3.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
@@ -471,6 +456,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9D69DB7C6B7F626E6391E3FA /* Pods-PlayerKit_Example.debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = 35R365FS4Q;
INFOPLIST_FILE = PlayerKit/Info.plist;
@@ -478,6 +464,8 @@
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -486,6 +474,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = CF08222175BD5B0E142D343A /* Pods-PlayerKit_Example.release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = 35R365FS4Q;
INFOPLIST_FILE = PlayerKit/Info.plist;
@@ -493,6 +482,8 @@
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -511,6 +502,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
};
name = Debug;
};
@@ -524,6 +517,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
};
name = Release;
};
+2 -1
View File
@@ -13,7 +13,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = PlayerViewController()
self.window?.makeKeyAndVisible()
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.0.0</string>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
+28 -14
View File
@@ -10,8 +10,10 @@ import UIKit
import PlayerKit
import AVFoundation
class PlayerViewController: UIViewController, PlayerDelegate {
private struct Constants {
class PlayerViewController: UIViewController, PlayerDelegate
{
private struct Constants
{
static let VideoURL = URL(string: "https://github.com/vimeo/PlayerKit/blob/master/Example/PlayerKit/video.mp4?raw=true")!
}
@@ -22,7 +24,8 @@ class PlayerViewController: UIViewController, PlayerDelegate {
private let player = RegularPlayer()
override func viewDidLoad() {
override func viewDidLoad()
{
super.viewDidLoad()
player.delegate = self
@@ -34,7 +37,8 @@ class PlayerViewController: UIViewController, PlayerDelegate {
// MARK: Setup
private func addPlayerToView() {
private func addPlayerToView()
{
player.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
player.view.frame = self.view.bounds
self.view.insertSubview(player.view, at: 0)
@@ -42,11 +46,13 @@ class PlayerViewController: UIViewController, PlayerDelegate {
// MARK: Actions
@IBAction func didTapPlayButton() {
@IBAction func didTapPlayButton()
{
self.player.playing ? self.player.pause() : self.player.play()
}
@IBAction func didChangeSliderValue() {
@IBAction func didChangeSliderValue()
{
let value = Double(self.slider.value)
let time = value * self.player.duration
@@ -56,10 +62,12 @@ class PlayerViewController: UIViewController, PlayerDelegate {
// MARK: VideoPlayerDelegate
func playerDidUpdateState(player: Player, previousState: PlayerState) {
func playerDidUpdateState(player: Player, previousState: PlayerState)
{
self.activityIndicator.isHidden = true
switch player.state {
switch player.state
{
case .loading:
self.activityIndicator.isHidden = false
@@ -74,24 +82,30 @@ class PlayerViewController: UIViewController, PlayerDelegate {
}
}
func playerDidUpdatePlaying(player: Player) {
func playerDidUpdatePlaying(player: Player)
{
self.playButton.isSelected = player.playing
}
func playerDidUpdateTime(player: Player) {
guard player.duration > 0 else {
func playerDidUpdateTime(player: Player)
{
guard player.duration > 0 else
{
return
}
let ratio = player.time / player.duration
if self.slider.isHighlighted == false {
if self.slider.isHighlighted == false
{
self.slider.value = Float(ratio)
}
}
func playerDidUpdateBufferedTime(player: Player) {
guard player.duration > 0 else {
func playerDidUpdateBufferedTime(player: Player)
{
guard player.duration > 0 else
{
return
}
+6 -8
View File
@@ -1,12 +1,10 @@
use_frameworks!
platform :ios, '8.0'
target 'PlayerKit_Example' do
pod 'PlayerKit', :path => '../'
pod 'SwiftLint', '0.28.0'
target 'PlayerKit_Tests' do
inherit! :search_paths
end
pod 'PlayerKit', :path => '../'
target 'PlayerKit_Tests' do
inherit! :search_paths
end
end
+3 -10
View File
@@ -1,23 +1,16 @@
PODS:
- PlayerKit (1.3.0)
- SwiftLint (0.28.0)
- PlayerKit (1.2.0)
DEPENDENCIES:
- PlayerKit (from `../`)
- SwiftLint (= 0.28.0)
SPEC REPOS:
https://github.com/cocoapods/specs.git:
- SwiftLint
EXTERNAL SOURCES:
PlayerKit:
:path: "../"
SPEC CHECKSUMS:
PlayerKit: 16dbe4196fbd6cbc0e677026d13ffb5fb80276fb
SwiftLint: 088cfacb75b45970017e62b7524d506776d60148
PlayerKit: ff4e4cd4c216d60ebcfccf3a68772d6299373e5a
PODFILE CHECKSUM: 90f31c00641fb4d5e9453ba687a3a06a32cca83f
PODFILE CHECKSUM: 071d8819500a822237123321021901352f4d91a4
COCOAPODS: 1.5.2
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2.0.0</string>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
+1 -1
View File
@@ -5,6 +5,6 @@ gem 'danger', '4.0.4'
gem 'xcode-install', '2.1.0'
gem 'xcpretty-json-formatter', '0.1.0'
gem 'danger-xcode_summary', '0.1.0'
gem 'nokogiri', '1.8.5'
gem 'nokogiri', '1.6.0'
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval(File.read(plugins_path), binding) if File.exist?(plugins_path)
+4 -4
View File
@@ -162,7 +162,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_magick (4.5.1)
mini_portile2 (2.3.0)
mini_portile (0.5.3)
minitest (5.11.3)
molinillo (0.6.5)
multi_json (1.13.1)
@@ -171,8 +171,8 @@ GEM
nanaimo (0.2.5)
nap (1.1.0)
netrc (0.11.0)
nokogiri (1.8.5)
mini_portile2 (~> 2.3.0)
nokogiri (1.6.0)
mini_portile (~> 0.5.0)
octokit (4.9.0)
sawyer (~> 0.8.0, >= 0.5.3)
open4 (1.3.4)
@@ -235,7 +235,7 @@ DEPENDENCIES
danger-xcode_summary (= 0.1.0)
fastlane (= 2.42.0)
fastlane-plugin-pretty_junit
nokogiri (= 1.8.5)
nokogiri (= 1.6.0)
xcode-install (= 2.1.0)
xcpretty-json-formatter (= 0.1.0)
+1 -3
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'PlayerKit'
s.version = '2.0.0'
s.version = '1.3.0'
s.summary = 'A modular video player system.'
s.description = <<-DESC
@@ -15,8 +15,6 @@ Pod::Spec.new do |s|
s.ios.deployment_target = '8.0'
s.tvos.deployment_target = '9.0'
s.swift_version = "4.2"
s.source_files = 'PlayerKit/Classes/**/*'
end
+12 -6
View File
@@ -9,24 +9,30 @@
import Foundation
import AVFoundation
extension AVPlayer {
var errorForPlayerOrItem: NSError? {
extension AVPlayer
{
var errorForPlayerOrItem: NSError?
{
// First try to return the current item's error
if let error = self.currentItem?.error {
if let error = self.currentItem?.error
{
// If current item's error has an underlying error, return that
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError
{
return underlyingError
}
else {
else
{
return error as NSError?
}
}
// Otherwise, try to return the player error
if let error = self.error {
if let error = self.error
{
return error as NSError?
}
+6 -3
View File
@@ -9,9 +9,12 @@
import Foundation
import AVFoundation
extension CMTime {
var timeInterval: TimeInterval? {
if CMTIME_IS_INVALID(self) || CMTIME_IS_INDEFINITE(self) {
extension CMTime
{
var timeInterval: TimeInterval?
{
if CMTIME_IS_INVALID(self) || CMTIME_IS_INDEFINITE(self)
{
return nil
}
+32 -16
View File
@@ -10,7 +10,8 @@ import UIKit
import AVKit
/// A player error
public enum PlayerError: Int {
public enum PlayerError: Int
{
case unknown
case loading
@@ -19,8 +20,10 @@ public enum PlayerError: Int {
/// The associated error
///
/// - Returns: The error
public func error() -> NSError {
switch self {
public func error() -> NSError
{
switch self
{
case .unknown:
return NSError(domain: type(of: self).Domain, code: self.rawValue, userInfo: [NSLocalizedDescriptionKey: "An unknown error occurred."])
@@ -37,14 +40,16 @@ public enum PlayerError: Int {
/// - loading: The player is loading or buffering
/// - ready: The player is ready for playback
/// - failed: The player has failed
@objc public enum PlayerState: Int {
@objc public enum PlayerState: Int
{
case loading
case ready
case failed
}
/// An object that adopts the PlayerDelegate protocol can receive updates from the player.
@objc public protocol PlayerDelegate: class {
@objc public protocol PlayerDelegate: class
{
func playerDidUpdateState(player: Player, previousState: PlayerState)
func playerDidUpdatePlaying(player: Player)
func playerDidUpdateTime(player: Player)
@@ -52,7 +57,8 @@ public enum PlayerError: Int {
}
/// An object that adopts the Player protocol is responsible for implementing the API and calling PlayerDelegate methods where appropriate.
@objc public protocol Player: class {
@objc public protocol Player: class
{
weak var delegate: PlayerDelegate? { get set }
var state: PlayerState { get }
@@ -82,34 +88,40 @@ public enum PlayerError: Int {
// MARK: Identity Protocols
/// A player that adopts the ProvidesView protocol is capable of providing a view to be added to a view hierarchy.
@objc public protocol ProvidesView {
@objc public protocol ProvidesView
{
var view: UIView { get }
}
// MARK: Capability Protocols
/// A player that adopts the ProvidesView protocol is capable of AirPlay playback.
@objc public protocol AirPlayCapable {
@objc public protocol AirPlayCapable
{
var isAirPlayEnabled: Bool { get set }
}
/// A player that adopts the ProvidesView protocol is capable of setting audio volume.
@objc public protocol VolumeCapable {
@objc public protocol VolumeCapable
{
var volume: Float { get set }
}
/// A player that adopts the ProvidesView protocol is capable of setting the video fill mode.
@objc public protocol FillModeCapable {
@objc public protocol FillModeCapable
{
var fillMode: FillMode { get set }
}
@objc public enum FillMode: Int {
@objc public enum FillMode: Int
{
case fit
case fill
}
/// The metadata that should be attached to any type of text track.
@objc public protocol TextTrackMetadata {
@objc public protocol TextTrackMetadata
{
var displayName: String { get }
var locale: Locale? { get }
// Indicates that the text track represents subtitles for the def and hard of hearing (SDH).
@@ -118,14 +130,17 @@ public enum PlayerError: Int {
@objc(displayNameWithLocale:) func displayName(with locale: Locale) -> String
}
extension TextTrackMetadata {
public func matches(_ other: TextTrackMetadata) -> Bool {
extension TextTrackMetadata
{
public func matches(_ other: TextTrackMetadata) -> Bool
{
return (self.locale == other.locale && self.isSDHTrack == other.isSDHTrack)
}
}
/// A player that conforms to the TextTrackCapable protocol is capable of advertising and displaying text tracks.
@objc public protocol TextTrackCapable {
@objc public protocol TextTrackCapable
{
var selectedTextTrack: TextTrackMetadata? { get }
var availableTextTracks: [TextTrackMetadata] { get }
@@ -135,7 +150,8 @@ extension TextTrackMetadata {
#if os(iOS)
/// A player that adopts the ProvidesView protocol is capable of Picture in Picture playback.
@objc public protocol PictureInPictureCapable {
@objc public protocol PictureInPictureCapable
{
@available(iOS 9.0, *)
var pictureInPictureController: AVPictureInPictureController? { get }
}
+162 -82
View File
@@ -11,15 +11,19 @@ import Foundation
import AVFoundation
import AVKit
extension AVMediaSelectionOption: TextTrackMetadata {
public var isSDHTrack: Bool {
extension AVMediaSelectionOption: TextTrackMetadata
{
public var isSDHTrack: Bool
{
return self.hasMediaCharacteristic(.describesMusicAndSoundForAccessibility) && self.hasMediaCharacteristic(.transcribesSpokenDialogForAccessibility)
}
}
/// A RegularPlayer is used to play regular videos.
@objc open class RegularPlayer: NSObject, Player, ProvidesView {
public struct Constants {
@objc open class RegularPlayer: NSObject, Player, ProvidesView
{
public struct Constants
{
public static let TimeUpdateInterval: TimeInterval = 0.1
}
@@ -32,10 +36,12 @@ extension AVMediaSelectionOption: TextTrackMetadata {
/// Sets an AVAsset on the player.
///
/// - Parameter asset: The AVAsset
@objc open func set(_ asset: AVAsset) {
@objc open func set(_ asset: AVAsset)
{
// Prepare the old item for removal
if let currentItem = self.player.currentItem {
if let currentItem = self.player.currentItem
{
self.removePlayerItemObservers(fromPlayerItem: currentItem)
}
@@ -50,27 +56,33 @@ extension AVMediaSelectionOption: TextTrackMetadata {
// MARK: ProvidesView
private class RegularPlayerView: UIView {
var playerLayer: AVPlayerLayer {
private class RegularPlayerView: UIView
{
var playerLayer: AVPlayerLayer
{
return self.layer as! AVPlayerLayer
}
override class var layerClass: AnyClass {
override class var layerClass: AnyClass
{
return AVPlayerLayer.self
}
func configureForPlayer(player: AVPlayer) {
func configureForPlayer(player: AVPlayer)
{
(self.layer as! AVPlayerLayer).player = player
}
}
public let view: UIView = RegularPlayerView(frame: .zero)
private var regularPlayerView: RegularPlayerView {
private var regularPlayerView: RegularPlayerView
{
return self.view as! RegularPlayerView
}
private var playerLayer: AVPlayerLayer {
private var playerLayer: AVPlayerLayer
{
return self.regularPlayerView.playerLayer
}
@@ -78,55 +90,68 @@ extension AVMediaSelectionOption: TextTrackMetadata {
weak public var delegate: PlayerDelegate?
public private(set) var state: PlayerState = .ready {
didSet {
public private(set) var state: PlayerState = .ready
{
didSet
{
self.delegate?.playerDidUpdateState(player: self, previousState: oldValue)
}
}
public var duration: TimeInterval {
public var duration: TimeInterval
{
return self.player.currentItem?.duration.timeInterval ?? 0
}
public private(set) var time: TimeInterval = 0 {
didSet {
public private(set) var time: TimeInterval = 0
{
didSet
{
self.delegate?.playerDidUpdateTime(player: self)
}
}
public private(set) var bufferedTime: TimeInterval = 0 {
didSet {
public private(set) var bufferedTime: TimeInterval = 0
{
didSet
{
self.delegate?.playerDidUpdateBufferedTime(player: self)
}
}
public var playing: Bool {
public var playing: Bool
{
return self.player.rate > 0
}
public var error: NSError? {
public var error: NSError?
{
return self.player.errorForPlayerOrItem
}
public func seek(to time: TimeInterval) {
let cmTime = CMTimeMakeWithSeconds(time, preferredTimescale: Int32(NSEC_PER_SEC))
public func seek(to time: TimeInterval)
{
let cmTime = CMTimeMakeWithSeconds(time, Int32(NSEC_PER_SEC))
self.player.seek(to: cmTime)
self.time = time
}
public func play() {
public func play()
{
self.player.play()
}
public func pause() {
public func pause()
{
self.player.pause()
}
// MARK: Lifecycle
public override init() {
public override init()
{
super.init()
self.addPlayerObservers()
@@ -136,8 +161,10 @@ extension AVMediaSelectionOption: TextTrackMetadata {
self.setupAirplay()
}
deinit {
if let playerItem = self.player.currentItem {
deinit
{
if let playerItem = self.player.currentItem
{
self.removePlayerItemObservers(fromPlayerItem: playerItem)
}
@@ -146,18 +173,22 @@ extension AVMediaSelectionOption: TextTrackMetadata {
// MARK: Setup
private func setupAirplay() {
private func setupAirplay()
{
self.player.usesExternalPlaybackWhileExternalScreenIsActive = true
}
// MARK: Observers
private struct KeyPath {
struct Player {
private struct KeyPath
{
struct Player
{
static let Rate = "rate"
}
struct PlayerItem {
struct PlayerItem
{
static let Status = "status"
static let PlaybackLikelyToKeepUp = "playbackLikelyToKeepUp"
static let LoadedTimeRanges = "loadedTimeRanges"
@@ -166,35 +197,41 @@ extension AVMediaSelectionOption: TextTrackMetadata {
private var playerTimeObserver: Any?
private func addPlayerItemObservers(toPlayerItem playerItem: AVPlayerItem) {
private func addPlayerItemObservers(toPlayerItem playerItem: AVPlayerItem)
{
playerItem.addObserver(self, forKeyPath: KeyPath.PlayerItem.Status, options: [.initial, .new], context: nil)
playerItem.addObserver(self, forKeyPath: KeyPath.PlayerItem.PlaybackLikelyToKeepUp, options: [.initial, .new], context: nil)
playerItem.addObserver(self, forKeyPath: KeyPath.PlayerItem.LoadedTimeRanges, options: [.initial, .new], context: nil)
}
private func removePlayerItemObservers(fromPlayerItem playerItem: AVPlayerItem) {
private func removePlayerItemObservers(fromPlayerItem playerItem: AVPlayerItem)
{
playerItem.removeObserver(self, forKeyPath: KeyPath.PlayerItem.Status, context: nil)
playerItem.removeObserver(self, forKeyPath: KeyPath.PlayerItem.PlaybackLikelyToKeepUp, context: nil)
playerItem.removeObserver(self, forKeyPath: KeyPath.PlayerItem.LoadedTimeRanges, context: nil)
}
private func addPlayerObservers() {
private func addPlayerObservers()
{
self.player.addObserver(self, forKeyPath: KeyPath.Player.Rate, options: [.initial, .new], context: nil)
let interval = CMTimeMakeWithSeconds(Constants.TimeUpdateInterval, preferredTimescale: Int32(NSEC_PER_SEC))
let interval = CMTimeMakeWithSeconds(Constants.TimeUpdateInterval, Int32(NSEC_PER_SEC))
self.playerTimeObserver = self.player.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { [weak self] (cmTime) in
if let strongSelf = self, let time = cmTime.timeInterval {
if let strongSelf = self, let time = cmTime.timeInterval
{
strongSelf.time = time
}
})
}
private func removePlayerObservers() {
private func removePlayerObservers()
{
self.player.removeObserver(self, forKeyPath: KeyPath.Player.Rate, context: nil)
if let playerTimeObserver = self.playerTimeObserver {
if let playerTimeObserver = self.playerTimeObserver
{
self.player.removeTimeObserver(playerTimeObserver)
self.playerTimeObserver = nil
@@ -203,44 +240,56 @@ extension AVMediaSelectionOption: TextTrackMetadata {
// MARK: Observation
override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
{
// Player Item Observers
if keyPath == KeyPath.PlayerItem.Status {
if let statusInt = change?[.newKey] as? Int, let status = AVPlayerItem.Status(rawValue: statusInt) {
if keyPath == KeyPath.PlayerItem.Status
{
if let statusInt = change?[.newKey] as? Int, let status = AVPlayerItemStatus(rawValue: statusInt)
{
self.playerItemStatusDidChange(status: status)
}
}
else if keyPath == KeyPath.PlayerItem.PlaybackLikelyToKeepUp {
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool {
else if keyPath == KeyPath.PlayerItem.PlaybackLikelyToKeepUp
{
if let playbackLikelyToKeepUp = change?[.newKey] as? Bool
{
self.playerItemPlaybackLikelyToKeepUpDidChange(playbackLikelyToKeepUp: playbackLikelyToKeepUp)
}
}
else if keyPath == KeyPath.PlayerItem.LoadedTimeRanges {
if let loadedTimeRanges = change?[.newKey] as? [NSValue] {
else if keyPath == KeyPath.PlayerItem.LoadedTimeRanges
{
if let loadedTimeRanges = change?[.newKey] as? [NSValue]
{
self.playerItemLoadedTimeRangesDidChange(loadedTimeRanges: loadedTimeRanges)
}
}
// Player Observers
else if keyPath == KeyPath.Player.Rate {
if let rate = change?[.newKey] as? Float {
else if keyPath == KeyPath.Player.Rate
{
if let rate = change?[.newKey] as? Float
{
self.playerRateDidChange(rate: rate)
}
}
// Fall Through Observers
else {
else
{
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
// MARK: Observation Helpers
private func playerItemStatusDidChange(status: AVPlayerItem.Status) {
switch status {
private func playerItemStatusDidChange(status: AVPlayerItemStatus)
{
switch status
{
case .unknown:
self.state = .loading
@@ -255,18 +304,22 @@ extension AVMediaSelectionOption: TextTrackMetadata {
}
}
private func playerRateDidChange(rate: Float) {
private func playerRateDidChange(rate: Float)
{
self.delegate?.playerDidUpdatePlaying(player: self)
}
private func playerItemPlaybackLikelyToKeepUpDidChange(playbackLikelyToKeepUp: Bool) {
private func playerItemPlaybackLikelyToKeepUpDidChange(playbackLikelyToKeepUp: Bool)
{
let state: PlayerState = playbackLikelyToKeepUp ? .ready : .loading
self.state = state
}
private func playerItemLoadedTimeRangesDidChange(loadedTimeRanges: [NSValue]) {
guard let bufferedCMTime = loadedTimeRanges.first?.timeRangeValue.end, let bufferedTime = bufferedCMTime.timeInterval else {
private func playerItemLoadedTimeRangesDidChange(loadedTimeRanges: [NSValue])
{
guard let bufferedCMTime = loadedTimeRanges.first?.timeRangeValue.end, let bufferedTime = bufferedCMTime.timeInterval else
{
return
}
@@ -287,20 +340,25 @@ extension AVMediaSelectionOption: TextTrackMetadata {
extension RegularPlayer: AirPlayCapable
{
public var isAirPlayEnabled: Bool {
get {
public var isAirPlayEnabled: Bool
{
get
{
return self.player.allowsExternalPlayback
}
set {
set
{
return self.player.allowsExternalPlayback = newValue
}
}
}
#if os(iOS)
extension RegularPlayer: PictureInPictureCapable {
extension RegularPlayer: PictureInPictureCapable
{
@available(iOS 9.0, *)
public var pictureInPictureController: AVPictureInPictureController? {
public var pictureInPictureController: AVPictureInPictureController?
{
return self._pictureInPictureController
}
}
@@ -308,27 +366,35 @@ extension RegularPlayer: PictureInPictureCapable {
extension RegularPlayer: VolumeCapable
{
public var volume: Float {
get {
public var volume: Float
{
get
{
return self.player.volume
}
set {
set
{
self.player.volume = newValue
}
}
}
extension RegularPlayer: FillModeCapable {
public var fillMode: FillMode {
get {
extension RegularPlayer: FillModeCapable
{
public var fillMode: FillMode
{
get
{
let gravity = (self.view.layer as! AVPlayerLayer).videoGravity
return gravity == .resizeAspect ? .fit : .fill
}
set {
set
{
let gravity: AVLayerVideoGravity
switch newValue {
switch newValue
{
case .fit:
gravity = .resizeAspect
@@ -343,48 +409,62 @@ extension RegularPlayer: FillModeCapable {
}
}
extension RegularPlayer: TextTrackCapable {
public var selectedTextTrack: TextTrackMetadata? {
guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else {
extension RegularPlayer: TextTrackCapable
{
public var selectedTextTrack: TextTrackMetadata?
{
guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else
{
return nil
}
if #available(iOS 9.0, *) {
if #available(iOS 9.0, *)
{
return self.player.currentItem?.currentMediaSelection.selectedMediaOption(in: group)
}
else {
else
{
return self.player.currentItem?.selectedMediaOption(in: group)
}
}
public var availableTextTracks: [TextTrackMetadata] {
guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else {
public var availableTextTracks: [TextTrackMetadata]
{
guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else
{
return []
}
return group.options
}
public func fetchTextTracks(completion: @escaping ([TextTrackMetadata], TextTrackMetadata?) -> Void) {
public func fetchTextTracks(completion: @escaping ([TextTrackMetadata], TextTrackMetadata?) -> Void)
{
self.player.currentItem?.asset.loadValuesAsynchronously(forKeys: [#keyPath(AVAsset.availableMediaCharacteristicsWithMediaSelectionOptions)]) { [weak self] in
guard let strongSelf = self, let group = strongSelf.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else {
guard let strongSelf = self, let group = strongSelf.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else
{
completion([], nil)
return
}
if #available(iOS 9.0, *) {
if #available(iOS 9.0, *)
{
completion(group.options, strongSelf.player.currentItem?.currentMediaSelection.selectedMediaOption(in: group))
}
else {
else
{
completion(group.options, strongSelf.player.currentItem?.selectedMediaOption(in: group))
}
}
}
public func select(_ textTrack: TextTrackMetadata?) {
guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else {
public func select(_ textTrack: TextTrackMetadata?)
{
guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else
{
return
}
guard let track = textTrack else {
guard let track = textTrack else
{
self.player.currentItem?.select(nil, in: group)
return
}
+3 -3
View File
@@ -23,7 +23,7 @@ To run the example project, clone the repo, and run `pod install` from the Examp
## Minimum Requirements
- iOS 8.0 / tvOS 9.0
- Swift 4.2
- Swift 3.2
## Installation
@@ -62,9 +62,9 @@ player.delegate = delegate
You can create your own players by creating objects that conform to the Player protocol and call the delegate methods when appropriate.
## Questions?
## Author
Post on [Stackoverflow](http://stackoverflow.com/questions/tagged/vimeo-ios) with the tag `vimeo-ios`. Get in touch [here](https://vimeo.com/help/contact). Interested in working at Vimeo? We're [hiring](https://vimeo.com/jobs)!
Gavin King, gavin@vimeo.com
## License
+1
View File
@@ -0,0 +1 @@
Example/Pods/Pods.xcodeproj
+19
View File
@@ -0,0 +1,19 @@
general:
artifacts:
- "build/Logs"
- "PlayerKit_Example-PlayerKit-Example.log"
machine:
xcode:
version: 9.0
dependencies:
override:
- bundle install --deployment
cache_directories:
- "~/.cocoapods"
- "Example/Pods"
- "vendor/bundle"
test:
override:
- bundle exec fastlane test
+2 -23
View File
@@ -17,10 +17,10 @@ default_platform :ios
platform :ios do
before_all do
ensure_xcode_version(version: "10.1")
ensure_xcode_version(version: "9.0.1")
end
desc "Run tests"
desc "run tests"
lane :test do |options|
cocoapods(
@@ -43,27 +43,6 @@ platform :ios do
end
end
desc "bumps the project and podspec version"
lane :version_bump do |options|
bump_type = options[:bump_type]
version_number = options[:version_number]
if bump_type.nil? && version_number.nil?
UI.user_error!("version_bump requires you to provide a bump_type [patch|minor|major] or specific version_number. Please try again.")
end
increment_version_number(
bump_type: bump_type,
version_number: version_number,
xcodeproj: "Example/PlayerKit.xcodeproj"
)
version_bump_podspec(
bump_type: bump_type,
version_number: version_number
)
end
after_all do |lane|
end
+1 -7
View File
@@ -27,19 +27,13 @@ xcode-select --install
<td width="33%"><code>sudo gem install fastlane -NV</code></td>
</tr>
</table>
# Available Actions
## iOS
### ios test
```
fastlane ios test
```
Run tests
### ios version_bump
```
fastlane ios version_bump
```
bumps the project and podspec version
run tests
----