当前位置: 首页 > 工具软件 > Swift-T > 使用案例 >

swift--使用AVPlayer框架播放影片

严正初
2023-12-01

描述

- 采用URL播放(本地视频也是将filePath转URL)

- 支持自适应横竖屏

- 支持保持视屏原有比例或填充屏幕

- 支持播放暂停控制

- 支持拖动进度条或者屏幕改变进度

- 支持显示播放时间

- 支持动态切换播放源

核心代码

//
//  PlayerViewController.swift
//  AVPlayer
//
//  Created by antu on 2020/2/20.
//  Copyright © 2020 antu. All rights reserved.
//

import UIKit
import AVFoundation

class PlayerViewController: UIViewController {
    private var videoName = "未知视频"
    //播放源队列
    private var videoQueue:Array<URL> = []
    //播放控制
    private var avPlayer = AVPlayer()
    //访问资源信息等
    private var playItem:AVPlayerItem!
    //控制视频内容在UI上的显示
    var playerLayer = AVPlayerLayer(player: AVPlayer())
    //用于监听播放进度
    private var playTimeObserver:Any?
    
    //UI控件
    private var videoView = UIView()
    private let progressView = UISlider()
    private let timeLabel = UILabel()
    private let playButton = UIButton()
    //底部控制栏
    private let controllView = UIView()
    //自定义顶部导航栏
    private let navigationView = UIView()
    
    //存储当前视频的最大时长
    private var maxTime = "--:--"
    
    //标志当前视频在播放队列中的位置
    private var currentIndex = 0
    //标志进度是否被拖动
    private var isSliding = false
    //标志用户的触摸状态
    private var isTouching = false
    //标志手指在屏幕上开始移动的坐标
    private var startPoint:CGPoint = CGPoint(x: 0, y: 0)
    //标志手指上次触摸位置的坐标
    private var lastPoint:CGPoint = CGPoint(x: 0, y: 0)
    

    /*
     初始化参数 url:URL
     描述 该模式下不显示下个视屏按钮
     **/
    init(videoName:String, url: URL) {
        self.init()
        self.videoName = videoName
        self.videoQueue.append(url)
    }
    
    /*
    初始化参数 urls:[URL]
    描述 可以动态切换播放源
    **/
    init(videoName:String, urls:[URL], preferred:Int) {
        self.init()
        self.videoName = videoName
        self.videoQueue = urls
        //设置默认播放源
        if self.videoQueue.count <= 0 {return}
        switchPlayBackSource(url: videoQueue[preferred])
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
    }
    
