前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >AVKit框架详细解析(四) —— 基于AVKit 和 AVFoundation框架的视频流App的构建

AVKit框架详细解析(四) —— 基于AVKit 和 AVFoundation框架的视频流App的构建

原创
作者头像
conanma
修改2021-09-06 10:07:49
6.8K0
修改2021-09-06 10:07:49
举报
文章被收录于专栏:正则正则

开始

首先看下主要内容:

了解如何使用 AVKitAVFoundation 框架构建视频流应用。内容来自翻译

接着看下写作环境:

Swift 5, iOS 14, Xcode 12

下面就是正文了。

你已经在 iOS 应用程序上工作了一段时间,你认为你很聪明。 你以为你已经做到了,嗯?

是的,你可能可以做一些基本的网络。 甚至可能会引入一些 JSON 并将一个像样的table view与包含文本和图像的单元格放在一起。

可以肯定,这是一份令人印象深刻的成就清单,但是……

你能做这个吗?

没错,是时候让您的应用更上一层楼并学习如何添加视频流了!

您将为所有这些旅行视频博主构建一个新应用程序。 有些人想制作关于他们旅行的艺术电影,有些人想在自己舒适的床上享受这些体验。

你来这里是为了让这两个梦想成真。 在此过程中,您将学习 AVKitAVFoundation 框架的基础知识。

在本教程中,您将学习如何:

  • 添加本地视频。
  • 添加流媒体视频。
  • 启用播放控件。
  • 实现循环。
  • 实现画中画。

下载好材料打开 TravelVlogs.xcodeproj 并转到 VideoFeedView.swift

注意:视频可能无法在模拟器中播放。 在真实设备上运行该应用程序将缓解该问题。

入门项目是一个 vlogger 应用程序,您将使用 AVKitAVFoundation 添加功能和特性。 用户可以选择一个视频,然后控制其播放选项。


Understanding AVKit

一个有用的开发智慧:始终支持您可用的最高抽象级别。 然后,当您的需求发生变化时,您可以降到较低的底层。 根据此建议,您将从最高级别的视频框架开始您的旅程。

AVKit 位于 AVFoundation 之上,提供与视频交互所需的所有 UI。

构建并运行该项目,您将看到一个应用程序,该应用程序已经设置了一个充满潜在视频的表格,供您观看。

您的目标是在用户点击其中一个单元格时显示视频播放器。

1. Adding Local Playback

您可以播放两种类型的视频。 您将看到的第一个是当前位于手机存储中的类型。 稍后,您将学习如何从服务器播放视频流。

首先,导航到 VideoFeedView.swift 并在 SwiftUI 导入的正下方添加以下导入:

代码语言:javascript
复制
import AVKit

看看下面这个,你会看到你已经有了一个列表和一个视频数组。 这就是应用程序如何用数据填充现有列表的方式。 视频本身来自嵌入在应用程序包中的 JSON 文件。 如果您好奇,您可以查看 Video.swift 以了解它们是如何获取的。

为了考虑用户的选择,向 VideoFeedView.swift 添加一个 state 属性:

代码语言:javascript
复制
@State private var selectedVideo: Video?

接下来,找到 List 中的按钮,在 Open Video Player注释下添加以下代码:

代码语言:javascript
复制
selectedVideo = video

然后,将 fullScreenCover(item:onDismiss:content:) 视图修饰符添加到 NavigationView

代码语言:javascript
复制
.fullScreenCover(item: $selectedVideo) {
  // On Dismiss Closure
} content: { item in
  makeFullScreenVideoPlayer(for: item)
}

这会将您之前定义的 selectedVideo 属性绑定到全屏封面。 每当您将其设置为非 nil 值时,就会显示全屏封面的内容。

Swift 正在寻找新的 makeFullScreenVideoPlayer(for:),因此添加以下内容以直接设置所有内容:

代码语言:javascript
复制
@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的位置。 不是加载本地视频,而是通过用以下内容替换该行来加载所有视频:

代码语言:javascript
复制
private let videos = Video.fetchLocalVideos() + Video.fetchRemoteVideos()

还有……就是这样! 转到 Video.swift。 在这里您可以看到 fetchRemoteVideos()只是加载另一个 JSON 文件。 如果您查看之前使用的 videoURL 计算属性,您会发现它首先查找 remoteVideoURL。 如果没有找到,您将获得 localVideoURL

构建并运行,然后滚动到feed的底部以找到 キツネ村(kitsune-mura)Fox Village 视频。

