七.AV Foundation 视频播放 - 图片进度条

news2024/10/24 15:14:37

引言

播放器的功能功能已经十分完善了,接下来我们给它添加一些提升用户体验的功能。当前市面上的主流播放器几乎都有一个非常友善的功能,用户在退拽进度条的时候可以看见进度条所处进度的视频画面,这对于用户来说是一种直观而且便捷的体验。而对于 iOS 平台的应用程序而言,实现这样的功能也并不复杂。在本文中,我们将探讨如何在 iOS 应用程序中实现图片进度条,以便用户在拖动进度条时能够即时预览视频的画面。通过添加这样一个小但强大的功能,我们可以进一步提升用户与应用程序之间的互动性,使观看视频的体验更加流畅和愉快。

原理

AV Foundation框架为我们提供了一个名为AVAssetImageGenerator的类,专门用来从一个AVAsset资源中提取图片,它为我们提供了两个方法:

  • 获取一张图片

generateCGImageAsynchronously(for: <#T##CMTime#>, completionHandler: <#T##(CGImage?, CMTime, Error?) -> Void#>)

该方法用于异步生成给定时间点的CGImage对象,

  1. for: 这是一个CMTime类型的参数,表示要生成图像的时间点。CMTime是Core Media框架中用于表示时间的数据类型,可以理解为一个精确的时间值。

  2. completionHandler: 这是一个闭包类型的参数,用于在生成图像完成时进行回调。闭包接受三个参数:

    • CGImage?: 生成的图像对象,如果生成失败则为nil。
    • CMTime: 表示生成图像的时间点,这个时间点可能与传入的时间点不完全相同,会受到视频帧率等因素的影响。
    • Error?: 如果生成图像过程中出现错误,则会传递一个Error对象,否则为nil。

这个方法的使用场景通常是在需要从视频中获取某个时间点的图像时,可以异步调用该方法,并在完成后通过闭包获取生成的图像。由于图像生成是一个比较耗时的操作,因此使用异步方法可以避免阻塞主线程。

  • 获取一组图片

generateCGImagesAsynchronously(forTimes: <#T##[NSValue]#>, completionHandler: <#T##AVAssetImageGeneratorCompletionHandler##AVAssetImageGeneratorCompletionHandler##(CMTime, CGImage?, CMTime, AVAssetImageGenerator.Result, Error?) -> Void#>)

该方法用于异步生成给定时间点数组的CGImage对象数组,

  1. forTimes: 这是一个[NSValue]类型的参数,表示要生成图像的时间点数组。每个时间点都由一个CMTime对象封装在NSValue中。你可以传递一个包含多个时间点的数组,生成器会按顺序为每个时间点生成相应的图像。

  2. completionHandler: 这是一个闭包类型的参数,用于在生成图像完成时进行回调。闭包接受五个参数:

    • CMTime: 表示生成图像的时间点,这个时间点可能与传入的时间点不完全相同,会受到视频帧率等因素的影响。
    • CGImage?: 生成的图像对象,如果生成失败则为nil。
    • CMTime: 表示生成图像的实际时间点,与传入时间点相匹配。
    • AVAssetImageGenerator.Result: 表示生成图像的结果,是一个枚举类型,可能的取值有.success表示成功生成图像,.failed表示生成失败。
    • Error?: 如果生成图像过程中出现错误,则会传递一个Error对象,否则为nil。

这个方法的使用场景通常是在需要从视频中获取多个时间点的图像时,可以异步调用该方法,并在完成后通过闭包获取生成的图像数组。与单个时间点的方法相比,这个方法适用于批量生成图像的情况,能够提高效率。

实现

给现有播放器新增这样一个图片的可视进度条,在这里我们拆分成两部分,分别从数据处理、UI渲染来实现来这个整体的图片进度条功能。

数据处理:

我们需要绘制一个视频的进度条,用到的一定是多张图片,所以我们采用第二种方式来获取一组图片。同样我们还是需要在视频准备开始播放的时候调用新创建的方法generateThumbnails开始处理图片数据。

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if context == &playerItemContext {
            guard let playerItem = playerItem else { return }
            guard let player = player else { return }
            if playerItem.status == .readyToPlay{
                playerItem.removeObserver(self, forKeyPath: status_keypath)
                player.play()
                let duration = playerItem.duration
                // 同步页面开始播放
                self.delegate?.playstart()
                // 同步时间
                self.delegate?.setCuttentTime(time: 0.0, duration: CMTimeGetSeconds(duration))
                // 设置标题
                let assetTitle = assertTitle()
                self.delegate?.setTitle(title: assetTitle)
                // 设置字幕
                let subtitles = loadMediaOptions()
                self.delegate?.setSubtitle(titles: subtitles)
                // 监听播放进度
                addPlayerItemTimeObserver()
                // 监听播放完成
                addItemEndObserverForPlayerItem()
                // 获取缩略图片
                generateThumbnails()
            }
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }

...
// 获取一组缩略图片
    func generateThumbnails() {
        
    }

generateThumbnails方法的实现

// 获取一组缩略图片
    func generateThumbnails() {
        //1.
        guard let asset = asset else { return }
        imageGenerator = AVAssetImageGenerator(asset: asset)
        guard let imageGenerator = imageGenerator else { return }
        imageGenerator.maximumSize = CGSize(width: 100.0, height: 0.0)
        //2.
        let status = asset.status(of: .duration)
        //定义增量
        var inscrement:CMTimeValue = 0
        var currentValue:CMTimeValue = CMTime.zero.value
        var duration:CMTime = CMTime.zero
        if case .loaded(let r_duration) = status {
            duration = r_duration
        }
        inscrement = duration.value / pic_count
        var times:[NSValue] = [NSValue]()
        while currentValue <= duration.value {
            let time = CMTime(value: currentValue, timescale: duration.timescale)
            times.append(NSValue(time: time))
            currentValue += inscrement
        }
        var thumbnails = [PHThumbnailModel]()
        var count = pic_count
        //3
        self.imageGenerator?.generateCGImagesAsynchronously(forTimes: times, completionHandler: {[weak self] requestedTime, imageRef, actualTime, result, error in
            if result == .succeeded,let cgImage = imageRef  {
                let image = UIImage(cgImage: cgImage)
                let thumbnail = PHThumbnailModel(time: actualTime, image: image)
                thumbnails.append(thumbnail)
            }
            count -= 1
            if count == 0 {
                guard let self = self else { return }
                DispatchQueue.main.async() {
                    self.loadPicProgressView(thumbnails: thumbnails)
                }
            }
        })

    }

上面的方法内容较多,之前也没有提及过相关的内容,所以在这里单独做一下解释。该方法大概可以分为三个部分:

1.创建一个AVAssetImageGenerator并指定maximumSize属性。指定一个width值为100、height值为0的CGSize。这样可以确保生成的图片都遵循一定宽度,并且会根据视频的宽高比自动设置高度值。

2.计算出需要获取图片时间点的数组。从视频中均匀的获取20个时间点创建一个时间数组。

3.加载图片。调用AVAssetImageGenerator提供的方法获取一组图片资源,当获取的资源数量与我们指定的数量相等时,则认为资源加载完毕开始渲染。

其中我们将数据构建成了一个PHThumbnailModel的数据模型,里面存放了时间信息和图片信息。

UI渲染:

创建一个名为PHPicProgressView的类继承自UIView,内部只定义了一个属性buttons用来缓存已经创建的button,还有一个loadThumbnails方法用来接收传入的PHThumbnailModel数据。

import UIKit



class PHPicProgressView: UIView {
    
    /// 按钮缓存池
    var buttons = [UIButton]()

    /// 加载图片进度条
    ///
    /// - Parameters:
    ///   - thumbnails: 缩略图资源数组
    func loadThumbnails(thumbnails:[PHThumbnailModel]) {
        for i in 0 ..< thumbnails.count {
            let thumbnail = thumbnails[i]
            var button:UIButton? = nil
            if i < buttons.count {
                button = buttons[i]
            } else {
                button = UIButton()
                self.addSubview(button!)
                buttons.append(button!)
            }
            button?.tag = 100 + i
            button?.setImage(thumbnail.image, for: .normal)
//            button?.addTarget(self, action: #selector(thumbnailOnclick), for: .touchUpInside)
        }
    }
    
    /// 缩略图点击
//    @objc func thumbnailOnclick(button:UIButton) {
//        
//    }

    override func layoutSubviews() {
        super.layoutSubviews()
        let button_width = self.bounds.width / CGFloat(buttons.count)
        let button_height = self.bounds.height
        for i in 0 ..< buttons.count {
            let button = buttons[i]
            button.frame = CGRect(x: 0.0 + CGFloat(i) * button_width, y: 0.0, width: button_width, height: button_height)
        }
    }
}

在PHControlView中添加PHPicProgressView,注意需要添加到进度条和当前时间标签的下层,避免出现遮挡。

import UIKit

let offset_x = 30.0
let play_width = 40.0

class PHControlView: UIView,PHControlDelegate {
    
     ....
    /// 图片进度条
    let picProgressView = PHPicProgressView()
    /// 图片高度
    var picHeight = 0.0
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
        setEvents()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupView() {
          ...
        self.addSubview(picProgressView)
        picProgressView.isHidden = true
          ...
    }
    ...
    
    // 进度条开始拖拽
    @objc func startSlider() {
        guard let delegate = delegate else { return }
        playButton.isSelected = true
        delegate.scrubbingDidStart()
        if picProgressView.isHidden {
            picProgressView.isHidden = false
        }
    }
    
    
    // 进度条拖拽完成
    @objc func endSlider() {
        guard let delegate = delegate else { return }
        playButton.isSelected = false
        delegate.scrubbedDidEnd(time: TimeInterval(sliderView.value))
        if !picProgressView.isHidden {
            picProgressView.isHidden = true
        }
    }
    
    
    /// 加载图片进度条
    func loadPicPogressView(thumbnails: [PHThumbnailModel]) {
        if let image = thumbnails.first?.image {
            let item_width = (self.bounds.size.width - offset_x*2)/10.0
            let item_height = item_width * image.size.height / image.size.width
            picHeight = item_height
            layoutSubviews()
            picProgressView.loadThumbnails(thumbnails: thumbnails)
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        ....
        picProgressView.frame = CGRect(x: offset_x, y: CGRectGetMaxY(currentTimeLabel.frame) - 30.0, width: self.bounds.size.width - offset_x*2, height: picHeight)
    }
    
}

在UI部分有两处需要说明的代码:

1.在picProgressView添加到父视图的时候,首先进行了隐藏处理,在进度条开始拖拽的时候设置为显示,结束拖拽的时候设置为隐藏。只在用户拖拽进度的过程中显示图片进度条。

2.loadPicPogressView方法,是在PHControlDelegate协议中声明的一个新的方法,目的是通知PHControlView开始加载图片进度条,并传入图片进度条相关数据。

图片进度条

结语

在本文中,我们探讨了如何在 iOS 应用程序中实现图片进度条的功能,以提升用户体验。通过使用 AVFoundation 框架中的 AVAssetImageGenerator 类,我们能够轻松地从视频中获取指定时间点的图像,并将其应用于进度条的展示中。这个小小的功能不仅使用户能够更直观地预览视频内容,还为应用程序增添了更多的交互性和便利性。

当然,除了本文介绍的方法外,还有许多其他的技术和功能可以进一步改进和丰富应用程序的视频播放体验。希望本文能够为您提供一些启发,并在您的开发工作中发挥一定的作用。感谢您的阅读!

如果您有任何问题、建议或想要分享您的经验,请随时在评论区留言,我们期待与您进一步的交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1497889.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

今天分享一个好看的输入法皮肤相信每个人心里住着一个少女心我们美化一下她吧

标题&#xff1a; 白日梦皮肤上线&#xff0c;百度输入法助你开启梦幻之旅&#xff01; 正文&#xff1a; 大家好呀&#xff01;今天我来给大家安利一款超级梦幻的百度输入法皮肤——“白日梦”系列&#xff01; 这款皮肤的设计灵感来源于我们内心深处的白日梦&#xff0c;充…

【Python】新手入门(9):数值和序列

&#x1f40d;【Python】新手入门&#xff08;9&#xff09;&#xff1a;数值和序列 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&am…

MySQL为什么要用B+树?

二叉树&#xff08;二叉查找树&#xff09; 平衡二叉树&#xff08;B树就是B-树&#xff09;(解决了二叉查找树的极端情况&#xff09; Q&#xff1a;具体是怎么解决的呢&#xff1f; A&#xff1a; 树左右两边层数相差不大于1一旦符合条件1的时候&#xff0c;就进行左旋/右…

重装系统后正版office如何安装

前言 重装系统后&#xff0c;正版office如何安装 登录官网 https://www.microsoft.com 下载office https://account.microsoft.com/services

KOA优化最近邻分类预测(matlab代码)

KOA-最近邻分类预测matlab代码 开普勒优化算法&#xff08;Kepler Optimization Algorithm&#xff0c;KOA&#xff09;是一种元启发式算法&#xff0c;灵感来源于开普勒的行星运动规律。该算法模拟行星在不同时间的位置和速度&#xff0c;每个行星代表一个候选解&#xff0c;…

STM32标准库——(19)PWR电源控制

1.PWR简介 PWR属于外设部分 调用时需要先开启时钟 2.电源框图 这个图可以分为三个部分&#xff0c;最上面是模拟部分供电叫做VDDA&#xff0c;中间是数字部分供电&#xff0c;包括两块区域&#xff0c;VDD供电区域和1.8v供电区域&#xff0c;下面是后备供电&#xff0c;叫做VB…

python中的文件操作2

文件遍历 在Python中&#xff0c;遍历文件通常指的是逐行读取文件中的内容。这种方式对于处理大型文件特别有用&#xff0c;因为它不需要一次性将整个文件加载到内存中。下面是几种常见的遍历文件内容的方法&#xff1a; 1. 使用with语句和for循环 这是最推荐的方式&#xf…

模拟实现std::string类(包含完整、分文件程序)

std库中的string是一个类&#xff0c;对string的模拟实现&#xff0c;既可以复习类的特性&#xff0c;也可以加深对std::string的理解。 &#x1f308;一、搭建框架 ☀️1.新命名空间 本质上string是一个储存在库std里面的类&#xff0c;现在需要模拟实现一个string类&#…

MySQl基础入门④

上一遍知识内容 1.使用语句方式选择与查看数据库 打开我们navicat软件之后。 先不要点左边的连接内容&#xff0c;而是直接点击工具&#xff0c;再点击命令界面&#xff0c;也可以直接快捷键F6启动。 在我们的命令窗口输入以下命令&#xff1a; USE dataname;按回车。 这个语…

片上网络(NoC)技术的发展及其给高端FPGA带来的优势

片上网络(NoC)技术的发展及其给高端FPGA带来的优势 1. 概述 在摩尔定律的推动下,集成电路工艺取得了高速发展,单位面积上的晶体管数量不断增加。 片上系统(System-on-Chip,SoC)具有集成度高、功耗低、成本低等优势,已经成为大规模集成电路系统设计的主流方向,解决了…

RHEL9实现系统间复制文件

以下环境使用的是RHEL9学员练习环境 文件传送至远端 - sftp sftp是一个文件传输程序&#xff0c;类似于ftp目标可以指定为[user]host[:path]或URL格式为sftp://[user]host[:port][/path]sftp和ftp都是FTP协议的客户端工具&#xff0c;工作模式和使用方式均相同&#xff0c;但是…

Linux文件描述符剖析

文章目录 文件描述符文件描述符分配规则重定向软硬链接软链接&#xff08;Symbolic Link&#xff09;&#xff1a;硬链接&#xff08;Hard Link&#xff09;&#xff1a; 文件描述符 文件描述符&#xff08;File Descriptor&#xff09;是一个非负整数&#xff0c;用于标识打开…

【电路笔记】-NPN晶体管

NPN晶体管 文章目录 NPN晶体管1、概述2、双极NPN晶体管配置3、NPN晶体管中的α和β关系4、示例5、共发射极配置1、概述 NPN 晶体管是三端三层器件,可用作放大器或电子开关。 在前面的文章中,我们看到标准双极晶体管或 BJT 有两种基本形式。 NPN(负-正-负)配置和PNP(正-负…

腾讯云服务器和阿里云服务器哪家更优惠?2024价格对比

2024年阿里云服务器和腾讯云服务器价格战已经打响&#xff0c;阿里云服务器优惠61元一年起&#xff0c;腾讯云服务器61元一年&#xff0c;2核2G3M、2核4G、4核8G、4核16G、8核16G、16核32G、16核64G等配置价格对比&#xff0c;阿腾云atengyun.com整理阿里云和腾讯云服务器详细配…

好物周刊#44:现代终端工具

https://github.com/cunyu1943 村雨遥的好物周刊&#xff0c;记录每周看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;每周五发布。 一、项目 1. Github-Hosts 通过修改 Hosts 解决国内 Github 经常抽风访问不到&#xff0c;每日更新。 2. 餐饮点餐商城 针对…

火柴排队(逆序对 + 离散化)

505. 火柴排队 原题链接 思路 如下是画图分析的一些过程 在这里贪心的思路是排序&#xff0c;然后两个数组都是从小到大那样对应的话最终的答案可达到最小 而我们只能交换相邻的火柴&#xff0c;故在这里先假设一个简化版本&#xff0c;即A有序&#xff0c;而只需要对B进行…

【学习笔记】数据结构与算法06 - 堆:上堆、下堆、Top-K问题以及代码实现

知识来源&#xff1a;https://www.hello-algo.com/chapter_heap/heap/#4 文章目录 2.5 堆2.5.1 堆&#xff08;优先队列2.5.1.1 堆的常用操作 2.5.2 堆的存储与表示2.5.2.1 访问堆顶元素2.5.2.2 入堆时间复杂度 2.5.2.3 堆顶元素出堆时间复杂度 2.5.3 堆的常见应用2.5.4 建堆问…

WEB自动化测试----------Webdriver API 的使用

&#x1f525; 交流讨论&#xff1a;欢迎加入我们一起学习&#xff01; &#x1f525; 资源分享&#xff1a;耗时200小时精选的「软件测试」资料包 &#x1f525; 教程推荐&#xff1a;火遍全网的《软件测试》教程 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1…

PTA L2-015 互评成绩

学生互评作业的简单规则是这样定的&#xff1a;每个人的作业会被k个同学评审&#xff0c;得到k个成绩。系统需要去掉一个最高分和一个最低分&#xff0c;将剩下的分数取平均&#xff0c;就得到这个学生的最后成绩。本题就要求你编写这个互评系统的算分模块。 输入格式&#xf…

[项目设计] 从零实现的高并发内存池(五)

&#x1f308; 博客个人主页&#xff1a;Chris在Coding &#x1f3a5; 本文所属专栏&#xff1a;[高并发内存池] ❤️ 前置学习专栏&#xff1a;[Linux学习] ⏰ 我们仍在旅途 ​ 目录 8 使用定长内存池脱离new 9. 释放对象时不传大小 10.性能优化 10.1…