iOS视频播放

离上一篇博客发布又是两周时间过去了,心里总是感觉空空的。两周没有发布一篇博客,感觉就像是虚度了。另外一个原因就是实在是不知道写点什么,有些内容兴趣不大就搁置了。其次就是之前的天坑真是没有动力去填了,比如CoreData,就放在那吧。上篇博客也提到了这篇做视频播放器,所以这篇就是讲讲视频播放器的。

上篇博客也提到了一些资源:

iOS开发系列–音频播放、录音、视频播放、拍照、视频录制
iOS音频播放 (一):概述
AVFoundation Programming Guide

第一个资源基本所有的东西都讲到了,音频、视频等。简单易懂,入门级学习都够了,也能满足基本的需求。
第二个资源主要讲解音频的,很专业。
第三个资源是苹果官方的编程指南,具有很好的指导意义,建议大家阅读。

其实基本知识点前面的这些资源都讲到了,我这里再讲也就没什么意思了。

所以我们这里就做一个完整功能的播放器,播放,暂停,快进,全屏等功能。

接下来我们就开始吧,先说下我的开发环境。

  • OS X EI Capitan(Version 10.11)
  • Xcode(Version 7.0.1)
  • 使用Swift作为开发语言

建立项目

如下图所示,我们选择Single View Application,项目名称叫做LWPlayer,语言选择Swift
alt text

alt text

然后我们将项目的Deployment Target修改为8.0。然后我们删除Main.storyboard文件,同时Main Interface里面的Main也去掉。因为我们删除了storyboard文件,如果不删除Main的话程序无法启动直接崩溃。

现在项目的界面应该如下所示:

alt text

这里我们删除了storyboard文件,我的打算是用代码手写界面,不使用storyboard和xib文件。大家可能都知道用代码写布局很繁琐,所以这里使用第三方的框架SnapKit。这个第三方框架其实就是OC版本的Masonry,这里就不细讲布局的事情了,参考资料 SnapKit Masonry

现在项目编译运行之后就是一片漆黑,之后我们要自己做一些初始化的工作。首先我们先添加第三方库,及SnapKit。我们使用依赖管理器来添加第三方库。可以使用CocoaPods,当然你也可以使用Carthage,如果是Swift以及iOS8.0以上推荐使用Carthage来管理。这里我们使用Carthage Carthage ,安装很简单,请自行了解。

Carthage的参考资料:

Carthage
Carthage:去中心化的Cocoa依赖管理器
Cocoa 新的依赖管理工具:Carthage

第一篇是官方的,第二篇相当于是官方的翻译,大家自行参考学习。

我们进入到项目目录执行如下命令,截图如下:

alt text

进入到项目目录后我们创建了Cartfile文件,然后在里面添加需要的第三方库。如下所示:

alt text

然后:wq保存退出,然后执行carthage update命令进行安装编译。

这里就算是安装编译完成了,我们可以进到工程目录里面看下编程生成的二进制framework文件。如下图所示:

alt text

不仅有iOS的,还有Mac的。现在我们还差最后一步,就是将framework添加到我们的工程。设置Framework Search Paths是最简单的方法,告诉工程到那里去找这些framework。设置为$(SRCROOT)/Carthage/Build/iOS,如果是OS X就把iOS改成Mac。如下所示:

alt text

这一步和官方说明的不一样,按照官方的方法,每次添加了新的framework都要自己到Xcode里面进行手动添加。

第三方库算是添加完成了,现在我们要修改AppDelegate文件。修改func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool方法如下:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.

    //appearance
    UINavigationBar.appearance().barTintColor = UIColor(red: 56.0/255.0, green: 62.0/255.0, blue: 72.0/255.0, alpha: 1.0)
    UINavigationBar.appearance().tintColor = UIColor.whiteColor()

    //rootViewController
    let rootVC = ViewController()

    window = UIWindow(frame: UIScreen.mainScreen().bounds)
    window!.rootViewController = rootVC
    window?.makeKeyAndVisible()

    return true
}

