当前位置: 首页 > news >正文

(三)深入了解AVFoundation-播放:AVPlayer 进阶 播放状态 进度监听全解析


引言

在上一部分中,我们介绍了如何使用 AVPlayer 来实现视频的基础播放功能,并讲解了 AVAsset、AVPlayerItem 和 AVPlayer 的基本配置。通过这些内容,我们能够实现视频的加载与播放。然而,在实际的应用场景中,视频播放的流畅性不仅仅依赖于如何播放视频,还涉及到如何优化播放过程中各种状态和进度的管理。

AVPlayer 提供了多种监听机制,包括播放状态、缓冲状态、播放进度等,通过合理利用这些监听,我们可以实现更精准的播放控制,如优化加载、减少卡顿、提升用户体验等。

本篇博客将带你深入探讨 AVPlayer 的状态监听、缓冲状态监听、播放进度监听等优化技术,并以此为基础,我们将开始构建一个 PHPlayerController 播放控制类,来更好地管理播放过程中的各项操作和优化策略。

PHPlayerController 播放控制类

PHPlayerController 将是一个我们用来封装所有播放器功能的播放器管理类,它的主要功能会包括:

  1. 创建 AVAsset、AVPlayerItem 以及初始化 AVPlayer。
  2. 监听播放状态、缓冲状态、播放进度、播放完成等。
  3. 对外暴露播放、暂停、快进、倍速等方法。

它的基本代码如下:

import UIKit
import AVFoundation

class PHPlayerController:NSObject {
    
    /// 媒体资源
    private var asset: AVAsset?
    /// 视频资源AVPlayerItem
    private var playerItem: AVPlayerItem!
    /// 播放器
    private var player: AVPlayer!
    
    
    
    /// 播放视频
    /// - Parameter url: 视频地址
    func play(url: URL) {
        ...
        let asset = AVURLAsset(url: url)
        playerItem = AVPlayerItem(asset: asset)
        if player == nil {
            player = AVPlayer(playerItem: playerItem)
        } else {
            player.replaceCurrentItem(with: playerItem)
        }
        player.play()
        self.asset = asset
        ....
    }

PHPlayerController基本代码中包含了播放器的几个主要组成部分,在后面我们会陆陆续续的往里添加新的功能和方法。

五大监听解析

一个播放器,并不是说只有 “播放” 功能就算完成了。在实际的项目开发中,我们需要对播放器进行更加精细的控制,比如:

  • 监听加载状态,确保视频能够正常播放。
  • 监听缓冲进度,反馈给用户视频的加载进度。
  • 追踪播放进度,实现进度条更新,提供良好的用户体验。
  • 监听播放完成,以便执行自动重播或播放下一个视频资源。
  • 监听播放状态,比如暂停、播放中、缓冲中,以优化UI展示反馈给用户。

通过这些监听,我们可以更好地掌控播放器的行为、让视频播放更加流程,提升用户体验。

这些监听可以分为两部分,前半部分是对 AVPlayerItem 播放资源的监听,而后半部分是对AVPlayer播放器的监听。

监听加载状态(AVPlayerItem.status)

AVPlayerItem 的 status 属性用于检查视频是否已经加载完成,以及视频是否可以播放。

这是我们在进行视频播放时,最先进行的一步监听,当 AVPlayerItem 绑定到 AVPlayer 后就会开始准备资源,当我们执行play() 方法时,一旦资源准备完毕,就会直接开始自动播放。

监听加载状态:

/// 监听 AVPlayerItem状态的keypath
private let kStatusKeyPath = "status"
/// 监听 AVPlayerItem 的标识
private var kPlayerItemStatusContext = 0


    /// 监听AVPlayerItem的加载状态
    private func _addObserverAVPlayerItemStatus() {
        print("PHPlayerController: addObserverAVPlayerItemStatus")
        playerItem.addObserver(self, forKeyPath: kStatusKeyPath, options: .new, context: &kPlayerItemStatusContext)
    }

    /// 移除监听AVPlayerItem的加载状态
    private func _removeObserverAVPlayerItemStatus() {
        print("PHPlayerController: removeObserverAVPlayerItemStatus")
        playerItem.removeObserver(self, forKeyPath: kStatusKeyPath, context: &kPlayerItemStatusContext)
    }

重写observeValue(forKeyPath:of:change:context)方法,实现监听。

    //MARK: - KVO
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if context == &kPlayerItemStatusContext {
            if keyPath == kStatusKeyPath {
                // 处理AVPlayerItem状态监听
                _observerAVPlayerItemStatus(change)
            }
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }

    /// 处理 AVPlayerItem监听
    /// - Parameter change: change
    private func _observerAVPlayerItemStatus(_ change: [NSKeyValueChangeKey : Any]?) {
        let status = change?[.newKey] as? AVPlayerItem.Status
        switch status {
        case .failed:
            print("PHPlayerController: AVPlayerItem failed")
        case .readyToPlay:
            print("PHPlayerController: AVPlayerItem readyToPlay")
        case .unknown:
            print("PHPlayerController: AVPlayerItem unknown")
        default:
            break
        }
    }

监听缓冲进度(AVPlayerItem.loadedTimeRanges)

loadedTimeRanges 属性可以用于获取缓冲数据的时间范围,可以根据数据给用户视觉上缓冲进度反馈,当然如果没有过分追求细节,这个监听其实可以省略,依赖播放状态来显示缓冲样式。

监听缓冲进度:

/// 监听 AVPlayerItem 的缓冲进度的keypath
private let kLoadedTimeRangesKeyPath = "loadedTimeRanges"
/// 监听 AVPlayerItem 的缓冲进度的标识
private var kPlayerItemLoadedTimeRangesContext = 1

    /// 监听 AVPlayerItem 的缓冲进度
    private func _addObserverAVPlayerItemLoadedTimeRanges() {
        print("PHPlayerController: addObserverAVPlayerItemLoadedTime")
        playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: &kPlayerItemLoadedTimeRangesContext)
    }

    /// 移除监听 AVPlayerItem 的缓冲进度
    private func _removeObserverAVPlayerItemLoadedTimeRanges() {
        print("PHPlayerController: removeObserverAVPlayerItemLoadedTime")
        playerItem.removeObserver(self, forKeyPath: "loadedTimeRanges", context: &kPlayerItemLoadedTimeRangesContext)
    }

在observeValue(forKeyPath:of:change:context)方法内实现监听的处理。

    //MARK: - KVO
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if context == &kPlayerItemStatusContext {
             ...
        } else if context == &kPlayerItemLoadedTimeRangesContext {
            if keyPath == kLoadedTimeRangesKeyPath {
                // 处理AVPlayerItem缓冲进度监听
                _observerAVPlayerItemLoadedTimeRanges(change)
            }
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }

监听播放完成(AVPlayerItemDidPlayToEndTime)

当视频资源播放完成时,我们可以通过监听 AVPlayerItem 的 AVPlayerItemDidPlayToEndTime,然后执行相关的操作,比如自动重播。

    /// 监听播放完成
    private func _addObserverAVPlayerItemDidPlayToEndTime() {
        NotificationCenter.default.addObserver(self, selector: #selector(_observerAVPlayerItemDidPlayToEndTime), name: .AVPlayerItemDidPlayToEndTime, object: nil)
    }
    
    /// 处理播放完成
    /// - Parameter notification: notification
    @objc private func _observerAVPlayerItemDidPlayToEndTime(_ notification: Notification) {
        print("PHPlayerController: AVPlayerItemDidPlayToEndTime")
        // 播放完成后,重新播放
        player.seek(to: .zero)
        player.play()
    }

监听(读取)播放进度(addPeriodicTimeObserver)

对于播放进度,其实并没有所谓的监听,也没有办法使用KVO的形式来监听播放进度。AVPlayer 为我们提供了一个 addPeriodicTimeObserver(forInterval:queue:using:) 方法来定期读取播放进度。

该方法需要我们传入一个读取的时间间隔、回调的队列、和回调的block。

    /// 监听播放进度的观察者
    private var timeObserver: Any?

    /// 监听播放进度
    private func _addPeriodicTimeObserver() {
        player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: .main) { [weak self] (time) in
            let currentTime = CMTimeGetSeconds(time)
            print("PHPlayerController: currentTime: \(currentTime)")
        }
    }

    /// 移除播放进度监听
    private func _removePeriodicTimeObserver() {
        if let observer = timeObserver {
            player.removeTimeObserver(observer)
            timeObserver = nil
        }
    }

监听播放状态(AVPlayer.timeControlStatus)

timeControlStatus 属性可以用于判断当前播放器是在播放、暂停还是等待缓冲的状态。

监听播放状态:

/// 监听 AVPlayer 的播放状态的keypath
private let kPlayerTimeControlStatus = "timeControlStatus"
/// 监听 AVPlayer 的播放状态的标识
private var kPlayerTimeControlStatusContext = 2


    /// 监听播放状态
    private func _addObserverAVPlayerTimeControlStatus() {
        print("PHPlayerController: addObserverAVPlayerTimeControlStatus")
        player.addObserver(self, forKeyPath: kPlayerTimeControlStatus, options: .new, context: &kPlayerTimeControlStatusContext)
    }

    /// 移除监听播放状态
    private func _removeObserverAVPlayerTimeControlStatus() {
        print("PHPlayerController: removeObserverAVPlayerTimeControlStatus")
        player.removeObserver(self, forKeyPath: kPlayerTimeControlStatus, context: &kPlayerTimeControlStatusContext)
    }

处理播放状态的监听:

    //MARK: - KVO
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if context == &kPlayerItemStatusContext {
            ...
        } else if context == &kPlayerItemLoadedTimeRangesContext {
            ...
        } else if context == &kPlayerTimeControlStatusContext {
            if keyPath == kPlayerTimeControlStatus {
                // 处理AVPlayer播放状态
                _observerAVPlayerTimeControlStatus(change)
            }
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }


    /// 处理播放状态
    /// - Parameter change: change
    private func _observerAVPlayerTimeControlStatus(_ change: [NSKeyValueChangeKey : Any]?) {
        let status = change?[.newKey] as? AVPlayer.TimeControlStatus
        switch status {
        case .paused:
            print("PHPlayerController: AVPlayer TimeControlStatus paused")
        case .playing:
            print("PHPlayerController: AVPlayer TimeControlStatus playing")
        case .waitingToPlayAtSpecifiedRate:
            print("PHPlayerController: AVPlayer TimeControlStatus waitingToPlayAtSpecifiedRate")
        default:
            break
        }
    }

    

结语

通过对 AVPlayerItem 和 AVPlayer 的5个关键点监听,我们可以全面掌控视频的播放状态,根据这些状态和数据,我们可以进行UI的视觉反馈,反馈给用户,从而提升播放体验。本篇博客还是实现了 PHPlayerController 播放管理器,为后序的播放器博客内容打下基础。

希望通过本篇博客的分享,能够帮助大家进步了解 AVFoundation 中的视频播放细节,在本系列博客的下一篇博客当中,将会分享,使用通过自定义UI控件 结合 PHPlayerController 实现自定义播放控制的UI,包括播放、暂停、进度条以及拖拽快进功能。

http://www.dtcms.com/a/118139.html

相关文章:

  • Spring Boot 自动装配原理
  • 前端如何检测项目中新版本的发布?
  • 聊聊Spring AI的RedisVectorStore
  • Lua 第5部分 表
  • 图的储存+图的遍历
  • Spring Boot 整合 Servlet三大组件(Servlet / Filter / Listene)
  • 开源大语言模型智能体应用开发平台——Dify
  • 项目复杂业务的数据流解耦处理方案整理
  • Java命令模式详解
  • Java面试39-Zookeeper中的Watch机制的原理
  • 前端服务配置详解:从入门到实战
  • 鸿蒙版小红书如何让图库访问完全由“你”掌控
  • 2025.04.07【数据科学新工具】| dynverse:数据标准化、排序、模拟与可视化的综合解决方案
  • MQTT-Dashboard-数据集成-WebHook、日志管理
  • 深入理解STAR法则
  • 如何开通google Free Tier长期免费云服务器(1C/1G)
  • Python----计算机视觉处理(Opencv:道路检测之车道线显示)
  • SpringWebFlux测试:WebTestClient与StepVerifier
  • 学透Spring Boot — 010. 单元测试和Spring Test
  • 青少年编程与数学 02-015 大学数学知识点 08课题、信息论
  • MySQL + ngram 最佳实践:轻量级中文 混合内容全文搜索方案
  • 秒杀系统设计方案
  • 一周学会Pandas2 Python数据处理与分析-NumPy数组的索引和切片
  • ResNet改进(21):基于ECA注意力机制的ResNet18网络实现
  • golang 内存逃逸 栈与堆区别
  • 如何解决:http2: Transport received Server‘s graceful shutdown GOAWAY
  • qemu仿真调试esp32,以及安装版和vscode版配置区别
  • 字符串操作栈和队列
  • MES生产工单管理系统,Java+Vue,含源码与文档,实现生产工单全流程管理,提升制造执行效率与精准度
  • C++使用Qt Charts可视化大规模点集