这就是 VideoPlayer 的美妙之处; 您只需要一个 URL,就可以开始了!

实际上,转到 RemoteVideos.json并找到这一行:

代码语言:javascript
复制
"remote_video_url": "https://wolverine.raywenderlich.com/content/ios/tutorials/video_streaming/foxVillage.mp4"

然后,用这个替换它:

代码语言:javascript
复制
"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) AVPlayerItemAVAsset 的动态对应物。此对象表示可播放视频的当前状态。这是您需要提供给 AVPlayer才能使事情顺利进行的内容。

AVFoundation 是一个巨大的框架,远远超出了这几个类。幸运的是,这就是您创建循环视频播放器所需的全部内容。

你会依次回到每一个,所以不要担心记住它们。

1. Writing a Custom Video View With AVPlayerLayer

您需要熟悉的第一个类是 AVPlayerLayer。 这个 CALayer 子类就像任何其他层:它显示其contents属性中的任何内容。

该层恰好用您通过其player属性提供的视频中的帧填充其内容。

问题是你不能直接在 SwiftUI 中使用这个层。 毕竟 SwiftUI 没有 CALayer的概念。 为此,您需要回到 UIKit

转到 LoopingPlayerView.swift,您将在其中找到一个用于显示视频的空视图。 它需要一组视频 URL 才能播放。

您需要做的第一件事是添加正确的import语句,这次是为 AVFoundation

代码语言:javascript
复制
import AVFoundation

好的开始! 现在您可以将 AVPlayerLayer 融入其中。

UIView 只是 CALayer 的包装器。 它提供触摸处理和辅助功能,但不是子类。 相反,它拥有并管理底层图层属性。 一个绝妙的技巧是,您实际上可以指定您希望视图子类拥有的图层类型。

添加以下属性覆盖来告诉 LoopingPlayerView.swift 它应该使用 AVPlayerLayer 而不是普通的 CALayer

代码语言:javascript
复制
override class var layerClass: AnyClass {
  return AVPlayerLayer.self
}

由于您将播放器层包装在视图中,因此您需要公开player属性。

为此,请添加以下计算属性,这样您就无需一直投射您的图层子类:

代码语言:javascript
复制
var playerLayer: AVPlayerLayer {
  return layer as! AVPlayerLayer
}

为了能够在 SwiftUI 中使用此视图,您需要使用 UIViewRepresentable 创建一个包装器。

在同一个文件中,在LoopingPlayerUIView定义之外添加这些代码行:

代码语言:javascript
复制
struct LoopingPlayerView: UIViewRepresentable {
  let videoURLs: [URL]
}

UIViewRepresentable 是一个协议。 你需要实现它的方法来完成 UIKitSwiftUI 之间的桥梁。

LoopingPlayerView 中添加这些:

代码语言:javascript
复制
// 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:

代码语言:javascript
复制
private let videoClips = VideoClip.urls

makeEmbeddedVideoPlayer()中,将 Rectangle()替换为以下代码,但保留视图修饰符:

代码语言:javascript
复制
LoopingPlayerView(videoURLs: videoClips)

构建并运行以查看……没什么新鲜的! 您刚刚将视频剪辑 URL 传递给视图,但您还没有对它们进行任何操作。

2. Writing the Looping Video View

接下来,转到 LoopingPlayerView.swift 并准备添加播放器。 毕竟,您现在知道您需要一个播放器来播放视频。

首先,将以下播放器player属性添加到 LoopingPlayerUIView

代码语言:javascript
复制
private var player: AVQueuePlayer?

挑剔的眼睛会发现这不是一个普通的 AVPlayer 实例。 没错,这是一个特殊的子类,叫做AVQueuePlayer。 正如您可能从名称中猜到的那样,此类允许您提供要播放的项目队列。

init(urls:) 替换为以下内容以初始化播放器:

代码语言:javascript
复制
init(urls: [URL]) {
  allURLs = urls
  
  player = AVQueuePlayer()

  super.init(frame: .zero)

  playerLayer.player = player
}

在这里,您首先创建player对象,然后将其连接到底层的 AVPlayerLayer

现在,是时候将您的视频剪辑列表添加到播放器中,以便它可以开始播放它们。

添加以下方法来执行此操作:

代码语言:javascript
复制
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 之前调用该方法:

代码语言:javascript
复制
addAllVideosToPlayer()

现在您已经设置好播放器,是时候进行一些配置了。