这里代码很简单,就不讲解了。运行启动后屏幕仍然是黑的,我们继续做些修改。将ViewController的代码修改为如下所示:

import UIKit
import SnapKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        view.backgroundColor = UIColor.whiteColor()

        let aView = UIView()
        aView.backgroundColor = UIColor.orangeColor()
        view.addSubview(aView)

        aView.snp_makeConstraints { (make) -> Void in
            make.edges.equalTo(view).offset(UIEdgeInsets(top: 10, left: 10, bottom: -10, right: -10))
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

我们首先导入SnapKit模块,然后修改视图的背景色为白色,然后添加了一个距离父视图编剧为10的橙色子视图。布局的代码,SnapKit的使用方法就不讲解了。

编译运行后会发现如下错误,

alt text

这里是因为之前修改Framework Search Paths导致的,其实前面的这一步貌似并没有效果。所以我们还是得按照官方的来,如下所示:

alt text

我们在Embedded Binaries这里将Carthage/Build/iOS/SnapKit.framework添加进来,然后编译运行,截图如下:

alt text

项目结构

首先我们删除ViewController,新建LWPlayerViewController,代码如下:

import UIKit
import SnapKit

class LWPlayerViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        let aView = UIView();
        aView.backgroundColor = UIColor.orangeColor();
        view.addSubview(aView)

        aView.snp_makeConstraints { (make) -> Void in
            make.edges.equalTo(view)
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

很简单就不讲解了,同时我们还需要修改AppDelegatefunc application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool方法,代码如下:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.

    //appearance
    UINavigationBar.appearance().barTintColor = UIColor(red: 56.0/255.0, green: 62.0/255.0, blue: 72.0/255.0, alpha: 1.0)
    UINavigationBar.appearance().tintColor = UIColor.whiteColor()

    //rootViewController
    let rootVC = LWPlayerViewController()

    window = UIWindow(frame: UIScreen.mainScreen().bounds)
    window!.rootViewController = rootVC
    window?.makeKeyAndVisible()

    return true
}

运行后截图如下:

alt text

你可能发现我们设置了导航栏的外观,所以我们接下来就新建LWNavigationController继承自UINavigationController,代码如下:

import UIKit

class LWNavigationController: UINavigationController {

    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // MARK: - StatusBar

    override func preferredStatusBarStyle() -> UIStatusBarStyle {
        return UIStatusBarStyle.LightContent
    }

}

这里的代码也没什么好讲解的,主要就是一些初始化方法,有些是必需的,没有会报错。之后我们修改AppDelegate,首先添加:

var navigationVC: LWNavigationController!

的属性,然后修改func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool方法:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.

    //appearance
    UINavigationBar.appearance().barTintColor = UIColor(red: 56.0/255.0, green: 62.0/255.0, blue: 72.0/255.0, alpha: 1.0)
    UINavigationBar.appearance().tintColor = UIColor.whiteColor()

    //rootViewController
    let rootVC = LWPlayerViewController()
    navigationVC = LWNavigationController(rootViewController: rootVC)

    window = UIWindow(frame: UIScreen.mainScreen().bounds)
    window!.rootViewController = navigationVC
    window?.makeKeyAndVisible()

    return true
}

运行后模拟器截图:

alt text

视频资源

说来惭愧,没有在网上找到可以用来进行远程播放的视频资源,所以我们要自己弄个。使用CocoaHTTPServer第三方库,让App内嵌一个HTTP server。

VKVideoPlayer

这里有一个开源的播放器,功能很完善,包括加载字幕、广告等等,大家可以参考学习下。

这里我们先来添加第三方库CocoaHTTPServer,前面我们使用的是Carthage来管理依赖的,貌似 这个第三方库不行。但是CocoaPods可以,但是我们这里就不使用CocoaPods,估计这里是可以用的,这两个依赖管理应该不会冲突,这里我们就不测试了,直接使用最原始的方法:拖放文件。

CocoaHTTPServer ,这里是下载地址,下载完成后我们将CoreExtensionsVendor放到新建的文件夹CocoaHTTPServer中,其他的东西我们不需要了。然后将这个文件夹拖放到工程中,如下图所示:

alt text

接下来我们添加libxml2.tbd,不添加会报找不到文件的错。添加后如下图所示。

alt text

因为这个第三方库是OC,如果要混编的话我们需要添加桥接头文件,LWPlayer-Bridging-Header.h。修改文件内容的代码如下:

#import "HTTPServer.h"

然后我们还要配置下头文件,如下所示:

alt text

然后我们参考 VKVideoPlayer HTTPStreamingServer,代码如下:

import UIKit

let HTTPStreamingServerSingleton = HTTPStreamingServer()

class HTTPStreamingServer: NSObject {

    var httpServer: HTTPServer!

    class var sharedInstance: HTTPStreamingServer {
        get {
            return HTTPStreamingServerSingleton
        }
    }

    override init() {
        super.init()
        initialize()
    }

    func initialize() {
        httpServer = HTTPServer()
        httpServer.setType("_http._tcp.")
        httpServer.setPort(12345)
        let webPath = NSBundle.mainBundle().resourcePath?.stringByAppendingString("/web")
        httpServer.setDocumentRoot(webPath)
    }

    func start() {
        do {
            try httpServer.start()
            print("Started HTTP Server on port \(httpServer.listeningPort())")
        } catch {
            print("Error starting HTTP Server")
        }
    }

    func stop() {
        httpServer.stop()
    }

}

定义了一个全局的常量,然后使用类计算型属性实现单列。其他的代码很简单,就不讲解了。

然后我们在func applicationDidBecomeActive(application: UIApplication)方法中加入HTTPStreamingServer.sharedInstance.start()func applicationWillTerminate(application: UIApplication)方法中添加HTTPStreamingServer.sharedInstance.stop(),运行之后在控制台输出Started HTTP Server on port 12345

然后我们将一些视频资源拖到项目中,这里我们就用VKVideoPlayer的资源,将web这个文件夹拖到我们项目中,注意我截图中的箭头,如下所示:

alt text

播放器

新建一个类LWPlayerView继承自UIView,代码如下:

import UIKit
import AVFoundation

class LWPlayerView: UIView {

    var player: AVPlayer {
        set {
            (layer as! AVPlayerLayer).player = newValue
        }
        get {
            return (layer as! AVPlayerLayer).player!
        }
    }

    override class func layerClass() -> AnyClass {
        return AVPlayerLayer.classForCoder()
    }

}

如果你看过苹果的编程指南或者苹果给的Demo,都是这样实现的,可以算是最佳实践吧。

接下来我们就来实现播放器,到LWPlayerViewController。首先我们添加如下所示的属性:

let videoPath = "http://localhost:12345/wifi_640_index.m3u8"//视频播放地址
let radio = CGFloat(375.0 / 238.0)//缩放比例
let width = UIScreen.mainScreen().bounds.size.width//屏幕宽度

let playContentView = UIView()//播放器容器视图
let toolContentView = UIView()//播放器工具容器视图

var player: AVPlayer!//播放器
var playerView = LWPlayerView()//播放视图

var hideTool = false

后面都有注释,最后一个属性用来表示是否隐藏状态栏和工具栏。然后我们添加初始化播放器的方法:

func initPlayer() {
    let url = NSURL(string: videoPath)!
    let playerItem = AVPlayerItem(URL: url)
    player = AVPlayer(playerItem: playerItem)
}

然后是初始化视图的方法:

func initView() {

    playContentView.backgroundColor = UIColor.blackColor()
    view.addSubview(playContentView)
    playContentView.snp_makeConstraints { (make) -> Void in
        make.top.left.right.equalTo(view)
        make.width.equalTo(playContentView.snp_height).multipliedBy(radio)
    }

    playerView.player = player
    playerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: Selector("sigleTapAction")))
    playContentView.addSubview(playerView)
    playerView.snp_makeConstraints { (make) -> Void in
        make.edges.equalTo(playContentView)
    }

    toolContentView.backgroundColor = UIColor(red: 56.0/255.0, green: 62.0/255.0, blue: 72.0/255.0, alpha: 1.0)
    playContentView.addSubview(toolContentView)
    toolContentView.snp_makeConstraints { (make) -> Void in
        make.left.bottom.right.equalTo(playContentView)
        make.height.equalTo(30.0)
    }
}