    //监听播放准备是否就绪
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "status" {
             //获取更改后的状态
            guard let ch = change else { return }
            
            for value in ch.values {
                let status:Int = value as! Int
                switch status {
                case 0:
                    print("AVPlayerStatusFailed")
                case 1:
                    print("AVPlayerStatusReadyToPlay")
                    //获取全部时间
                    //CMTime value表示帧数,TimeScale是帧率。value/TimeScale就是时间
                    let duration = self.playItem.duration
                    //当前视频的最大帧数
                    let value = duration.value
                    //当前视频的帧率
                    let timescale = duration.timescale
                    //最大时间作为进度条的最大值
                    self.progressView.maximumValue = Float(value)/Float(timescale)
                    //把最大时间转换成字符形式在UI显示
                    self.maxTime = "\(transToHourMinSec(time: Float(CMTimeGetSeconds(duration).rounded())))"
                    //开始监听播放进度
                    monitoringProgress(timescale: timescale)
                default:
                    print("AVPlayerStatusUnknown")
                }
            }
            
        }
    }
    
    //屏幕将要旋转监控
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        let width = videoView.bounds.height
        let height = videoView.bounds.width
        UIView.animate(withDuration: 0.4) {
            self.playerLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
        }
    }
    
    /******************** 屏幕手势监测 **********************/
    //开始触摸
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        isTouching = true
        showControll()
        avPlayerPause()
        for touch:AnyObject in touches {
            let t:UITouch = touch as! UITouch
            startPoint = t.location(in: self.view)
        }
    }

    //手指移动
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        avPlayerPause()
        isTouching = true
        for touch:AnyObject in touches {
            let t:UITouch = touch as! UITouch
            let point = t.location(in: self.view)
            startPoint = lastPoint
            lastPoint = point
            let operating = getOperating()
            let value = operating.1
            switch operating.0 {
            case .brightness:
                break
            case .volume:
                break
            case .progress:
                isSliding = true
                let newValue = self.progressView.maximumValue * value
                self.progressView.value = self.progressView.value + newValue
                print("newValue = \(newValue)")
                playerSliderValueChanged(sender: self.progressView)
            case .unknown:
                break
            }
        }
    }
    //手指离开屏幕
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        startPoint = CGPoint.zero
        lastPoint = CGPoint.zero
        isTouching = false
        isSliding = false
        hiddenControll()
        avPlayerPlay()
    }
    
    enum Operating {
        case volume
        case brightness
        case progress
        case unknown
    }
    
    //该手势类别,不必要的手势就抛弃
    func getOperating() -> (Operating,Float) {
        var operating = Operating.unknown
        var value:Float = 0
//        if startPoint.x <= videoView.bounds.width/2 {
//            operating = Operating.brightness
//        } else {
//            operating = Operating.volume
//        }
//        value = (lastPoint.y - startPoint.y) / videoView.bounds.height * 1.5
        if startPoint.y > videoView.bounds.height * 0.3 && startPoint.y < videoView.bounds.height * 0.7 {
            operating = Operating.progress
            value = Float((lastPoint.x - startPoint.x) / videoView.bounds.width)
        }
        
        print("operating = \(operating),value = \(value)")
        return (operating,value)
        
    }
    
    //检测有没有产生循环引用
    deinit {
        avPlayer = AVPlayer()
        print("== PlayerViewController 释放 ==")
    }
    
    /************************UI相关方法***********************************/
    //设置UI
    private func setUI(){
//        self.frame = rect
        videoView.frame = self.view.bounds
//        self.addSubview(videoView)
        
        videoView.translatesAutoresizingMaskIntoConstraints = false
        videoView.backgroundColor = UIColor.black
        self.view.addSubview(videoView)
        let videoViewH = NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[View]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":videoView])
        let videoViewV = NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[View]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":videoView])
        self.view.addConstraints(videoViewH)
        self.view.addConstraints(videoViewV)

        //自定义导航栏
        navigationView.translatesAutoresizingMaskIntoConstraints = false
        navigationView.backgroundColor = UIColor.init(displayP3Red: 0.1, green: 0.1, blue: 0.1, alpha: 0.25)
        self.view.addSubview(navigationView)
        let navigationViewH = NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[View]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":navigationView])
        let navigationViewV = NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[View(80)]", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":navigationView])
        self.view.addConstraints(navigationViewH)
        self.view.addConstraints(navigationViewV)
        
        //left
        //返回按钮
        let backButton = UIButton()
        backButton.translatesAutoresizingMaskIntoConstraints = false
        navigationView.addSubview(backButton)
        let backButtonH = NSLayoutConstraint.constraints(withVisualFormat: "H:|-25-[View(30)]", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":backButton])
        let backButtonV = NSLayoutConstraint.constraints(withVisualFormat: "V:[View(30)]-10-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":backButton])
        navigationView.addConstraints(backButtonH)
        navigationView.addConstraints(backButtonV)
        backButton.setImage(UIImage(named: "chevron-left"), for: UIControl.State.normal)
        backButton.addTarget(self, action: #selector(self.backBtnClick), for: UIControl.Event.touchUpInside)
        
        let titleLabel = UILabel()
        titleLabel.textColor = UIColor.white
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        navigationView.addSubview(titleLabel)
        let titleLabelH = NSLayoutConstraint.constraints(withVisualFormat: "H:[Left]-20-[View(200)]", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":titleLabel,"Left":backButton])
        let titleLabelV = NSLayoutConstraint.constraints(withVisualFormat: "V:[View(30)]-10-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":titleLabel])
        navigationView.addConstraints(titleLabelH)
        navigationView.addConstraints(titleLabelV)
        titleLabel.text = videoName
        
        //right
        let cropButton = UIButton()
        cropButton.translatesAutoresizingMaskIntoConstraints = false
        navigationView.addSubview(cropButton)
        let cropButtonH = NSLayoutConstraint.constraints(withVisualFormat: "H:[View(100)]-25-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":cropButton])
        let cropButtonV = NSLayoutConstraint.constraints(withVisualFormat: "V:[View(30)]-10-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":cropButton])
        navigationView.addConstraints(cropButtonH)
        navigationView.addConstraints(cropButtonV)
        cropButton.setTitle("切换为填充", for: UIControl.State.normal)
        cropButton.setTitle("切换为等比", for: UIControl.State.selected)
        cropButton.addTarget(self, action: #selector(self.cropBtnClick(sender:)), for: UIControl.Event.touchUpInside)
        
        //底部控制栏
        controllView.translatesAutoresizingMaskIntoConstraints = false
        controllView.backgroundColor = UIColor.init(displayP3Red: 0.1, green: 0.1, blue: 0.1, alpha: 0.25)
        self.view.addSubview(controllView)
        let controllViewH = NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[View]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":controllView])
        let controllViewV = NSLayoutConstraint.constraints(withVisualFormat: "V:[View(100)]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":controllView])
        self.view.addConstraints(controllViewH)
        self.view.addConstraints(controllViewV)
        
        let controllStack = UIStackView()
        controllStack.translatesAutoresizingMaskIntoConstraints = false
        controllView.addSubview(controllStack)
        let controllStackH = NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[View]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":controllStack])
        let controllStackV = NSLayoutConstraint.constraints(withVisualFormat: "V:|-5-[View]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":controllStack])
        controllView.addConstraints(controllStackH)
        controllView.addConstraints(controllStackV)
        
        controllStack.axis = .vertical
        controllStack.alignment = .fill
        controllStack.distribution = .fill

        //进度条
        progressView.value = 0.0
        progressView.tintColor = UIColor.red
        progressView.addTarget(self, action: #selector(self.playerSliderValueChanged(sender:)), for: UIControl.Event.valueChanged)
        progressView.addTarget(self, action: #selector(self.playerSliderTouchDown(sender:)), for: UIControl.Event.touchDown)
        progressView.addTarget(self, action: #selector(self.playerSliderTouchUpInside(sender:)), for: UIControl.Event.touchUpInside)
        
        controllStack.addArrangedSubview(progressView)
        
        //进度条下方的按钮等
        let ctrBtnView = UIView()
        ctrBtnView.backgroundColor = UIColor.clear
        ctrBtnView.heightAnchor.constraint(equalToConstant: 40).isActive = true
        controllStack.addArrangedSubview(ctrBtnView)
        //left
        //播放暂停按钮
        playButton.translatesAutoresizingMaskIntoConstraints = false
        ctrBtnView.addSubview(playButton)
        let playButtonH = NSLayoutConstraint.constraints(withVisualFormat: "H:|-25-[View(30)]", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":playButton])
        let playButtonV = NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[View]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":playButton])
        ctrBtnView.addConstraints(playButtonH)
        ctrBtnView.addConstraints(playButtonV)
        playButton.setImage(UIImage(named: "Play"), for: UIControl.State.normal)
        playButton.setImage(UIImage(named: "Pause"), for: UIControl.State.selected)
        playButton.addTarget(self, action: #selector(self.playOrPauseBtnClick(sender:)), for: UIControl.Event.touchUpInside)
        //下个视频按钮 预留功能支持接收URL数组实现动态切换播放源
        if videoQueue.count > 1 {
            let nextButton = UIButton()
            nextButton.translatesAutoresizingMaskIntoConstraints = false
            ctrBtnView.addSubview(nextButton)
            let nextButtonH = NSLayoutConstraint.constraints(withVisualFormat: "H:[Left]-20-[View(30)]", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":nextButton,"Left":playButton])
            let nextButtonV = NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[View]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":nextButton])
            ctrBtnView.addConstraints(nextButtonH)
            ctrBtnView.addConstraints(nextButtonV)
            nextButton.setImage(UIImage(named: "forward"), for: UIControl.State.normal)
            nextButton.addTarget(self, action: #selector(self.nextBtnClick), for: UIControl.Event.touchUpInside)
        }
        
        //right
        //视频播放时间
        timeLabel.text = "--:--:--/--:--:--"
        timeLabel.font = UIFont(name: timeLabel.font.fontName, size: 14)
        timeLabel.textAlignment = .right
        timeLabel.textColor = UIColor.white
        timeLabel.translatesAutoresizingMaskIntoConstraints = false
        ctrBtnView.addSubview(timeLabel)
        let timeLabelH = NSLayoutConstraint.constraints(withVisualFormat: "H:[View(105)]-25-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":timeLabel])
        let timeLabelV = NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[View]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":timeLabel])
        ctrBtnView.addConstraints(timeLabelH)
        ctrBtnView.addConstraints(timeLabelV)
        //全屏切换按钮
        let fullScreenBtn = UIButton()
        fullScreenBtn.translatesAutoresizingMaskIntoConstraints = false
        ctrBtnView.addSubview(fullScreenBtn)
        let fullScreenBtnH = NSLayoutConstraint.constraints(withVisualFormat: "H:[View(30)]-20-[Right]", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":fullScreenBtn,"Right":timeLabel])
        let fullScreenBtnV = NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[View]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["View":fullScreenBtn])
        ctrBtnView.addConstraints(fullScreenBtnH)
        ctrBtnView.addConstraints(fullScreenBtnV)
        fullScreenBtn.setImage(UIImage(named: "quxiaoquanping"), for: UIControl.State.normal)
        fullScreenBtn.addTarget(self, action: #selector(self.fullScreenBtnClick), for: UIControl.Event.touchUpInside)
        
        //底部做机型适配用的小部件
        let bottomView = UIView()
        bottomView.backgroundColor = UIColor.clear
        bottomView.heightAnchor.constraint(equalToConstant: 20).isActive = true
        controllStack.addArrangedSubview(bottomView)
        
        //默认隐藏导航栏和控制栏
        self.navigationView.alpha = 0
        self.controllView.alpha = 0
        
        //设置视屏frame
        playerLayer.frame = videoView.frame
    }
    
    //显示控制栏
    private func showControll(){
        UIView.animate(withDuration: 0.3) {
            self.controllView.alpha = 1
            self.navigationView.alpha = 1
        }
    }
    
    //隐藏控制栏
    private func hiddenControll(){
        weak var WeakSelf = self
        if self.isTouching == true { return }
        //延时四秒 隐藏控制器
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 4) {
            UIView.animate(withDuration: 0.4, animations: {
                guard let weakSelf = WeakSelf else { return }
                if weakSelf.isTouching == true { return }
                weakSelf.controllView.alpha = 0
                weakSelf.navigationView.alpha = 0
            }) { (finshed) in
                if finshed == true {

                }
            }
        }
    }
    
    /******************主要控件的触摸事件*******************/
    //进度条被触摸
    @objc private func playerSliderTouchDown(sender:UISlider) {
        print("正在拖动进度条")
        isSliding = true
    }
    //拖动进度条
    @objc private func playerSliderValueChanged(sender:UISlider) {
        isSliding = true
        self.avPlayerPause()
        // 跳转到拖拽秒处
        let changedTime = CMTimeMakeWithSeconds(Float64(sender.value), preferredTimescale: Int32(1.0))
        print("changedTime.timescale2 =\(changedTime.timescale)")
        avPlayer.seek(to: changedTime, toleranceBefore: CMTimeMakeWithSeconds(1, preferredTimescale: 1000), toleranceAfter: CMTimeMakeWithSeconds(1, preferredTimescale: 1000)) { (finished) in
            //跳转完成之后
            if finished == true {
                self.avPlayerPlay()
            }
        }
    }
    //手指松开进度条
    @objc private func playerSliderTouchUpInside(sender:UISlider) {
        print("松开进度条")
        isSliding = false
        print("self.progressView.value = \(self.progressView.value)")
    }
    
    //返回
    @objc private func backBtnClick(){
        self.navigationController?.popViewController(animated: true)
    }

    //控制视频的填充方式
    @objc private func cropBtnClick(sender:UIButton){
        sender.isSelected = !sender.isSelected
        if sender.isSelected == true {
            self.playerLayer.videoGravity = .resizeAspectFill
        } else {
            self.playerLayer.videoGravity = .resizeAspect
        }
    }
    
    //播放/暂停按钮事件
    @objc private func playOrPauseBtnClick(sender:UIButton) {
        print("按钮被点击了")
        if playButton.isSelected == true {
            avPlayerPause()
        } else {
            avPlayerPlay()
        }
    }
    
    //下一个视屏按钮事件
    @objc private func nextBtnClick(){
        currentIndex = currentIndex + 1
        if currentIndex >= videoQueue.count {
            currentIndex = 0
        }
        switchPlayBackSource(url: videoQueue[currentIndex])
    }
    
    //全屏按钮事件(目前担任返回)
    @objc private func fullScreenBtnClick(){
        self.navigationController?.popViewController(animated: true)
    }
    
    
    /*************** 基础方法 ********************** */
    //切换播放源
    private func switchPlayBackSource(url:URL) {
        playItem = AVPlayerItem(url: url)
        avPlayer.replaceCurrentItem(with: playItem)
        // 观察status属性,
        playItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
        playerLayer = AVPlayerLayer(player: avPlayer)
        playerLayer.frame = videoView.bounds
        playerLayer.videoGravity = AVLayerVideoGravity.resizeAspect
        if videoView.layer.sublayers != nil && videoView.layer.sublayers!.count > 0 {
            videoView.layer.sublayers!.removeAll()
        }
        videoView.layer.addSublayer(playerLayer)
        avPlayerPlay()
    }

    //播放
    public func avPlayerPlay() {
        avPlayer.play()
        playButton.isSelected = true
    }
    
    //暂停
    public func avPlayerPause() {
        avPlayer.pause()
        playButton.isSelected = false
    }

    //监听进度
    private func monitoringProgress(timescale:CMTimeScale) {
        weak var WeakSelf = self
        playTimeObserver = self.avPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: timescale/1000), queue: DispatchQueue.global()) { (time) in
            DispatchQueue.main.async {
                guard let weakSelf = WeakSelf else { return }
                let currentime:Float = Float(time.value)/Float(time.timescale)
                let timeStr = weakSelf.transToHourMinSec(time: currentime)
                weakSelf.timeLabel.text = timeStr + "/" + weakSelf.maxTime
                if weakSelf.isSliding == false {
                    weakSelf.progressView.value = Float(time.value)/Float(time.timescale)
                }
            }
        }
    }
    
    //把时间转换为00:00:00的格式
    private func transToHourMinSec(time: Float) -> String {
        let allTime: Int = Int(time)
        var hours = 0
        var minutes = 0
        var seconds = 0
        var hoursText = ""
        var minutesText = ""
        var secondsText = ""
        
        hours = allTime / 3600
        hoursText = hours > 9 ? "\(hours)" : "0\(hours)"
        
        minutes = allTime % 3600 / 60
        minutesText = minutes > 9 ? "\(minutes)" : "0\(minutes)"
        
        seconds = allTime % 3600 % 60
        secondsText = seconds > 9 ? "\(seconds)" : "0\(seconds)"

        return "\(hoursText):\(minutesText):\(secondsText)"
    }
}

两种初始化方法

let url = URL()
let playVC = PlayerViewController(url: url)

let urls:Array<URL> = []
let playVC = PlayerViewController(videoName: "VideoName", urls: urls, preferred: 0)

self.navigationController?.pushViewController(playVC, animated: true)

 

 类似资料: