版本记录
版本号 | 时间 |
---|---|
V1.0 | 2021.08.15 星期日 |
前言
AVKit框架为媒体播放创建视图级别的服务,包含用户控件,章节导航以及对字幕和隐藏式字幕的支持。接下来几篇我们就一起看一下这个框架。感兴趣的可以看下面几篇文章。
1. AVKit框架详细解析(一) —— 基本概览(一)
2. AVKit框架详细解析(二) —— 基于视频播放器的画中画实现(一)
3. AVKit框架详细解析(三) —— 基于视频播放器的画中画实现(二)
开始
首先看下主要内容:
了解如何使用
AVKit
和AVFoundation
框架构建视频流应用。内容来自翻译。
接着看下写作环境:
Swift 5, iOS 14, Xcode 12
下面就是正文了。
你已经在 iOS 应用程序上工作了一段时间,你认为你很聪明。 你以为你已经做到了,嗯?
是的,你可能可以做一些基本的网络。 甚至可能会引入一些 JSON
并将一个像样的table view
与包含文本和图像的单元格放在一起。
可以肯定,这是一份令人印象深刻的成就清单,但是……
你能做这个吗?
没错,是时候让您的应用更上一层楼并学习如何添加视频流了!
您将为所有这些旅行视频博主构建一个新应用程序。 有些人想制作关于他们旅行的艺术电影,有些人想在自己舒适的床上享受这些体验。
你来这里是为了让这两个梦想成真。 在此过程中,您将学习 AVKit
和 AVFoundation
框架的基础知识。
在本教程中,您将学习如何:
- 添加本地视频。
- 添加流媒体视频。
- 启用播放控件。
- 实现循环。
- 实现画中画。
下载好材料打开 TravelVlogs.xcodeproj
并转到 VideoFeedView.swift
。
注意:视频可能无法在模拟器中播放。 在真实设备上运行该应用程序将缓解该问题。
入门项目是一个 vlogger
应用程序,您将使用 AVKit
和 AVFoundation
添加功能和特性。 用户可以选择一个视频,然后控制其播放选项。
Understanding AVKit
一个有用的开发智慧:始终支持您可用的最高抽象级别。 然后,当您的需求发生变化时,您可以降到较低的底层。 根据此建议,您将从最高级别的视频框架开始您的旅程。
AVKit
位于 AVFoundation
之上,提供与视频交互所需的所有 UI。
构建并运行该项目,您将看到一个应用程序,该应用程序已经设置了一个充满潜在视频的表格,供您观看。
您的目标是在用户点击其中一个单元格时显示视频播放器。
1. Adding Local Playback
您可以播放两种类型的视频。 您将看到的第一个是当前位于手机存储中的类型。 稍后,您将学习如何从服务器播放视频流。
首先,导航到 VideoFeedView.swift
并在 SwiftUI
导入的正下方添加以下导入:
import AVKit
看看下面这个,你会看到你已经有了一个列表和一个视频数组。 这就是应用程序如何用数据填充现有列表的方式。 视频本身来自嵌入在应用程序包中的 JSON
文件。 如果您好奇,您可以查看 Video.swift
以了解它们是如何获取的。
为了考虑用户的选择,向 VideoFeedView.swift
添加一个 state
属性:
@State private var selectedVideo: Video?
接下来,找到 List
中的按钮,在 Open Video Player
注释下添加以下代码:
selectedVideo = video
然后,将 fullScreenCover(item:onDismiss:content:)
视图修饰符添加到 NavigationView
:
.fullScreenCover(item: $selectedVideo) {
// On Dismiss Closure
} content: { item in
makeFullScreenVideoPlayer(for: item)
}
这会将您之前定义的 selectedVideo
属性绑定到全屏封面。 每当您将其设置为非 nil
值时,就会显示全屏封面的内容。
Swift
正在寻找新的 makeFullScreenVideoPlayer(for:)
,因此添加以下内容以直接设置所有内容:
@ViewBuilder
private func makeFullScreenVideoPlayer(for video: Video) -> some View {
// 1
if let url = video.videoURL {
// 2
let avPlayer = AVPlayer(url: url)
// 3
VideoPlayer(player: avPlayer)
// 4
.edgesIgnoringSafeArea(.all)
.onAppear {
// 5
avPlayer.play()
}
} else {
ErrorView()
}
}
- 1) 所有
Video
对象都有一个videoURL
属性,表示视频文件的路径。 - 2) 在这里,您获取
url
并创建一个AVPlayer
对象。
AVPlayer
是在 iOS 上播放视频的核心。
播放器对象可以启动和停止您的视频,更改其播放速率,甚至可以调高和调低音量。 将播放器视为能够一次管理一个媒体资产的播放的控制器对象。
- 3)
VideoPlayer
是一个方便的SwiftUI
视图,需要播放器对象才能发挥作用。 您可以使用它来播放视频。 - 4) 默认情况下,
SwiftUI
视图考虑设备的安全区域。 由于呈现超出状态栏和主页指示器的视频播放器看起来更好,因此您添加了此修饰符。 - 5) 一旦视频播放器出现在屏幕上,您就可以调用
play()
来启动视频。
这就是全部! 构建并运行以查看它的外观。
您可以看到视频播放器显示了一组基本控件。 这包括一个播放按钮、一个静音按钮和用于前进和后退的 15 秒跳过按钮。
2. Adding Remote Playback
那很容易,对吧? 如何从远程 URL 添加视频播放? 那一定要难很多!
转到 VideoFeedView.swift
并找到设置videos
的位置。 不是加载本地视频,而是通过用以下内容替换该行来加载所有视频:
private let videos = Video.fetchLocalVideos() + Video.fetchRemoteVideos()
还有……就是这样! 转到 Video.swift
。 在这里您可以看到 fetchRemoteVideos()
只是加载另一个 JSON
文件。 如果您查看之前使用的 videoURL
计算属性,您会发现它首先查找 remoteVideoURL
。 如果没有找到,您将获得 localVideoURL
。
构建并运行,然后滚动到feed
的底部以找到 キツネ村(kitsune-mura)
或 Fox Village
视频。
这就是 VideoPlayer
的美妙之处; 您只需要一个 URL
,就可以开始了!
实际上,转到 RemoteVideos.json
并找到这一行:
"remote_video_url": "https://wolverine.raywenderlich.com/content/ios/tutorials/video_streaming/foxVillage.mp4"
然后,用这个替换它:
"remote_video_url": "https://wolverine.raywenderlich.com/content/ios/tutorials/video_streaming/foxVillage.m3u8"
构建并运行,你会看到 Fox Village
视频仍然有效。
唯一的区别是第二个 URL
表示 HTTP live stream (HLS)
。 HLS
的工作原理是将视频分成 10 秒的块。 这些然后一次一个块地提供给客户端。 如果您的互联网连接速度较慢,您会发现视频开始播放的速度比使用 MP4
版本时快得多。
Adding a Looping Video Preview
您可能已经注意到列表顶部的黑框。 您的下一个任务是将黑框变成自定义视频播放器。 它的目的是播放一组循环剪辑,让用户对所有这些视频感到兴奋。
然后,您需要添加一些自定义手势,例如点击打开声音和双击将其更改为 2 倍速度。当您想对事物的工作方式进行非常具体的控制时,最好编写自己的视频视图。
让事情顺利进行是你的工作。
Understanding AVFoundation
虽然 AVFoundation
感觉有点吓人,但您处理的大多数对象仍然是相当高级的。
您需要熟悉的主要类是:
- 1)
AVPlayerLayer
:这个特殊的CALayer
子类可以显示给定AVPlayer
对象的播放。 - 2)
AVAsset
:这些是媒体资产的静态表示。资产对象包含持续时间和创建日期等信息。 - 3)
AVPlayerItem
:AVAsset
的动态对应物。此对象表示可播放视频的当前状态。这是您需要提供给AVPlayer
才能使事情顺利进行的内容。
AVFoundation
是一个巨大的框架,远远超出了这几个类。幸运的是,这就是您创建循环视频播放器所需的全部内容。
你会依次回到每一个,所以不要担心记住它们。
1. Writing a Custom Video View With AVPlayerLayer
您需要熟悉的第一个类是 AVPlayerLayer
。 这个 CALayer
子类就像任何其他层:它显示其contents
属性中的任何内容。
该层恰好用您通过其player
属性提供的视频中的帧填充其内容。
问题是你不能直接在 SwiftUI
中使用这个层。 毕竟 SwiftUI
没有 CALayer
的概念。 为此,您需要回到 UIKit
。
转到 LoopingPlayerView.swift
,您将在其中找到一个用于显示视频的空视图。 它需要一组视频 URL
才能播放。
您需要做的第一件事是添加正确的import
语句,这次是为 AVFoundation
:
import AVFoundation
好的开始! 现在您可以将 AVPlayerLayer
融入其中。
UIView
只是 CALayer
的包装器。 它提供触摸处理和辅助功能,但不是子类。 相反,它拥有并管理底层图层属性。 一个绝妙的技巧是,您实际上可以指定您希望视图子类拥有的图层类型。
添加以下属性覆盖来告诉 LoopingPlayerView.swift
它应该使用 AVPlayerLayer
而不是普通的 CALayer
:
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
由于您将播放器层包装在视图中,因此您需要公开player
属性。
为此,请添加以下计算属性,这样您就无需一直投射您的图层子类:
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
为了能够在 SwiftUI
中使用此视图,您需要使用 UIViewRepresentable
创建一个包装器。
在同一个文件中,在LoopingPlayerUIView
定义之外添加这些代码行:
struct LoopingPlayerView: UIViewRepresentable {
let videoURLs: [URL]
}
UIViewRepresentable
是一个协议。 你需要实现它的方法来完成 UIKit
和 SwiftUI
之间的桥梁。
在 LoopingPlayerView
中添加这些:
// 1
func makeUIView(context: Context) -> LoopingPlayerUIView {
// 2
let view = LoopingPlayerUIView(urls: videoURLs)
return view
}
// 3
func updateUIView(_ uiView: LoopingPlayerUIView, context: Context) { }
- 1) 当
SwiftUI
需要一个新的UIView
实例时,它会调用makeUIView(context:)
。 - 2) 您使用初始值设定项创建
LoopingPlayerUIView
的新实例并返回新实例。 - 3)
SwiftUI
在需要更新底层UIView
时会调用此方法。 现在,将其留空。
现在,返回 VideoFeedView.swift
并添加以下属性以获取视频剪辑的 URL:
private let videoClips = VideoClip.urls
在 makeEmbeddedVideoPlayer()
中,将 Rectangle()
替换为以下代码,但保留视图修饰符:
LoopingPlayerView(videoURLs: videoClips)
构建并运行以查看……没什么新鲜的! 您刚刚将视频剪辑 URL 传递给视图,但您还没有对它们进行任何操作。
2. Writing the Looping Video View
接下来,转到 LoopingPlayerView.swift
并准备添加播放器。 毕竟,您现在知道您需要一个播放器来播放视频。
首先,将以下播放器player
属性添加到 LoopingPlayerUIView
:
private var player: AVQueuePlayer?
挑剔的眼睛会发现这不是一个普通的 AVPlayer
实例。 没错,这是一个特殊的子类,叫做AVQueuePlayer
。 正如您可能从名称中猜到的那样,此类允许您提供要播放的项目队列。
将 init(urls:)
替换为以下内容以初始化播放器:
init(urls: [URL]) {
allURLs = urls
player = AVQueuePlayer()
super.init(frame: .zero)
playerLayer.player = player
}
在这里,您首先创建player
对象,然后将其连接到底层的 AVPlayerLayer
。
现在,是时候将您的视频剪辑列表添加到播放器中,以便它可以开始播放它们。
添加以下方法来执行此操作:
private func addAllVideosToPlayer() {
for url in allURLs {
// 1
let asset = AVURLAsset(url: url)
// 2
let item = AVPlayerItem(asset: asset)
// 3
player?.insert(item, after: player?.items().last)
}
}
在这里,您正在循环播放所有剪辑。 对于每一项,您:
- 1) 从每个视频剪辑对象的
URL
创建一个AVURLAsset
。 - 2) 然后,您使用播放器可用于控制播放的
asset
创建一个AVPlayerItem
。 - 3) 最后,您使用
insert(_:after:)
将每个项目添加到队列中。
现在,回到 init(urls:)
并在 super.init(frame:)
之后和将player
设置为 playerLayer
之前调用该方法:
addAllVideosToPlayer()
现在您已经设置好播放器,是时候进行一些配置了。
为此,在 init(urls:)
中 addAllVideosToPlayer()
之后添加以下两行:
player?.volume = 0.0
player?.play()
默认情况下,这会将您的循环剪辑显示设置为自动播放和音频关闭。
构建并运行以查看您的完整工作剪辑节目!
不幸的是,当最后一个剪辑播放完毕后,视频播放器会变黑。
3. Implementing the Actual Looping
Apple
编写了一个漂亮的新类,称为 AVPlayerLooper
。 此类将采用单播放器项目并处理循环播放该项目所需的所有逻辑。 不幸的是,这对您没有帮助!
您想要的是循环播放所有这些视频。 看起来您必须以手动方式做事。 您需要做的就是跟踪您的播放器和当前播放的项目。 当它到达最后一个视频时,您将再次将所有剪辑添加到队列中。
当谈到“跟踪”播放器的信息时,唯一的途径就是使用键值观察(KVO)
。
是的,这是 Apple
提出的最奇怪的 API
之一。 如果你小心,它是一种实时观察和响应状态变化的强大方法。 如果你完全不熟悉 KVO,这里有一个简单的解释:基本思想是你在特定属性的值发生变化时注册通知。 在这种情况下,您想知道播放器的 currentItem
何时发生变化。 每次收到通知时,您都会知道播放器已进入下一个视频。
要在 Swift
中使用 KVO
——比在 Objective-C
中好得多——你需要保留对观察者的引用。 将以下属性添加到 LoopingPlayerUIView
中的现有属性:
private var token: NSKeyValueObservation?
要开始观察该属性,请将以下内容添加到 init(urls:)
的末尾:
token = player?.observe(\.currentItem) { [weak self] player, _ in
if player.items().count == 1 {
self?.addAllVideosToPlayer()
}
}
在这里,每次播放器的 currentItem
属性更改时,您都会注册一个block
来运行。 当前视频发生变化时,您要检查播放器是否已移动到最终视频。 如果有,那么是时候将所有视频剪辑添加回队列了。
这里的所有都是它的! 构建并运行以查看您的剪辑无限循环。
4. Playing with Player Controls
接下来,是时候添加一些控件了。 你的任务是:
- 1) 单击时取消视频静音。
- 2) 双击时在
1x
和2x
速度之间切换。
您将从完成这些事情所需的实际方法开始。 首先,您需要在 LoopingPlayerUIView
中公开一些可以直接访问播放器的方法。 其次,您需要创建一种从 LoopingPlayerView
调用这些方法的方法。
将这些方法添加到 LoopingPlayerUIView
:
func setVolume(_ value: Float) {
player?.volume = value
}
func setRate(_ value: Float) {
player?.rate = value
}
顾名思义,您可以使用这些方法来控制视频音量和播放速率。 您还可以将 0.0
传递给 setRate(_:)
以暂停视频。
将这些方法连接到 SwiftUI
的方法是使用 Binding
。
将这些属性添加到 LoopingPlayerView
正下方的 let videoURLs: [URL]
:
@Binding var rate: Float
@Binding var volume: Float
确保使用您已经实现的方法将绑定值传递给底层 UIView
:
func makeUIView(context: Context) -> LoopingPlayerUIView {
let view = LoopingPlayerUIView(urls: videoURLs)
view.setVolume(volume)
view.setRate(rate)
return view
}
func updateUIView(_ uiView: LoopingPlayerUIView, context: Context) {
uiView.setVolume(volume)
uiView.setRate(rate)
}
这一次,您还向 updateUIView(_:context:)
添加了一些行,以说明当视图在屏幕上时音量和速率的变化。
由于您将从该结构体外部控制播放,因此您可以从 LoopingPlayerUIView
的初始值设定项中删除这两行:
player?.volume = 0.0
player?.play()
现在,返回 VideoFeedView.swift
并添加这些用于更改和观察嵌入视频的音量和播放速率的状态属性:
@State private var embeddedVideoRate: Float = 0.0
@State private var embeddedVideoVolume: Float = 0.0
然后,将以下状态属性传递给 makeEmbeddedVideoPlayer()
中的 LoopingPlayerView
:
LoopingPlayerView(
videoURLs: videoClips,
rate: $embeddedVideoRate,
volume: $embeddedVideoVolume)
最后,将以下视图修饰符添加到 makeEmbeddedVideoPlayer()
中的 LoopingPlayerView
:
// 1
.onAppear {
embeddedVideoRate = 1
}
// 2
.onTapGesture(count: 2) {
embeddedVideoRate = embeddedVideoRate == 1.0 ? 2.0 : 1.0
}
// 3
.onTapGesture {
embeddedVideoVolume = embeddedVideoVolume == 1.0 ? 0.0 : 1.0
}
逐条看下:
- 1) 通过将速率设置为
1.0
,您可以像以前一样播放视频。 - 2) 当有人双击播放器视图时,您可以添加一个侦听器。 这会在
2x
和1x
的播放速率之间切换。 - 3) 当有人单击播放器视图时,您可以添加一个侦听器。 这会切换视频的静音状态。
注意:确保首先添加双击侦听器,然后单击。 如果你反过来做,双击监听器将永远不会被调用。
再次构建并运行,您将能够点击和双击来播放剪辑的速度和音量。 这表明添加自定义控件以与自定义视频视图交互是多么容易。
现在,您只需轻按一下即可提高音量并进入快播状态。
5. Playing Video Efficiently
在继续之前要注意的一件事是播放视频是一项资源密集型任务。 事实上,即使您开始观看全屏视频,您的应用程序也会继续播放这些剪辑。
要解决此问题,请转到 VideoFeedView.swift
并在 makeFullScreenVideoPlayer(for:)
中找到 VideoPlayer
的 onAppear
块。 通过将速率设置为 0.0
来停止视频剪辑播放:
embeddedVideoRate = 0.0
要在全屏视频关闭时恢复播放,请在 VideoFeedView
主体中找到 fullScreenCover
视图修饰符,并在 On Dismiss Closure
注释后添加以下内容:
embeddedVideoRate = 1.0
当系统不再需要播放器对象时,您还可以停止播放视频并从播放器对象中删除所有项目。 为此,请返回 LoopingPlayerView.swift
并将此方法添加到 LoopingPlayerUIView
:
func cleanup() {
player?.pause()
player?.removeAllItems()
player = nil
}
幸运的是,SwiftUI
提供了一种调用此清理方法的方法。 将以下内容添加到 LoopingPlayerView
:
static func dismantleUIView(_ uiView: LoopingPlayerUIView, coordinator: ()) {
uiView.cleanup()
}
这使您的包装器成为 SwiftUI
世界中非常好的工具!
构建并运行,然后转到全屏视频。 当您返回到feed
时,预览会从停止的地方恢复。
6. Trying Not to Steal the Show
如果您打算制作一个包含视频的应用,那么考虑您的应用将如何影响您的用户非常重要。
是的,这听起来非常明显。 但是,您使用过多少次启动无声视频但关闭音乐的应用程序?
如果您从未体验过这种第一世界的讽刺,请插入耳机......哦,对不起,现在的版本:蓝牙连接您的耳机。 打开一些音乐,然后运行该应用程序。 当您这样做时,您会注意到即使视频循环播放器没有发出任何噪音,您的音乐也已关闭!
作为一个体贴的应用程序开发人员,您应该允许用户关闭他们自己的音乐,而不是大胆地假设您的应用程序应该胜过所有其他应用程序。 幸运的是,通过调整 AVAudioSession
的设置来解决这个问题并不难。
前往 AppMain.swift
并将以下import
添加到文件顶部:
import AVFoundation
接下来,使用以下行实现默认初始化程序:
init() {
setMixWithOthersPlaybackCategory()
}
不要忘记实现你刚刚使用的方法:
private func setMixWithOthersPlaybackCategory() {
try? AVAudioSession.sharedInstance().setCategory(
AVAudioSession.Category.ambient,
mode: AVAudioSession.Mode.moviePlayback,
options: [.mixWithOthers])
}
在这里,您告诉共享的 AVAudioSession
您希望您的音频属于环境类别。 默认是 AVAudioSession.Category.soloAmbient
,它解释了关闭来自其他应用程序的音频。
您还指定您的应用程序使用音频进行“电影播放”,并且您可以将声音与来自其他来源的声音混合。
构建并运行,开始备份音乐并再次启动应用程序。
您现在拥有一个视频应用程序,让您可以自由地成为自己船的船长。
Bonus: Adding Picture-in-Picture
如果您可以在设备上做其他事情的同时继续观看视频会怎样?
您将向应用程序添加画中画(PiP)
功能。
首先,您需要为应用声明这种兼容性。 在应用程序目标的Signing & Capabilities
部分,添加Audio, AirPlay, and Picture in Picture
背景模式。
接下来,您需要更改音频会话类别。 PiP
视频无法在环境模式下播放。 打开 AppMain.swift
并添加此方法:
private func setVideoPlaybackCategory() {
try? AVAudioSession.sharedInstance().setCategory(.playback)
}
在初始化程序中,确保调用此方法而不是旧方法:
init() {
setVideoPlaybackCategory()
}
构建并运行,然后点击列表项之一以打开全屏播放器。 您会在左上角看到画中画按钮……否则不会!
缺点是,在撰写本文时,iOS 14.5
是可用的最新版本,VideoPlayer
的 SwiftUI
视图未显示画中画按钮。 如果你想使用画中画,你需要使用 AVPlayerViewController
,它属于 UIKit
。 好处是你知道如何在 SwiftUI
和 UIKit
之间建立桥梁。
创建一个名为 VideoPlayerView.swift
的文件并将其内容替换为以下内容:
import SwiftUI
// 1
import AVKit
// 2
struct VideoPlayerView: UIViewControllerRepresentable {
// 3
let player: AVPlayer?
func makeUIViewController(context: Context) -> AVPlayerViewController {
// 4
let controller = AVPlayerViewController()
controller.player = player
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {}
}
- 1) 您导入
AVKit
,因为AVPlayerViewController
位于此模块内。 - 2) 您定义了一个符合
UIViewControllerRepresentable
的结构,以便能够在SwiftUI
中使用AVPlayerViewController
。 - 3) 与您目前看到的所有播放视频的方式一样,
AVPlayerViewController
也需要一个播放器。 - 4) 你创建一个
AVPlayerViewController
的实例,设置它的播放器并返回实例。
这就是桥梁。 返回 VideoFeedView.swift
并将 makeFullScreenVideoPlayer(for:)
中的 VideoPlayer(player: avPlayer)
替换为:
VideoPlayerView(player: avPlayer)
构建并运行,打开一个全屏视频并观看出现在左上角的画中画按钮。
注意:画中画可能不适用于模拟器。 尝试在设备上运行。
您还可以向 LoopingPlayerView
添加画中画支持。为了本文的简洁起见,执行此操作的代码包含在最终项目中。
您已经成功地构建了一个可以播放本地和远程视频的应用程序。它还有效地展示了平台上所有最酷视频的精彩片段。
如果您想了解有关视频播放的更多信息,这只是冰山一角。 AVFoundation
是一个庞大的框架,可以处理诸如:
- 使用内置摄像头捕捉视频。
- 视频格式之间的转码。
- 实时将过滤器应用于视频流。
与往常一样,在尝试了解有关特定主题的更多信息时查看 WWDC 视频是不费吹灰之力的。
本教程中没有特别提到的一件事是对 AVPlayerItem
的 status
属性做出反应。观察远程视频的状态会告诉您网络状况和流媒体视频的播放质量。
此外,还有很多关于使用 HLS
进行直播的知识。如果您对它感兴趣,请查看 Apple’s documentation。此页面包含可用于了解更多信息的其他资源的链接列表。
您还可以查看 raywenderlich.com Forums并在那里提出您的问题。
后记
本篇主要讲述了基于
AVKit
和AVFoundation
框架的视频流App的构建,感兴趣的给个赞或者关注~~~
网友评论