然后我们实现sigleTapAction()方法:

func sigleTapAction() {
    if hideTool {
        hideTool = false
        navigationController?.navigationBar.hidden = false
        toolContentView.hidden = false
    } else {
        hideTool = true
        navigationController?.navigationBar.hidden = true
        toolContentView.hidden = true
    }
}

然后修改func viewDidLoad()方法,当点击的时候会隐藏状态栏和工具栏:

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = UIColor.whiteColor()
    initPlayer()
    initView()
    player.play()
}

运行后截图如下:

alt text

大家都知道HTTP请求在iOS9里面有变化,所以有的同学可能没有看到视频播放的画面;同样的VKVideoPlayer你可能也看不到视频播放,都是相同的原因,大家搜索都能解决。设置下.plist文件就可以了,如下图所示:

alt text

注意这两个字段的类型,一个是字典类型,一个是布尔类型。

播放器界面

接下来我们就来完成播放器的界面,让它开起来更像一个播放器。首先我们添加如下所示的属性:

let playButton = UIButton()//播放按钮
let muteButton = UIButton()//静音按钮
let progress = UISlider()//播放进度
let srtsButton = UIButton()//字幕按钮
let fullButton = UIButton()//全屏按钮

接下来我们就来布局这些按钮,修改initView()方法,代码如下,直接替换即可:

func initView() {

    playContentView.backgroundColor = UIColor.blackColor()
    view.addSubview(playContentView)
    playContentView.snp_makeConstraints { (make) -> Void in
        make.top.left.right.equalTo(view)
        make.width.equalTo(playContentView.snp_height).multipliedBy(radio)
    }

    playerView.player = player
    playerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: Selector("sigleTapAction")))
    playContentView.addSubview(playerView)
    playerView.snp_makeConstraints { (make) -> Void in
        make.edges.equalTo(playContentView)
    }

    toolContentView.backgroundColor = UIColor(red: 56.0/255.0, green: 62.0/255.0, blue: 72.0/255.0, alpha: 1.0)
    playContentView.addSubview(toolContentView)
    toolContentView.snp_makeConstraints { (make) -> Void in
        make.left.bottom.right.equalTo(playContentView)
        make.height.equalTo(30.0)
    }

    playButton.setImage(UIImage(named: "pause_icon_vertical"), forState: UIControlState.Normal)
    playButton.setImage(UIImage(named: "play_icon_vertical"), forState: UIControlState.Selected)
    muteButton.setImage(UIImage(named: "sound_on_vertical"), forState: UIControlState.Normal)
    muteButton.setImage(UIImage(named: "sound_off_vertical"), forState: UIControlState.Selected)
    srtsButton.setImage(UIImage(named: "chinese_english_shift_vertical"), forState: UIControlState.Normal)
    srtsButton.setImage(UIImage(named: "english_shift_vertical"), forState: UIControlState.Selected)
    fullButton.setImage(UIImage(named: "full_screen_icon_vertical"), forState: UIControlState.Normal)

    progress.minimumValue = 0.0
    progress.maximumValue = 1.0
    progress.continuous = false
    progress.setThumbImage(UIImage(named: "VKScrubber_thumb_vertical"), forState: UIControlState.Normal)
    progress.value = 0.0
    progress.minimumTrackTintColor = UIColor(red: 10 / 255, green: 137 / 255, blue: 153 / 255, alpha: 1.0)
    progress.maximumTrackTintColor = UIColor(red: 10 / 255, green: 137 / 255, blue: 153 / 255, alpha: 0.4)

    playButton.addTarget(self, action: Selector("playAction:"), forControlEvents: UIControlEvents.TouchUpInside)
    muteButton.addTarget(self, action: Selector("muteAction:"), forControlEvents: UIControlEvents.TouchUpInside)
    srtsButton.addTarget(self, action: Selector("srtsAction:"), forControlEvents: UIControlEvents.TouchUpInside)
    fullButton.addTarget(self, action: Selector("fullAction:"), forControlEvents: UIControlEvents.TouchUpInside)

    toolContentView.addSubview(playButton)
    toolContentView.addSubview(muteButton)
    toolContentView.addSubview(progress)
    toolContentView.addSubview(srtsButton)
    toolContentView.addSubview(fullButton)

    playButton.snp_makeConstraints { (make) -> Void in
        make.left.equalTo(10.0)
        make.centerY.equalTo(toolContentView.snp_centerY)
        make.height.width.equalTo(20.0)
    }

    muteButton.snp_makeConstraints { (make) -> Void in
        make.left.equalTo(playButton.snp_right).offset(13.0)
        make.centerY.equalTo(toolContentView.snp_centerY)
        make.height.width.equalTo(20.0)
    }

    progress.snp_makeConstraints { (make) -> Void in
        make.left.equalTo(muteButton.snp_right).offset(22.0)
        make.right.equalTo(srtsButton.snp_left).offset(-16.0)
        make.centerY.equalTo(toolContentView.snp_centerY)
        make.height.equalTo(2.0)
    }

    srtsButton.snp_makeConstraints { (make) -> Void in
        make.right.equalTo(fullButton.snp_left).offset(-14.0)
        make.centerY.equalTo(toolContentView.snp_centerY)
        make.height.width.equalTo(20.0)
    }

    fullButton.snp_makeConstraints { (make) -> Void in
        make.right.equalTo(-10.0)
        make.centerY.equalTo(toolContentView.snp_centerY)
        make.height.width.equalTo(20.0)
    }
}