为此,在 init(urls:)addAllVideosToPlayer()之后添加以下两行:

代码语言:javascript
复制
player?.volume = 0.0
player?.play()

默认情况下,这会将您的循环剪辑显示设置为自动播放和音频关闭。

构建并运行以查看您的完整工作剪辑节目!

不幸的是,当最后一个剪辑播放完毕后,视频播放器会变黑。

3. Implementing the Actual Looping

Apple 编写了一个漂亮的新类,称为 AVPlayerLooper。 此类将采用单播放器项目并处理循环播放该项目所需的所有逻辑。 不幸的是,这对您没有帮助!

您想要的是循环播放所有这些视频。 看起来您必须以手动方式做事。 您需要做的就是跟踪您的播放器和当前播放的项目。 当它到达最后一个视频时,您将再次将所有剪辑添加到队列中。

当谈到“跟踪”播放器的信息时,唯一的途径就是使用键值观察(KVO)

是的,这是 Apple 提出的最奇怪的 API 之一。 如果你小心,它是一种实时观察和响应状态变化的强大方法。 如果你完全不熟悉 KVO,这里有一个简单的解释:基本思想是你在特定属性的值发生变化时注册通知。 在这种情况下,您想知道播放器的 currentItem 何时发生变化。 每次收到通知时,您都会知道播放器已进入下一个视频。

要在 Swift 中使用 KVO——比在 Objective-C 中好得多——你需要保留对观察者的引用。 将以下属性添加到 LoopingPlayerUIView 中的现有属性:

代码语言:javascript
复制
private var token: NSKeyValueObservation?

要开始观察该属性,请将以下内容添加到 init(urls:) 的末尾:

代码语言:javascript
复制
token = player?.observe(\.currentItem) { [weak self] player, _ in
  if player.items().count == 1 {
    self?.addAllVideosToPlayer()
  }
}

在这里,每次播放器的 currentItem 属性更改时,您都会注册一个block来运行。 当前视频发生变化时,您要检查播放器是否已移动到最终视频。 如果有,那么是时候将所有视频剪辑添加回队列了。

这里的所有都是它的! 构建并运行以查看您的剪辑无限循环。

4. Playing with Player Controls

接下来,是时候添加一些控件了。 你的任务是:

  • 1) 单击时取消视频静音。
  • 2) 双击时在 1x2x 速度之间切换。

您将从完成这些事情所需的实际方法开始。 首先,您需要在 LoopingPlayerUIView 中公开一些可以直接访问播放器的方法。 其次,您需要创建一种从 LoopingPlayerView 调用这些方法的方法。

将这些方法添加到 LoopingPlayerUIView

代码语言:javascript
复制
func setVolume(_ value: Float) {
  player?.volume = value
}

func setRate(_ value: Float) {
  player?.rate = value
}

顾名思义,您可以使用这些方法来控制视频音量和播放速率。 您还可以将 0.0 传递给 setRate(_:) 以暂停视频。

将这些方法连接到 SwiftUI 的方法是使用 Binding

将这些属性添加到 LoopingPlayerView 正下方的 let videoURLs: [URL]

代码语言:javascript
复制
@Binding var rate: Float
@Binding var volume: Float

确保使用您已经实现的方法将绑定值传递给底层 UIView

代码语言:javascript
复制
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 的初始值设定项中删除这两行:

代码语言:javascript
复制
player?.volume = 0.0
player?.play()

现在,返回 VideoFeedView.swift 并添加这些用于更改和观察嵌入视频的音量和播放速率的状态属性:

代码语言:javascript
复制
@State private var embeddedVideoRate: Float = 0.0
@State private var embeddedVideoVolume: Float = 0.0

然后,将以下状态属性传递给 makeEmbeddedVideoPlayer()中的 LoopingPlayerView

代码语言:javascript
复制
LoopingPlayerView(
  videoURLs: videoClips,
  rate: $embeddedVideoRate,
  volume: $embeddedVideoVolume)

最后,将以下视图修饰符添加到 makeEmbeddedVideoPlayer() 中的 LoopingPlayerView

代码语言:javascript
复制
// 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) 当有人双击播放器视图时,您可以添加一个侦听器。 这会在 2x1x的播放速率之间切换。
  • 3) 当有人单击播放器视图时,您可以添加一个侦听器。 这会切换视频的静音状态。

注意:确保首先添加双击侦听器,然后单击。 如果你反过来做,双击监听器将永远不会被调用。

再次构建并运行,您将能够点击和双击来播放剪辑的速度和音量。 这表明添加自定义控件以与自定义视频视图交互是多么容易。

现在,您只需轻按一下即可提高音量并进入快播状态。

5. Playing Video Efficiently

在继续之前要注意的一件事是播放视频是一项资源密集型任务。 事实上,即使您开始观看全屏视频,您的应用程序也会继续播放这些剪辑。

要解决此问题,请转到 VideoFeedView.swift 并在 makeFullScreenVideoPlayer(for:) 中找到 VideoPlayeronAppear块。 通过将速率设置为 0.0 来停止视频剪辑播放:

代码语言:javascript
复制
embeddedVideoRate = 0.0

要在全屏视频关闭时恢复播放,请在 VideoFeedView 主体中找到 fullScreenCover 视图修饰符,并在 On Dismiss Closure 注释后添加以下内容:

代码语言:javascript
复制
embeddedVideoRate = 1.0

当系统不再需要播放器对象时,您还可以停止播放视频并从播放器对象中删除所有项目。 为此,请返回 LoopingPlayerView.swift 并将此方法添加到 LoopingPlayerUIView

代码语言:javascript
复制
func cleanup() {
  player?.pause()
  player?.removeAllItems()
  player = nil
}

幸运的是,SwiftUI 提供了一种调用此清理方法的方法。 将以下内容添加到 LoopingPlayerView

代码语言:javascript
复制
static func dismantleUIView(_ uiView: LoopingPlayerUIView, coordinator: ()) {
  uiView.cleanup()
}

这使您的包装器成为 SwiftUI 世界中非常好的工具!

构建并运行,然后转到全屏视频。 当您返回到feed时,预览会从停止的地方恢复。

6. Trying Not to Steal the Show

如果您打算制作一个包含视频的应用,那么考虑您的应用将如何影响您的用户非常重要。

是的,这听起来非常明显。 但是,您使用过多少次启动无声视频但关闭音乐的应用程序?

如果您从未体验过这种第一世界的讽刺,请插入耳机......哦,对不起,现在的版本:蓝牙连接您的耳机。 打开一些音乐,然后运行该应用程序。 当您这样做时,您会注意到即使视频循环播放器没有发出任何噪音,您的音乐也已关闭!

作为一个体贴的应用程序开发人员,您应该允许用户关闭他们自己的音乐,而不是大胆地假设您的应用程序应该胜过所有其他应用程序。 幸运的是,通过调整 AVAudioSession 的设置来解决这个问题并不难。

前往 AppMain.swift并将以下import添加到文件顶部:

代码语言:javascript
复制
import AVFoundation

接下来,使用以下行实现默认初始化程序:

代码语言:javascript
复制
init() {
  setMixWithOthersPlaybackCategory()
}

不要忘记实现你刚刚使用的方法:

代码语言:javascript
复制
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 并添加此方法:

代码语言:javascript
复制
private func setVideoPlaybackCategory() {
  try? AVAudioSession.sharedInstance().setCategory(.playback)
}

在初始化程序中,确保调用此方法而不是旧方法:

代码语言:javascript
复制
init() {
  setVideoPlaybackCategory()
}

构建并运行,然后点击列表项之一以打开全屏播放器。 您会在左上角看到画中画按钮……否则不会!

缺点是,在撰写本文时,iOS 14.5是可用的最新版本,VideoPlayerSwiftUI 视图未显示画中画按钮。 如果你想使用画中画,你需要使用 AVPlayerViewController,它属于 UIKit。 好处是你知道如何在 SwiftUIUIKit 之间建立桥梁。

创建一个名为 VideoPlayerView.swift 的文件并将其内容替换为以下内容:

代码语言:javascript
复制
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)替换为:

代码语言:javascript
复制
VideoPlayerView(player: avPlayer)

构建并运行,打开一个全屏视频并观看出现在左上角的画中画按钮。

注意:画中画可能不适用于模拟器。 尝试在设备上运行。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开始
  • Understanding AVKit
    • 1. Adding Local Playback
      • 2. Adding Remote Playback
      • Adding a Looping Video Preview
      • Understanding AVFoundation
        • 1. Writing a Custom Video View With AVPlayerLayer
          • 2. Writing the Looping Video View
            • 3. Implementing the Actual Looping
              • 4. Playing with Player Controls
                • 5. Playing Video Efficiently
                  • 6. Trying Not to Steal the Show
                  • Bonus: Adding Picture-in-Picture
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档