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

引言

播放器的功能功能已经十分完善了,接下来我们给它添加一些提升用户体验的功能。当前市面上的主流播放器几乎都有一个非常友善的功能,用户在退拽进度条的时候可以看见进度条所处进度的视频画面,这对于用户来说是一种直观而且便捷的体验。而对于 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 = 0var currentValue:CMTimeValue = CMTime.zero.valuevar duration:CMTime = CMTime.zeroif case .loaded(let r_duration) = status {duration = r_duration}inscrement = duration.value / pic_countvar 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//3self.imageGenerator?.generateCGImagesAsynchronously(forTimes: times, completionHandler: {[weak self] requestedTime, imageRef, actualTime, result, error inif result == .succeeded,let cgImage = imageRef  {let image = UIImage(cgImage: cgImage)let thumbnail = PHThumbnailModel(time: actualTime, image: image)thumbnails.append(thumbnail)}count -= 1if 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 UIKitclass PHPicProgressView: UIView {/// 按钮缓存池var buttons = [UIButton]()/// 加载图片进度条////// - Parameters:///   - thumbnails: 缩略图资源数组func loadThumbnails(thumbnails:[PHThumbnailModel]) {for i in 0 ..< thumbnails.count {let thumbnail = thumbnails[i]var button:UIButton? = nilif i < buttons.count {button = buttons[i]} else {button = UIButton()self.addSubview(button!)buttons.append(button!)}button?.tag = 100 + ibutton?.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.heightfor 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 UIKitlet offset_x = 30.0
let play_width = 40.0class PHControlView: UIView,PHControlDelegate {..../// 图片进度条let picProgressView = PHPicProgressView()/// 图片高度var picHeight = 0.0override 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 = truedelegate.scrubbingDidStart()if picProgressView.isHidden {picProgressView.isHidden = false}}// 进度条拖拽完成@objc func endSlider() {guard let delegate = delegate else { return }playButton.isSelected = falsedelegate.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.0let item_height = item_width * image.size.height / image.size.widthpicHeight = item_heightlayoutSubviews()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.hqwc.cn/news/519018.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue2的element UI 表格单选

代码 this.$refs.multipleTable.toggleRowSelection(selection.shift(), false);multipleTable 是定义的表格的ref

国产蓝鹏测控测径仪如何?

随着国力增强&#xff0c;中国制造品质提升&#xff0c;不仅仅是国外更多人认可&#xff0c;国内对国产制品也接受良好&#xff0c;测径仪这种智能测量设备&#xff0c;很多国内外厂家也在用国产设备。 测径仪厂家 蓝鹏测控作为智能几何尺寸测量仪生产厂家&#xff0c;已有10多…

高效提升控制效率 | 基于ACM32 MCU的LED灯箱控制器方案

LED灯箱上各种文字、图案有序跳跃、交替辉映&#xff0c;产生强烈的视觉冲击力&#xff0c;被广泛应用于商场、美容美发、宾馆、娱乐场所等地方。 锁存器的工作原理 在LED和数码管显示方面&#xff0c;要维持一个数据的显示&#xff0c;往往要持续的快速的刷新。尤其是在四段八…

Mybatis-Plus——07,性能分析插件

性能分析插件 一、导入插件二、SpringBoot中配置环境为dev或test环境三、运行测试————————创作不易&#xff0c;笔记不易&#xff0c;如觉不错&#xff0c;请三连&#xff0c;谢谢~~ MybatisPlus也提供了性能分析插件&#xff0c;如果超过这个时间就停止运行&#xff0…

1.3 数据库系统的结构

目录 1.3.1 数据库系统模式的概念 1.3.2 数据库系统的三级模式结构 1. 模式 2. 外模式 3.内模式&#xff08;也称存储模式&#xff09; 1.3.3 数据库的二级映像功能与数据独立性 1.外模式&#xff0f;模式映像 2.模式&#xff0f;内模式映像 1.3.4 总结 模式 内模式…

nicegui学习使用

https://www.douyin.com/shipin/7283814177230178363 python轻量级高自由度web框架 - NiceGUI (6) - 知乎 python做界面&#xff0c;为什么我会强烈推荐nicegui 秒杀官方实现&#xff0c;python界面库&#xff0c;去掉90%事件代码的nicegui python web GUI框架-NiceGUI 教程…

C++前置声明的学习

【C】C中前置声明的应用与陷阱_前置生命如何使用-CSDN博客 首先&#xff0c;这样写会报错&#xff1a; #pragma once #include "A.h" class B {A a; public:B(void);~B(void); };#include "B.h" B::B(void) { }B::~B(void) { } #pragma once #include &…

【LeetCode】并查集OJ

目录 1.省份数量 2. 等式方程的可满足性 1.省份数量 题目地址&#xff1a; 547. 省份数量 - 力扣&#xff08;LeetCode&#xff09; 解题思路&#xff1a;对于该题我们直接使用并查集&#xff0c;将可以直接的城市都归类一个集合&#xff0c;最后统计数组中集合的总数就是…

Linux Ubuntu部署SVN服务端结合内网穿透实现客户端公网访问

文章目录 前言1. Ubuntu安装SVN服务2. 修改配置文件2.1 修改svnserve.conf文件2.2 修改passwd文件2.3 修改authz文件 3. 启动svn服务4. 内网穿透4.1 安装cpolar内网穿透4.2 创建隧道映射本地端口 5. 测试公网访问6. 配置固定公网TCP端口地址6.1 保留一个固定的公网TCP端口地址6…

docker部署若依项目

目录 目录 一、搭建局域 二、redis安装 1.创建目录 2. redis.conf修改 三、MySQL安装 1. 安装 2. 设置远程连接 3. 创建数据库 四、若依后端项目搭建 1. 切换到家目录 2. 上传jar包 3. 上传Dockerfile文件 4. 构建镜像 5. 运行容器 6. 查看运行情况 7. 测试(自己…

实用干货!产品经理的进阶秘籍:必备的10项核心技能

在现代商业环境中&#xff0c;产品经理的角色变得愈发关键和多样化。成功的产品经理不仅需要深厚的行业知识&#xff0c;还要具备一系列多面手的技能&#xff0c;以应对不断变化的市场需求和竞争压力。本文将深入探讨产品经理必备的10项核心技能&#xff0c;揭示职场进阶的秘籍…

个推与华为深度合作,成为首批支持兼容HarmonyOS NEXT的服务商

自华为官方宣布HarmonyOS NEXT鸿蒙星河版开放申请以来&#xff0c;越来越多的头部APP宣布启动鸿蒙原生开发&#xff0c;鸿蒙生态也随之进入全新发展的第二阶段。 作为华为鸿蒙生态的重要合作伙伴&#xff0c;个推一直积极参与鸿蒙生态建设。为帮助用户在HarmonyOS NEXT上持续享…