代码很长,但是基本没有难点,大都是布局的代码,这里就不讲解了。然后我们添加按钮相应的Action:

func playAction(sender: UIButton) {
    if player.rate == 0 {
        player.play()
        sender.selected = true
    } else {
        player.pause()
        sender.selected = false
    }
}

func muteAction(sender: UIButton) {
    if player.muted {
        player.muted = false
        sender.selected = false
    } else {
        player.muted = true
        sender.selected = true
    }
}

func srtsAction(sender: UIButton) {

}

func fullAction(sender: UIButton) {

}

然后我们将viewDidLoad()方法里面的播放的代码player.play()去掉,运行之后截图如下:

alt text

强制横屏

首先我们修改横屏按钮要调用的方法,代码如下:

func fullAction(sender: UIButton) {
    UIDevice.currentDevice().setValue(NSNumber(integer: UIInterfaceOrientation.LandscapeRight.rawValue), forKey: "orientation")
    UIViewController.attemptRotationToDeviceOrientation()
}

接下来就是要调整下横屏时的视图,代码如下:

override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) {
    if UIInterfaceOrientationIsLandscape(toInterfaceOrientation) {
        playContentView.snp_remakeConstraints { (make) -> Void in
            make.edges.equalTo(view)
        }

        toolContentView.snp_remakeConstraints { (make) -> Void in
            make.left.bottom.right.equalTo(playContentView)
            make.height.equalTo(52.0)
        }

        playButton.setImage(UIImage(named: "pause_icon_landscape"), forState: UIControlState.Normal)
        playButton.setImage(UIImage(named: "play_icon_landscape"), forState: UIControlState.Selected)
        muteButton.setImage(UIImage(named: "sound_on_landscape"), forState: UIControlState.Normal)
        muteButton.setImage(UIImage(named: "sound_off_landscape"), forState: UIControlState.Selected)
        srtsButton.setImage(UIImage(named: "chinese_english_shift_landscape"), forState: UIControlState.Normal)
        srtsButton.setImage(UIImage(named: "english_shift_landscape"), forState: UIControlState.Selected)
        fullButton.setImage(UIImage(named: "full_screen_icon_landscape"), forState: UIControlState.Normal)
        progress.setThumbImage(UIImage(named: "VKScrubber_thumb_landscape"), forState: UIControlState.Normal)

        playButton.snp_remakeConstraints { (make) -> Void in
            make.left.equalTo(25.0)
            make.centerY.equalTo(toolContentView.snp_centerY)
            make.height.width.equalTo(24.0)
        }

        muteButton.snp_remakeConstraints { (make) -> Void in
            make.left.equalTo(playButton.snp_right).offset(32.0)
            make.centerY.equalTo(toolContentView.snp_centerY)
            make.height.width.equalTo(24.0)
        }

        progress.snp_remakeConstraints { (make) -> Void in
            make.left.equalTo(muteButton.snp_right).offset(30.0)
            make.right.equalTo(srtsButton.snp_left).offset(-31.0)
            make.centerY.equalTo(toolContentView.snp_centerY)
            make.height.equalTo(4.0)
        }

        srtsButton.snp_remakeConstraints { (make) -> Void in
            make.right.equalTo(fullButton.snp_left).offset(-26.0)
            make.centerY.equalTo(toolContentView.snp_centerY)
            make.height.width.equalTo(24.0)
        }

        fullButton.snp_remakeConstraints { (make) -> Void in
            make.right.equalTo(-27.0)
            make.centerY.equalTo(toolContentView.snp_centerY)
            make.height.width.equalTo(24.0)
        }
    } else {
        playContentView.snp_remakeConstraints { (make) -> Void in
            make.top.left.right.equalTo(view)
            make.width.equalTo(playContentView.snp_height).multipliedBy(radio)
        }

        toolContentView.snp_remakeConstraints { (make) -> Void in
            make.left.bottom.right.equalTo(playContentView)
            make.height.equalTo(30.0)
        }
        UIApplication.sharedApplication().statusBarHidden = true
        playButton.setImage(UIImage(named: "pause_icon_vertical"), forState: UIControlState.Normal)
        playButton.setImage(UIImage(named: "play_icon_vertical"), forState: UIControlState.Selected)
        muteButton.setImage(UIImage(named: "sound_on_vertical"), forState: UIControlState.Normal)
        muteButton.setImage(UIImage(named: "sound_off_vertical"), forState: UIControlState.Selected)
        srtsButton.setImage(UIImage(named: "chinese_english_shift_vertical"), forState: UIControlState.Normal)
        srtsButton.setImage(UIImage(named: "english_shift_vertical"), forState: UIControlState.Selected)
        fullButton.setImage(UIImage(named: "full_screen_icon_vertical"), forState: UIControlState.Normal)
        progress.setThumbImage(UIImage(named: "VKScrubber_thumb_vertical"), forState: UIControlState.Normal)

        playButton.snp_remakeConstraints { (make) -> Void in
            make.left.equalTo(10.0)
            make.centerY.equalTo(toolContentView.snp_centerY)
            make.height.width.equalTo(20.0)
        }

        muteButton.snp_remakeConstraints { (make) -> Void in
            make.left.equalTo(playButton.snp_right).offset(13.0)
            make.centerY.equalTo(toolContentView.snp_centerY)
            make.height.width.equalTo(20.0)
        }

        progress.snp_remakeConstraints { (make) -> Void in
            make.left.equalTo(muteButton.snp_right).offset(22.0)
            make.right.equalTo(srtsButton.snp_left).offset(-16.0)
            make.centerY.equalTo(toolContentView.snp_centerY)
            make.height.equalTo(2.0)
        }

        srtsButton.snp_remakeConstraints { (make) -> Void in
            make.right.equalTo(fullButton.snp_left).offset(-14.0)
            make.centerY.equalTo(toolContentView.snp_centerY)
            make.height.width.equalTo(20.0)
        }

        fullButton.snp_remakeConstraints { (make) -> Void in
            make.right.equalTo(-10.0)
            make.centerY.equalTo(toolContentView.snp_centerY)
            make.height.width.equalTo(20.0)
        }
    }
}

运行之后截图如下:

alt text

现在只能横屏,还不能返回到竖屏的状态,接下来修改fullAction(sender: UIButton)方法,代码如下:

func fullAction(sender: UIButton) {
    if UIDevice.currentDevice().orientation == UIDeviceOrientation.Portrait {
        UIDevice.currentDevice().setValue(NSNumber(integer: UIInterfaceOrientation.LandscapeRight.rawValue), forKey: "orientation")
        UIViewController.attemptRotationToDeviceOrientation()
    } else {
        UIDevice.currentDevice().setValue(NSNumber(integer: UIInterfaceOrientation.Portrait.rawValue), forKey: "orientation")
        UIViewController.attemptRotationToDeviceOrientation()
    }
}

播放进度

其实实现播放器进度很简单,如果看了最开始的那些资源的话。几行代码就可以解决了:

func addProgressObserver() {
    let playerItem = player.currentItem!
    player.addPeriodicTimeObserverForInterval(CMTimeMake(1, 1), queue: dispatch_get_main_queue()) { (time: CMTime) -> Void in
        let current = CMTimeGetSeconds(time)
        let total = CMTimeGetSeconds(playerItem.duration)
        let progress = current / total
        self.progress.setValue(Float(progress), animated: true)
    }
}

获取当前播放器的播放资源,然后得出总的时间和当前的时间,然后做一个百分比,最后设置UISlider的值。这个方法在我之前做进度条的时候就用到了,那里唯一不同的地方就是每秒调用10次,这里是每秒一次,设置CMTimeMake(1, 1)CMTimeMake(1, 10)就是每秒调用10次。

然后我们在初始化播放器的方法里面添加调用该方法的代码:

func initPlayer() {
    let url = NSURL(string: videoPath)!
    let playerItem = AVPlayerItem(URL: url)
    player = AVPlayer(playerItem: playerItem)
    addProgressObserver()
}

运行之后如下所示:

alt text

拖动进度

首先我们添加一个属性,如下所示:

var timeObserver: AnyObject!

然后修改addProgressObserver()方法,如下:

func addProgressObserver() {
    let playerItem = player.currentItem!
    timeObserver = player.addPeriodicTimeObserverForInterval(CMTimeMake(1, 1), queue: dispatch_get_main_queue()) { (time: CMTime) -> Void in
        let current = CMTimeGetSeconds(time)
        let total = CMTimeGetSeconds(playerItem.duration)
        let progress = current / total
        self.progress.setValue(Float(progress), animated: true)
    }
}

然后添加一个新的方法:

func removeProgressObserve() {
    player.removeTimeObserver(timeObserver)
}

苹果说这两个方法最好成对出现,你Cmd点击这些方法名就能看见苹果给的注释和描述了。

现在我们就来实现主要的逻辑了,但是首先还是要给我们的UISlider添加Action,代码如下:

progress.addTarget(self, action: Selector("updateProgress:"), forControlEvents: UIControlEvents.ValueChanged)

然后我们实现updateProgress(sender: UISlider)方法,这里就是主要逻辑了,代码如下:

func updateProgress(sender: UISlider) {
    removeProgressObserve()
    let progress = Float64(sender.value)
    let seekTime = CMTimeGetSeconds(player.currentItem!.duration) * progress
    player.seekToTime(CMTimeMakeWithSeconds(seekTime, 1), toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) { (finished: Bool) -> Void in
        if finished {
            self.addProgressObserver()
        }
    }
}

首先我们移除掉进度监视的方法,避免在拖动的时候还在设置进度的值。然后我们算出拖动的值对应的要播放的时间,然后我们就调用AVPlayer的方法就可以了,然后在结束的时候再添加进度监视就OK了。经测试,不调用这两个方法也是OK的。

运行后截图如下:

alt text

alt text

第一张图启动后没有点击播放,然后我们拖动进度条,拖动后的效果就是第二张图。这个进度条不太好拖动,如果你是模拟器,用鼠标点击圆点的左上角比较好拖动。

播放器到此就基本完成了,当然功能不够完善,比如音量不能调节,只能静音开关。还剩下一个字幕的功能也没有做,这里就不再继续了。字幕的实现主要难点就是解析字幕文件了,留给大家了,不会就参考 VKVideoPlayer

完整源码 ,这里我把图片资源去掉了,不方便贡献给大家,见谅。

坚持原创技术分享,您的支持将鼓励我继续创作!