引言
播放器的功能功能已经十分完善了,接下来我们给它添加一些提升用户体验的功能。当前市面上的主流播放器几乎都有一个非常友善的功能,用户在退拽进度条的时候可以看见进度条所处进度的视频画面,这对于用户来说是一种直观而且便捷的体验。而对于 iOS 平台的应用程序而言,实现这样的功能也并不复杂。在本文中,我们将探讨如何在 iOS 应用程序中实现图片进度条,以便用户在拖动进度条时能够即时预览视频的画面。通过添加这样一个小但强大的功能,我们可以进一步提升用户与应用程序之间的互动性,使观看视频的体验更加流畅和愉快。
原理
AV Foundation框架为我们提供了一个名为AVAssetImageGenerator的类,专门用来从一个AVAsset资源中提取图片,它为我们提供了两个方法:
-
获取一张图片
generateCGImageAsynchronously(for: <#T##CMTime#>, completionHandler: <#T##(CGImage?, CMTime, Error?) -> Void#>)
该方法用于异步生成给定时间点的CGImage对象,
-
for
: 这是一个CMTime类型的参数,表示要生成图像的时间点。CMTime是Core Media框架中用于表示时间的数据类型,可以理解为一个精确的时间值。 -
completionHandler
: 这是一个闭包类型的参数,用于在生成图像完成时进行回调。闭包接受三个参数:CGImage?
: 生成的图像对象,如果生成失败则为nil。CMTime
: 表示生成图像的时间点,这个时间点可能与传入的时间点不完全相同,会受到视频帧率等因素的影响。Error?
: 如果生成图像过程中出现错误,则会传递一个Error对象,否则为nil。
这个方法的使用场景通常是在需要从视频中获取某个时间点的图像时,可以异步调用该方法,并在完成后通过闭包获取生成的图像。由于图像生成是一个比较耗时的操作,因此使用异步方法可以避免阻塞主线程。
-
获取一组图片
generateCGImagesAsynchronously(forTimes: <#T##[NSValue]#>, completionHandler: <#T##AVAssetImageGeneratorCompletionHandler##AVAssetImageGeneratorCompletionHandler##(CMTime, CGImage?, CMTime, AVAssetImageGenerator.Result, Error?) -> Void#>)
该方法用于异步生成给定时间点数组的CGImage对象数组,
-
forTimes
: 这是一个[NSValue]
类型的参数,表示要生成图像的时间点数组。每个时间点都由一个CMTime
对象封装在NSValue
中。你可以传递一个包含多个时间点的数组,生成器会按顺序为每个时间点生成相应的图像。 -
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 类,我们能够轻松地从视频中获取指定时间点的图像,并将其应用于进度条的展示中。这个小小的功能不仅使用户能够更直观地预览视频内容,还为应用程序增添了更多的交互性和便利性。
当然,除了本文介绍的方法外,还有许多其他的技术和功能可以进一步改进和丰富应用程序的视频播放体验。希望本文能够为您提供一些启发,并在您的开发工作中发挥一定的作用。感谢您的阅读!
如果您有任何问题、建议或想要分享您的经验,请随时在评论区留言,我们期待与您进一步的交流。