diff --git a/VideoViewController/TimelineView.swift b/VideoViewController/TimelineView.swift index 112caea..03e2a72 100644 --- a/VideoViewController/TimelineView.swift +++ b/VideoViewController/TimelineView.swift @@ -1,100 +1,142 @@ +// TimelineView.swift // -// TimelineView.swift -// VideoViewControllerExample +// Copyright (c) 2016 Danil Gontovnik (http://gontovnik.com/) // -// Created by Danil Gontovnik on 1/4/16. -// Copyright © 2016 Danil Gontovnik. 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. import UIKit -class TimelineView: UIView { +public class TimelineView: UIView { // MARK: - Vars - var duration: Double = 0.0 { + /// The duration of the video in seconds. + public var duration: NSTimeInterval = 0.0 { didSet { setNeedsDisplay() } } - var initialTime: Double = 0.0 { + /// Time in seconds when rewind began. + public var initialTime: NSTimeInterval = 0.0 { didSet { currentTime = initialTime } } - var currentTime: Double = 0.0 { + /// Current timeline time in seconds. + public var currentTime: NSTimeInterval = 0.0 { didSet { setNeedsDisplay() currentTimeDidChange?(currentTime) } } + /// Internal zoom variable. private var _zoom: CGFloat = 1.0 { didSet { setNeedsDisplay() } } - var zoom: CGFloat { + /// The zoom of the timeline view. The higher zoom value, the more accurate rewind is. Default is 1.0. + public var zoom: CGFloat { get { return _zoom } set { _zoom = max(min(newValue, maxZoom), minZoom) } } - var minZoom: CGFloat = 1.0 { + /// Indicates minimum zoom value. Default is 1.0. + public var minZoom: CGFloat = 1.0 { didSet { zoom = _zoom } } - var maxZoom: CGFloat = 3.5 { + /// Indicates maximum zoom value. Default is 3.5. + public var maxZoom: CGFloat = 3.5 { didSet { zoom = _zoom } } - var intervalWidth: CGFloat = 24.0 { + /// The width of a line representing a specific time interval on a timeline. If zoom is not equal 1, then actual interval width equals to intervalWidth * zoom. Value will be used during rewind for calculations — for example, if zoom is 1, intervalWidth is 30 and intervalDuration is 15, then when user moves 10pixels left or right we will rewind by +5 or -5 seconds; + public var intervalWidth: CGFloat = 24.0 { didSet { setNeedsDisplay() } } - var intervalDuration: CGFloat = 15.0 { + /// The duration of an interval in seconds. If video is 55 seconds and interval is 15 seconds — then we will have 3 full intervals and one not full interval. Value will be used during rewind for calculations. + public var intervalDuration: CGFloat = 15.0 { didSet { setNeedsDisplay() } } - var currentTimeDidChange: ((Double) -> ())? + /// Block which will be triggered everytime currentTime value changes. + public var currentTimeDidChange: ((NSTimeInterval) -> ())? // MARK: - Constructors - init() { + public init() { super.init(frame: .zero) opaque = false } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Methods - func currentIntervalWidth() -> CGFloat { + /** + Calculate current interval width. It takes two variables in count - intervalWidth and zoom. + */ + private func currentIntervalWidth() -> CGFloat { return intervalWidth * zoom } - func durationFromWidth(width: CGFloat) -> Double { - return Double(width * intervalDuration / currentIntervalWidth()) + /** + Calculates time interval in seconds from passed width. + + - Parameter width: The distance. + */ + public func timeIntervalFromDistance(distance: CGFloat) -> NSTimeInterval { + return NSTimeInterval(distance * intervalDuration / currentIntervalWidth()) } - func widthFromDuration(duration: Double) -> CGFloat { - return currentIntervalWidth() * CGFloat(duration) / intervalDuration + /** + Calculates distance from given time interval. + + - Parameter duration: The duration of an interval. + */ + public func distanceFromTimeInterval(timeInterval: NSTimeInterval) -> CGFloat { + return currentIntervalWidth() * CGFloat(timeInterval) / intervalDuration } - func rewindByWidth(width: CGFloat) { - let newCurrentTime = currentTime + durationFromWidth(width) + /** + Rewinds by distance. Calculates interval width and adds it to the current time. + + - Parameter distance: The distance how far it should rewind by. + */ + public func rewindByDistance(distance: CGFloat) { + let newCurrentTime = currentTime + timeIntervalFromDistance(distance) currentTime = max(min(newCurrentTime, duration), 0.0) } // MARK: - Draw - override func drawRect(rect: CGRect) { + override public func drawRect(rect: CGRect) { super.drawRect(rect) let intervalWidth = currentIntervalWidth() - let originX: CGFloat = bounds.width / 2.0 - widthFromDuration(currentTime) + let originX: CGFloat = bounds.width / 2.0 - distanceFromTimeInterval(currentTime) let context = UIGraphicsGetCurrentContext() let lineHeight: CGFloat = 5.0 @@ -111,12 +153,12 @@ class TimelineView: UIView { // Draw elapsed line CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor) - let elapsedPath = UIBezierPath(roundedRect: CGRect(x: originX, y: 0.0, width: widthFromDuration(currentTime), height: lineHeight), cornerRadius: lineHeight).CGPath + let elapsedPath = UIBezierPath(roundedRect: CGRect(x: originX, y: 0.0, width: distanceFromTimeInterval(currentTime), height: lineHeight), cornerRadius: lineHeight).CGPath CGContextAddPath(context, elapsedPath) CGContextFillPath(context) // Draw current time dot - CGContextFillEllipseInRect(context, CGRect(x: originX + widthFromDuration(initialTime), y: 7.0, width: 3.0, height: 3.0)) + CGContextFillEllipseInRect(context, CGRect(x: originX + distanceFromTimeInterval(initialTime), y: 7.0, width: 3.0, height: 3.0)) // Draw full line separators CGContextSetFillColorWithColor(context, UIColor(white: 0.0, alpha: 0.5).CGColor) diff --git a/VideoViewController/VideoViewController.swift b/VideoViewController/VideoViewController.swift index 31e410f..5d92901 100644 --- a/VideoViewController/VideoViewController.swift +++ b/VideoViewController/VideoViewController.swift @@ -1,15 +1,29 @@ +// VideoViewController.swift // -// VideoViewController.swift -// VideoViewControllerExample +// Copyright (c) 2016 Danil Gontovnik (http://gontovnik.com/) // -// Created by Danil Gontovnik on 1/4/16. -// Copyright © 2016 Danil Gontovnik. 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. import UIKit import AVFoundation -class VideoViewController: UIViewController { +public class VideoViewController: UIViewController { // MARK: - Vars @@ -27,45 +41,49 @@ class VideoViewController: UIViewController { private let rewindDimView = UIVisualEffectView() private let rewindContentView = UIView() - private let rewindTimelineView = TimelineView() - + public let rewindTimelineView = TimelineView() private let rewindPreviewShadowLayer = CALayer() private let rewindPreviewImageView = UIImageView() private let rewindCurrentTimeLabel = UILabel() - var rewindPreviewMaxHeight: CGFloat = 112.0 { + /// Indicates the maximum height of rewindPreviewImageView. Default value is 112. + public var rewindPreviewMaxHeight: CGFloat = 112.0 { didSet { assetGenerator.maximumSize = CGSize(width: CGFloat.max, height: rewindPreviewMaxHeight * UIScreen.mainScreen().scale) } } + + /// Indicates whether player should start playing on viewDidLoad. Default is true. + public var autoplays: Bool = true // MARK: - Constructors - init(videoURL: NSURL) { + /** + Returns an initialized VideoViewController object + + - Parameter videoURL: Local URL to the video asset + */ + public init(videoURL: NSURL) { super.init(nibName: nil, bundle: nil) self.videoURL = videoURL asset = AVURLAsset(URL: videoURL) - playerItem = AVPlayerItem(asset: asset) - player = AVPlayer(playerItem: playerItem) - player.actionAtItemEnd = .None - playerLayer = AVPlayerLayer(player: player) - + assetGenerator = AVAssetImageGenerator(asset: asset) assetGenerator.maximumSize = CGSize(width: CGFloat.max, height: rewindPreviewMaxHeight * UIScreen.mainScreen().scale) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - - override func loadView() { + override public func loadView() { super.loadView() view.backgroundColor = .blackColor() @@ -95,7 +113,10 @@ class VideoViewController: UIViewController { dispatch_async(dispatch_get_main_queue()) { strongSelf.rewindPreviewImageView.image = image - strongSelf.layoutRewindPreviewImageViewIfNeeded() + + if strongSelf.rewindPreviewImageView.bounds.size != image.size { + strongSelf.viewWillLayoutSubviews() + } } } } @@ -121,15 +142,27 @@ class VideoViewController: UIViewController { rewindContentView.addSubview(rewindPreviewImageView) } - override func viewDidLoad() { + override public func viewDidLoad() { super.viewDidLoad() - player.play() + if autoplays { + play() + } } // MARK: - Methods - func longPressed(gesture: UILongPressGestureRecognizer) { + /// Resumes playback + public func play() { + player.play() + } + + /// Pauses playback + public func pause() { + player.pause() + } + + public func longPressed(gesture: UILongPressGestureRecognizer) { let location = gesture.locationInView(gesture.view!) rewindTimelineView.zoom = (location.y - rewindTimelineView.center.y - 10.0) / 30.0 @@ -141,7 +174,7 @@ class VideoViewController: UIViewController { self.rewindContentView.alpha = 1.0 }, completion: nil) } else if gesture.state == .Changed { - rewindTimelineView.rewindByWidth(previousLocationX - location.x) + rewindTimelineView.rewindByDistance(previousLocationX - location.x) } else { player.play() @@ -159,13 +192,13 @@ class VideoViewController: UIViewController { } } - override func prefersStatusBarHidden() -> Bool { + override public func prefersStatusBarHidden() -> Bool { return true } // MARK: - Layout - override func viewWillLayoutSubviews() { + override public func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() playerLayer.frame = view.bounds @@ -180,18 +213,8 @@ class VideoViewController: UIViewController { rewindPreviewImageView.frame = CGRect(x: (rewindContentView.bounds.width - rewindPreviewImageViewWidth) / 2.0, y: (rewindContentView.bounds.height - rewindPreviewMaxHeight - verticalSpacing - rewindCurrentTimeLabel.bounds.height - verticalSpacing - timelineHeight) / 2.0, width: rewindPreviewImageViewWidth, height: rewindPreviewMaxHeight) rewindCurrentTimeLabel.frame = CGRect(x: 0.0, y: rewindPreviewImageView.frame.maxY + verticalSpacing, width: rewindTimelineView.bounds.width, height: rewindCurrentTimeLabel.frame.height) rewindTimelineView.frame = CGRect(x: 0.0, y: rewindCurrentTimeLabel.frame.maxY + verticalSpacing, width: rewindContentView.bounds.width, height: timelineHeight) - - layoutRewindPreviewImageViewIfNeeded() - } - - private func layoutRewindPreviewImageViewIfNeeded() { - guard let image = rewindPreviewImageView.image where rewindPreviewImageView.bounds.size != image.size else { - return - } - - rewindPreviewImageView.frame = CGRect(x: (rewindContentView.bounds.width - image.size.width) / 2.0, y: rewindPreviewImageView.frame.minY, width: image.size.width, height: rewindPreviewImageView.bounds.height) rewindPreviewShadowLayer.frame = rewindPreviewImageView.frame - + let path = UIBezierPath(roundedRect: rewindPreviewImageView.bounds, cornerRadius: 5.0).CGPath rewindPreviewShadowLayer.shadowPath = path (rewindPreviewImageView.layer.mask as! CAShapeLayer).path = path diff --git a/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/project.pbxproj b/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/project.pbxproj index e888c1b..27e1ec3 100644 --- a/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/project.pbxproj +++ b/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/project.pbxproj @@ -13,8 +13,8 @@ 05F686971C3B1695000CDF62 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 05F686961C3B1695000CDF62 /* Assets.xcassets */; }; 05F6869A1C3B1695000CDF62 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 05F686981C3B1695000CDF62 /* LaunchScreen.storyboard */; }; 05F686A61C3B1A28000CDF62 /* exampleVideo.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 05F686A51C3B1A28000CDF62 /* exampleVideo.mp4 */; }; - 05F686AC1C3BF770000CDF62 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F686AA1C3BF770000CDF62 /* TimelineView.swift */; }; - 05F686AD1C3BF770000CDF62 /* VideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F686AB1C3BF770000CDF62 /* VideoViewController.swift */; }; + F00876D81C4D12F500C099E8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00876D61C4D12F500C099E8 /* TimelineView.swift */; }; + F00876D91C4D12F500C099E8 /* VideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00876D71C4D12F500C099E8 /* VideoViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -26,8 +26,8 @@ 05F686991C3B1695000CDF62 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 05F6869B1C3B1695000CDF62 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 05F686A51C3B1A28000CDF62 /* exampleVideo.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = exampleVideo.mp4; sourceTree = ""; }; - 05F686AA1C3BF770000CDF62 /* TimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; - 05F686AB1C3BF770000CDF62 /* VideoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoViewController.swift; sourceTree = ""; }; + F00876D61C4D12F500C099E8 /* TimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; + F00876D71C4D12F500C099E8 /* VideoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -60,7 +60,7 @@ 05F6868E1C3B1695000CDF62 /* VideoViewControllerExample */ = { isa = PBXGroup; children = ( - 05F686A91C3BF770000CDF62 /* VideoViewController */, + F00876D51C4D12F500C099E8 /* VideoViewController */, 05F686A41C3B1A28000CDF62 /* Resources */, 05F6868F1C3B1695000CDF62 /* AppDelegate.swift */, 05F686911C3B1695000CDF62 /* ViewController.swift */, @@ -80,13 +80,14 @@ path = Resources; sourceTree = ""; }; - 05F686A91C3BF770000CDF62 /* VideoViewController */ = { + F00876D51C4D12F500C099E8 /* VideoViewController */ = { isa = PBXGroup; children = ( - 05F686AA1C3BF770000CDF62 /* TimelineView.swift */, - 05F686AB1C3BF770000CDF62 /* VideoViewController.swift */, + F00876D61C4D12F500C099E8 /* TimelineView.swift */, + F00876D71C4D12F500C099E8 /* VideoViewController.swift */, ); - path = VideoViewController; + name = VideoViewController; + path = ../../VideoViewController; sourceTree = ""; }; /* End PBXGroup section */ @@ -162,8 +163,8 @@ buildActionMask = 2147483647; files = ( 05F686921C3B1695000CDF62 /* ViewController.swift in Sources */, - 05F686AC1C3BF770000CDF62 /* TimelineView.swift in Sources */, - 05F686AD1C3BF770000CDF62 /* VideoViewController.swift in Sources */, + F00876D81C4D12F500C099E8 /* TimelineView.swift in Sources */, + F00876D91C4D12F500C099E8 /* VideoViewController.swift in Sources */, 05F686901C3B1695000CDF62 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -314,6 +315,7 @@ 05F686A01C3B1695000CDF62 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/project.xcworkspace/xcuserdata/danilgontovnik.xcuserdatad/UserInterfaceState.xcuserstate b/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/project.xcworkspace/xcuserdata/danilgontovnik.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..3c5359d Binary files /dev/null and b/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/project.xcworkspace/xcuserdata/danilgontovnik.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/xcuserdata/danilgontovnik.xcuserdatad/xcschemes/VideoViewControllerExample.xcscheme b/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/xcuserdata/danilgontovnik.xcuserdatad/xcschemes/VideoViewControllerExample.xcscheme new file mode 100644 index 0000000..64dac54 --- /dev/null +++ b/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/xcuserdata/danilgontovnik.xcuserdatad/xcschemes/VideoViewControllerExample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/xcuserdata/danilgontovnik.xcuserdatad/xcschemes/xcschememanagement.plist b/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/xcuserdata/danilgontovnik.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..2e06361 --- /dev/null +++ b/VideoViewControllerExample/VideoViewControllerExample.xcodeproj/xcuserdata/danilgontovnik.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,22 @@ + + + + + SchemeUserState + + VideoViewControllerExample.xcscheme + + orderHint + 0 + + + SuppressBuildableAutocreation + + 05F6868B1C3B1695000CDF62 + + primary + + + + + diff --git a/VideoViewControllerExample/VideoViewControllerExample/VideoViewController/TimelineView.swift b/VideoViewControllerExample/VideoViewControllerExample/VideoViewController/TimelineView.swift deleted file mode 100644 index 03e2a72..0000000 --- a/VideoViewControllerExample/VideoViewControllerExample/VideoViewController/TimelineView.swift +++ /dev/null @@ -1,175 +0,0 @@ -// TimelineView.swift -// -// Copyright (c) 2016 Danil Gontovnik (http://gontovnik.com/) -// -// 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. - -import UIKit - -public class TimelineView: UIView { - - // MARK: - Vars - - /// The duration of the video in seconds. - public var duration: NSTimeInterval = 0.0 { - didSet { setNeedsDisplay() } - } - - /// Time in seconds when rewind began. - public var initialTime: NSTimeInterval = 0.0 { - didSet { - currentTime = initialTime - } - } - - /// Current timeline time in seconds. - public var currentTime: NSTimeInterval = 0.0 { - didSet { - setNeedsDisplay() - currentTimeDidChange?(currentTime) - } - } - - /// Internal zoom variable. - private var _zoom: CGFloat = 1.0 { - didSet { setNeedsDisplay() } - } - - /// The zoom of the timeline view. The higher zoom value, the more accurate rewind is. Default is 1.0. - public var zoom: CGFloat { - get { return _zoom } - set { _zoom = max(min(newValue, maxZoom), minZoom) } - } - - /// Indicates minimum zoom value. Default is 1.0. - public var minZoom: CGFloat = 1.0 { - didSet { zoom = _zoom } - } - - /// Indicates maximum zoom value. Default is 3.5. - public var maxZoom: CGFloat = 3.5 { - didSet { zoom = _zoom } - } - - /// The width of a line representing a specific time interval on a timeline. If zoom is not equal 1, then actual interval width equals to intervalWidth * zoom. Value will be used during rewind for calculations — for example, if zoom is 1, intervalWidth is 30 and intervalDuration is 15, then when user moves 10pixels left or right we will rewind by +5 or -5 seconds; - public var intervalWidth: CGFloat = 24.0 { - didSet { setNeedsDisplay() } - } - - /// The duration of an interval in seconds. If video is 55 seconds and interval is 15 seconds — then we will have 3 full intervals and one not full interval. Value will be used during rewind for calculations. - public var intervalDuration: CGFloat = 15.0 { - didSet { setNeedsDisplay() } - } - - /// Block which will be triggered everytime currentTime value changes. - public var currentTimeDidChange: ((NSTimeInterval) -> ())? - - // MARK: - Constructors - - public init() { - super.init(frame: .zero) - - opaque = false - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Methods - - /** - Calculate current interval width. It takes two variables in count - intervalWidth and zoom. - */ - private func currentIntervalWidth() -> CGFloat { - return intervalWidth * zoom - } - - /** - Calculates time interval in seconds from passed width. - - - Parameter width: The distance. - */ - public func timeIntervalFromDistance(distance: CGFloat) -> NSTimeInterval { - return NSTimeInterval(distance * intervalDuration / currentIntervalWidth()) - } - - /** - Calculates distance from given time interval. - - - Parameter duration: The duration of an interval. - */ - public func distanceFromTimeInterval(timeInterval: NSTimeInterval) -> CGFloat { - return currentIntervalWidth() * CGFloat(timeInterval) / intervalDuration - } - - /** - Rewinds by distance. Calculates interval width and adds it to the current time. - - - Parameter distance: The distance how far it should rewind by. - */ - public func rewindByDistance(distance: CGFloat) { - let newCurrentTime = currentTime + timeIntervalFromDistance(distance) - currentTime = max(min(newCurrentTime, duration), 0.0) - } - - // MARK: - Draw - - override public func drawRect(rect: CGRect) { - super.drawRect(rect) - - let intervalWidth = currentIntervalWidth() - - let originX: CGFloat = bounds.width / 2.0 - distanceFromTimeInterval(currentTime) - let context = UIGraphicsGetCurrentContext() - let lineHeight: CGFloat = 5.0 - - // Calculate how many intervals it contains - let intervalsCount = CGFloat(duration) / intervalDuration - - // Draw full line - CGContextSetFillColorWithColor(context, UIColor(white: 0.45, alpha: 1.0).CGColor) - - let totalPath = UIBezierPath(roundedRect: CGRect(x: originX, y: 0.0, width: intervalWidth * intervalsCount, height: lineHeight), cornerRadius: lineHeight).CGPath - CGContextAddPath(context, totalPath) - CGContextFillPath(context) - - // Draw elapsed line - CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor) - - let elapsedPath = UIBezierPath(roundedRect: CGRect(x: originX, y: 0.0, width: distanceFromTimeInterval(currentTime), height: lineHeight), cornerRadius: lineHeight).CGPath - CGContextAddPath(context, elapsedPath) - CGContextFillPath(context) - - // Draw current time dot - CGContextFillEllipseInRect(context, CGRect(x: originX + distanceFromTimeInterval(initialTime), y: 7.0, width: 3.0, height: 3.0)) - - // Draw full line separators - CGContextSetFillColorWithColor(context, UIColor(white: 0.0, alpha: 0.5).CGColor) - - var intervalIdx: CGFloat = 0.0 - repeat { - intervalIdx += 1.0 - if intervalsCount - intervalIdx > 0.0 { - CGContextFillRect(context, CGRect(x: originX + intervalWidth * intervalIdx, y: 0.0, width: 1.0, height: lineHeight)) - } - } while intervalIdx < intervalsCount - } - -} diff --git a/VideoViewControllerExample/VideoViewControllerExample/VideoViewController/VideoViewController.swift b/VideoViewControllerExample/VideoViewControllerExample/VideoViewController/VideoViewController.swift deleted file mode 100644 index 5d92901..0000000 --- a/VideoViewControllerExample/VideoViewControllerExample/VideoViewController/VideoViewController.swift +++ /dev/null @@ -1,223 +0,0 @@ -// VideoViewController.swift -// -// Copyright (c) 2016 Danil Gontovnik (http://gontovnik.com/) -// -// 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. - -import UIKit -import AVFoundation - -public class VideoViewController: UIViewController { - - // MARK: - Vars - - private var videoURL: NSURL! - - private var asset: AVURLAsset! - private var playerItem: AVPlayerItem! - private var player: AVPlayer! - private var playerLayer: AVPlayerLayer! - private var assetGenerator: AVAssetImageGenerator! - - private var longPressGestureRecognizer: UILongPressGestureRecognizer! - - private var previousLocationX: CGFloat = 0.0 - - private let rewindDimView = UIVisualEffectView() - private let rewindContentView = UIView() - public let rewindTimelineView = TimelineView() - private let rewindPreviewShadowLayer = CALayer() - private let rewindPreviewImageView = UIImageView() - private let rewindCurrentTimeLabel = UILabel() - - /// Indicates the maximum height of rewindPreviewImageView. Default value is 112. - public var rewindPreviewMaxHeight: CGFloat = 112.0 { - didSet { - assetGenerator.maximumSize = CGSize(width: CGFloat.max, height: rewindPreviewMaxHeight * UIScreen.mainScreen().scale) - } - } - - /// Indicates whether player should start playing on viewDidLoad. Default is true. - public var autoplays: Bool = true - - // MARK: - Constructors - - /** - Returns an initialized VideoViewController object - - - Parameter videoURL: Local URL to the video asset - */ - public init(videoURL: NSURL) { - super.init(nibName: nil, bundle: nil) - - self.videoURL = videoURL - - asset = AVURLAsset(URL: videoURL) - playerItem = AVPlayerItem(asset: asset) - player = AVPlayer(playerItem: playerItem) - playerLayer = AVPlayerLayer(player: player) - - assetGenerator = AVAssetImageGenerator(asset: asset) - assetGenerator.maximumSize = CGSize(width: CGFloat.max, height: rewindPreviewMaxHeight * UIScreen.mainScreen().scale) - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - - - override public func loadView() { - super.loadView() - - view.backgroundColor = .blackColor() - view.layer.addSublayer(playerLayer) - - longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: Selector("longPressed:")) - view.addGestureRecognizer(longPressGestureRecognizer) - - view.addSubview(rewindDimView) - - rewindContentView.alpha = 0.0 - view.addSubview(rewindContentView) - - rewindTimelineView.duration = CMTimeGetSeconds(asset.duration) - rewindTimelineView.currentTimeDidChange = { [weak self] (currentTime) in - guard let strongSelf = self, playerItem = strongSelf.playerItem, assetGenerator = strongSelf.assetGenerator else { return } - - let minutesInt = Int(currentTime / 60.0) - let secondsInt = Int(currentTime) - minutesInt * 60 - strongSelf.rewindCurrentTimeLabel.text = (minutesInt > 9 ? "" : "0") + "\(minutesInt)" + ":" + (secondsInt > 9 ? "" : "0") + "\(secondsInt)" - - let requestedTime = CMTime(seconds: currentTime, preferredTimescale: playerItem.currentTime().timescale) - - assetGenerator.generateCGImagesAsynchronouslyForTimes([NSValue(CMTime: requestedTime)]) { [weak self] (_, CGImage, _, _, _) in - guard let strongSelf = self, CGImage = CGImage else { return } - let image = UIImage(CGImage: CGImage, scale: UIScreen.mainScreen().scale, orientation: .Up) - - dispatch_async(dispatch_get_main_queue()) { - strongSelf.rewindPreviewImageView.image = image - - if strongSelf.rewindPreviewImageView.bounds.size != image.size { - strongSelf.viewWillLayoutSubviews() - } - } - } - } - rewindContentView.addSubview(rewindTimelineView) - - rewindCurrentTimeLabel.text = " " - rewindCurrentTimeLabel.font = .systemFontOfSize(16.0) - rewindCurrentTimeLabel.textColor = .whiteColor() - rewindCurrentTimeLabel.textAlignment = .Center - rewindCurrentTimeLabel.sizeToFit() - rewindContentView.addSubview(rewindCurrentTimeLabel) - - rewindPreviewShadowLayer.shadowOpacity = 1.0 - rewindPreviewShadowLayer.shadowColor = UIColor(white: 0.1, alpha: 1.0).CGColor - rewindPreviewShadowLayer.shadowRadius = 15.0 - rewindPreviewShadowLayer.shadowOffset = .zero - rewindPreviewShadowLayer.masksToBounds = false - rewindPreviewShadowLayer.actions = ["position": NSNull(), "bounds": NSNull(), "shadowPath": NSNull()] - rewindContentView.layer.addSublayer(rewindPreviewShadowLayer) - - rewindPreviewImageView.contentMode = .ScaleAspectFit - rewindPreviewImageView.layer.mask = CAShapeLayer() - rewindContentView.addSubview(rewindPreviewImageView) - } - - override public func viewDidLoad() { - super.viewDidLoad() - - if autoplays { - play() - } - } - - // MARK: - Methods - - /// Resumes playback - public func play() { - player.play() - } - - /// Pauses playback - public func pause() { - player.pause() - } - - public func longPressed(gesture: UILongPressGestureRecognizer) { - let location = gesture.locationInView(gesture.view!) - rewindTimelineView.zoom = (location.y - rewindTimelineView.center.y - 10.0) / 30.0 - - if gesture.state == .Began { - player.pause() - rewindTimelineView.initialTime = CMTimeGetSeconds(playerItem.currentTime()) - UIView.animateWithDuration(0.2, delay: 0.0, options: [.CurveEaseOut], animations: { - self.rewindDimView.effect = UIBlurEffect(style: .Dark) - self.rewindContentView.alpha = 1.0 - }, completion: nil) - } else if gesture.state == .Changed { - rewindTimelineView.rewindByDistance(previousLocationX - location.x) - } else { - player.play() - - let newTime = CMTime(seconds: rewindTimelineView.currentTime, preferredTimescale: playerItem.currentTime().timescale) - playerItem.seekToTime(newTime) - - UIView.animateWithDuration(0.2, delay: 0.0, options: [.CurveEaseOut], animations: { - self.rewindDimView.effect = nil - self.rewindContentView.alpha = 0.0 - }, completion: nil) - } - - if previousLocationX != location.x { - previousLocationX = location.x - } - } - - override public func prefersStatusBarHidden() -> Bool { - return true - } - - // MARK: - Layout - - override public func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - - playerLayer.frame = view.bounds - rewindDimView.frame = view.bounds - - rewindContentView.frame = view.bounds - - let timelineHeight: CGFloat = 10.0 - let verticalSpacing: CGFloat = 25.0 - - let rewindPreviewImageViewWidth = rewindPreviewImageView.image?.size.width ?? 0.0 - rewindPreviewImageView.frame = CGRect(x: (rewindContentView.bounds.width - rewindPreviewImageViewWidth) / 2.0, y: (rewindContentView.bounds.height - rewindPreviewMaxHeight - verticalSpacing - rewindCurrentTimeLabel.bounds.height - verticalSpacing - timelineHeight) / 2.0, width: rewindPreviewImageViewWidth, height: rewindPreviewMaxHeight) - rewindCurrentTimeLabel.frame = CGRect(x: 0.0, y: rewindPreviewImageView.frame.maxY + verticalSpacing, width: rewindTimelineView.bounds.width, height: rewindCurrentTimeLabel.frame.height) - rewindTimelineView.frame = CGRect(x: 0.0, y: rewindCurrentTimeLabel.frame.maxY + verticalSpacing, width: rewindContentView.bounds.width, height: timelineHeight) - rewindPreviewShadowLayer.frame = rewindPreviewImageView.frame - - let path = UIBezierPath(roundedRect: rewindPreviewImageView.bounds, cornerRadius: 5.0).CGPath - rewindPreviewShadowLayer.shadowPath = path - (rewindPreviewImageView.layer.mask as! CAShapeLayer).path = path - } - -}