AVFoundation框架解析(二十一)—— 一个简单的视频流预览和播放示例之解析(一)

版本记录

版本号 时间
V1.0 2018.10.01 星期一

前言

AVFoundation框架是ios中很重要的框架,所有与视频音频相关的软硬件控制都在这个框架里面,接下来这几篇就主要对这个框架进行介绍和讲解。感兴趣的可以看我上几篇。
1. AVFoundation框架解析(一)—— 基本概览
2. AVFoundation框架解析(二)—— 实现视频预览录制保存到相册
3. AVFoundation框架解析(三)—— 几个关键问题之关于框架的深度概括
4. AVFoundation框架解析(四)—— 几个关键问题之AVFoundation探索(一)
5. AVFoundation框架解析(五)—— 几个关键问题之AVFoundation探索(二)
6. AVFoundation框架解析(六)—— 视频音频的合成(一)
7. AVFoundation框架解析(七)—— 视频组合和音频混合调试
8. AVFoundation框架解析(八)—— 优化用户的播放体验
9. AVFoundation框架解析(九)—— AVFoundation的变化(一)
10. AVFoundation框架解析(十)—— AVFoundation的变化(二)
11. AVFoundation框架解析(十一)—— AVFoundation的变化(三)
12. AVFoundation框架解析(十二)—— AVFoundation的变化(四)
13. AVFoundation框架解析(十三)—— 构建基本播放应用程序
14. AVFoundation框架解析(十四)—— VAssetWriter和AVAssetReader的Timecode支持(一)
15. AVFoundation框架解析(十五)—— VAssetWriter和AVAssetReader的Timecode支持(二)
16. AVFoundation框架解析(十六)—— 一个简单示例之播放、录制以及混合视频(一)
17. AVFoundation框架解析(十七)—— 一个简单示例之播放、录制以及混合视频之源码及效果展示(二)
18. AVFoundation框架解析(十八)—— AVAudioEngine之基本概览(一)
19. AVFoundation框架解析(十九)—— AVAudioEngine之详细说明和一个简单示例(二)
20. AVFoundation框架解析(二十)—— AVAudioEngine之详细说明和一个简单示例源码(三)

开始

首先看一下写作环境

Swift 4.2, iOS 12, Xcode 10

注意:如果您正在使用模拟器Build,请准备好关闭断点或大量点击跳过按钮。 如果您看到以AudioHAL_ClientCUICatalog开头的错误或警告,请随意忽略它们并继续。

在开始时,首先建立工程并配置可用资源,这里就不多说了,然后,打开TravelVlogs.xcodeproj并前往VideoFeedViewController.swift


Introduction to AVKit - AVKit介绍

一个有用的开发智慧:始终支持您可用的最高抽象级别。 然后,当您使用的不再适合您的需求时,您可以降低到较低等级的API。 根据这一建议,您将在最高级别的视频框架中开始您的旅程。

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

如果您构建并运行,您将看到一个已经设置了一个充满潜在视频的表的应用程序。

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

1. Adding Local Playback - 添加本地播放

实际上您可以播放两种类型的视频。 你要看的第一个是当前已经存在于手机硬盘上的类型。 稍后,您将学习如何播放从服务器流式传输的视频。

要开始使用,请导航到VideoFeedViewController.swift。 在UIKit导入下方添加以下导入。

import AVKit

看下面这个,你会看到你已经有一个tableViewVideos数组定义。 这就是现有tableView填充数据的方式。Videos本身来自视频管理类。 您可以查看AppDelegate.swift以了解它们是如何获取的。

接下来,向下滚动,直到找到tableView(_ tableView:didSelectRowAt :)。 将以下代码添加到现有方法:

//1
let video = videos[indexPath.row]

//2
let videoURL = video.url
let player = AVPlayer(url: videoURL)
  • 1) 首先,您获取视频模型对象。
  • 2) 所有Video对象都有一个url属性,表示视频文件的路径。 在这里,您获取URL并创建一个AVPlayer对象。

AVPlayer是在iOS上播放视频的核心。

播放器对象可以启动和停止视频,更改播放速率甚至可以上下调节音量。 您可以将播放器视为能够一次管理一个媒体资源播放的控制器对象。

在方法结束时,添加以下行以获取视图控制器设置。

let playerViewController = AVPlayerViewController()
playerViewController.player = player

present(playerViewController, animated: true) {
  player.play()
}

AVPlayerViewController是一个方便的视图控制器,需要一个player对象才有用。 一旦有了它,就将它呈现为全屏视频播放器。

演示动画完成后,您可以调用play()来启动视频。

这就是它的全部! 构建并运行以查看其外观。

视图控制器显示一组基本控件。 这包括一个播放器按钮,一个静音按钮和15秒跳过按钮来前进和后退。

2. Adding Remote Playback - 添加远程播放

这很简单。 如何从远程URL添加视频播放? 当然,这肯定要困难得多。

转到AppDelegate.swift。 找到feed.videos设置的行。 而不是加载本地视频,通过用以下内容替换该行来加载所有视频。

feed.videos = Video.allVideos()

就是这样! 转到Video.swift。 在这里你可以看到allVideos()只是加载一个额外的视频。 唯一的区别是它的url属性表示Web上的地址而不是文件路径。

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

这就是AVPlayerViewController的美感,你需要的只是一个URL,你很高兴!

实际上,转到allVideos()并换掉这一行:

let videoURLString = 
  "https://wolverine.raywenderlich.com/content/ios/tutorials/video_streaming/foxVillage.mp4"

用下面这个

let videoURLString = 
  "https://wolverine.raywenderlich.com/content/ios/tutorials/video_streaming/foxVillage.m3u8"

Build并运行,你会看到fox village的视频仍然有效。

唯一的区别是第二个URL代表HLS LivestreamHLS实时流式传输通过将视频分成10秒块来实现。 然后,这些服务一次被提供给客户端。 正如您在示例GIF中看到的那样,视频开始播放的速度比使用MP4版本时快得多。


Adding a Looping Video Preview - 添加循环视频预览

您可能已经注意到右下角的黑盒子。 你将把那个黑盒子变成一个浮动的自定义视频播放器。 它的目的是播放一组旋转剪辑,让用户对所有这些视频感到兴奋。

然后你需要添加一些自定义手势,如点击打开声音和双击以将其更改为2倍速度。 当您想要对事物的工作方式进行非常具体的控制时,最好编写自己的视频视图。

返回VideoFeedViewController.swift并查看属性定义。 您将看到此类的shell已存在,并且正在使用一组视频剪辑创建。


Introduction to AVFoundation - AVFoundation简介

虽然AVFoundation可能会让人觉得有点吓人,但是你所处理的大多数对象仍然是相当高级的,所有事情都要考虑。

您需要熟悉的主要课程是:

  • 1) AVPlayerLayer:这个特殊的CALayer子类可以显示给定AVPlayer对象的回放。
  • 2)AVAsset:这些是媒体资产的静态表示。 资产对象包含持续时间和创建日期等信息。
  • 3)AVPlayerItemAVAsset的动态对应对象。 此对象表示可播放视频的当前状态。 这是您需要为AVPlayer提供的东西。

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

你将依次回到这些中,所以不要担心记忆它们或任何东西。

1. Writing a Custom Video View with AVPlayerLayer - 使用AVPlayerLayer写一个自定义视频视图

您需要考虑的第一个类是AVPlayerLayer。 此CALayer子类与任何其他图层类似:它在屏幕上显示其contents属性中的任何内容。、

这个图层恰好用你通过player属性给出的视频中的帧填充其内容。

转到VideoPlayerView.swift,您将在其中找到一个用于显示视频的空视图。

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

import AVFoundation

现在你可以将AVPlayerLayer融入其中。

UIView实际上只是一个封装CALayer的包装器。 它提供了触摸处理和可访问性功能,但不是子类。 相反,它拥有并管理底层layer属性。 一个很好的技巧是你可以实际指定你希望视图子类拥有哪种类型的层。

添加以下属性覆盖以通知此类它应使用AVPlayerLayer而不是普通CALayer

override class var layerClass: AnyClass {
  return AVPlayerLayer.self
}

由于您要在视图中包装player layer,因此您需要暴露player属性。

为此,首先添加以下计算属性,这样您就不需要一直强制转换图层子类。

var playerLayer: AVPlayerLayer {
  return layer as! AVPlayerLayer
}

接下来,使用gettersetter添加实际的player定义。

var player: AVPlayer? {
  get {
    return playerLayer.player
  }

  set {
    playerLayer.player = newValue
  }
}

在这里,您只是设置并获取您的playerLayerplayer对象。 UIView真的只是中间媒介。

当你开始与player本身互动时,真正的魔力再次出现。

构建并运行以查看...

你已经到了一半,即使你看不到任何新东西!

2. Writing the Looping Video View - 编写循环视频视图

接下来,转到VideoLooperView.swift并准备好使用VideoPlayerView。 这个类已经有一组VideoClip,正在初始化一个VideoPlayerView属性。

您需要做的就是获取这些片段并想办法如何在连续循环中播放它们。

首先,添加以下player属性。

private let player = AVQueuePlayer()

这里你会看到这不是普通的AVPlayer实例。 没错,这是一个名为AVQueuePlayer的特殊子类。 正如您可以通过名称猜测的那样,此类允许您提供要播放的项目队列。

添加以下方法以开始设置播放器。

private func initializePlayer() {
  videoPlayerView.player = player
}

在这里,您将player传递给videoPlayerView以将其连接到底层AVPlayerLayer

现在是时候将视频片段列表添加到player中,以便它可以开始播放。

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

private func addAllVideosToPlayer() {
  for video in clips {
    //1
    let asset = AVURLAsset(url: video.url)
    let item = AVPlayerItem(asset: asset)

    //2
    player.insert(item, after: player.items().last)
  }
}

在这里,你循环遍历所有clips。 对于每一个,您:

  • 1) 从每个视频clip对象的URL创建AVURLAsset
  • 2) 然后,使用播放器可用于控制播放的资源创建AVPlayerItem
  • 3) 最后,使用insert(_after :)方法将每个项添加到队列中。

现在,返回initializePlayer()并调用该方法。

player.volume = 0.0
player.play()

这会将循环clip显示设置为默认自动播放和音频关闭。

最后,您需要调用您一直在使用的方法。 转到init(clips :)方法并在底部添加此行。

initializePlayer()

构建并运行以查看完整的节目!

不幸的是,当最后一个片段播放完毕后,视频播放器会渐变为黑色。

3. Doing the Actual Looping - 进行实际循环

Apple编写了一个名为AVPlayerLooper的漂亮新类。 这个类将采用单个播放器项目,并负责在循环中播放该项目所需的所有逻辑。 不幸的是,这对你没有帮助!

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

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

是的,这是苹果公司提出的更为令人难以置信的API之一。 即便如此,如果你小心,它是一种实时观察和响应状态变化的有效方式。 如果您对KVO完全不熟悉,下面就是其简要的了解。 基本思想是您在特定属性的值发生变化时注册通知。 在这种情况下,您想知道player‘s currentItem何时发生变化。 每次收到通知时,您都会知道播放器已进入下一个视频。

您需要做的第一件事是更改之前定义的player属性。 转到文件顶部并将旧定义替换为:

@objc private let player = AVQueuePlayer()

唯一的区别是你添加了@objc指令。 这告诉Swift你想要将属性暴露给像KVO这样的Objective-C之类的东西。 要在Swift中使用KVO - 比Objective-C好得多 - 你需要保留对观察者的引用。 在player之后添加以下属性:

private var token: NSKeyValueObservation?

要开始观察属性,请返回initializePlayer()并在结尾处添加以下内容:

token = player.observe(\.currentItem) { [weak self] player, _ in
  if player.items().count == 1 {
    self?.addAllVideosToPlayer()
  }
}

在这里,您每次注册playercurrentItem属性时都会注册一个块。 当前视频更改时,您需要检查播放器是否已移至最终视频。 如果有,那么是时候将所有视频剪辑添加回队列。

下面,构建并运行以查看无限期循环的clips

4. Playing Video Efficiently - 有效播放视频

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

要修复它,首先将以下两个方法添加到VideoLooperView.swift的底部。

func pause() {
  player.pause()
}

func play() {
  player.play()
}

如您所见,您正在公开play()pause()方法,并将消息传递给此视图的播放器。

现在,转到VideoFeedViewController.swift并找到viewWillDisappear(_ :)。 在那里,添加以下调用以暂停视频循环器。

videoPreviewLooper.pause()

然后,转到viewWillAppear(_ :)并添加匹配的调用以在用户返回时恢复播放。

videoPreviewLooper.play()

构建并运行,并转到全屏视频。 当您返回Feed时,预览将从中断处继续。

5. Playing with Player Controls - 播放器控制

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

  • 1) 单击发生时取消静音视频。
  • 2) 当双击发生时,在1x和2x速度之间切换。

您将从实现这些事情所需的实际方法开始。 首先,返回VideoLooperView.swift并找到添加播放和暂停方法的位置。

添加以下单击分页处理程序,将在0.01.0之间切换卷。

@objc func wasTapped() {
  player.volume = player.volume == 1.0 ? 0.0 : 1.0
}

接下来,添加双击处理程序。

@objc func wasDoubleTapped() {
  player.rate = player.rate == 1.0 ? 2.0 : 1.0
}

这一点类似于它在1.0和2.0之间切换播放速率。

接下来,添加以下方法定义,以创建两个手势识别器。

func addGestureRecognizers() {
  // 1
  let tap = UITapGestureRecognizer(target: self, action: #selector(VideoLooperView.wasTapped))
  let doubleTap = UITapGestureRecognizer(target: self,
                                         action: #selector(VideoLooperView.wasDoubleTapped))
  doubleTap.numberOfTapsRequired = 2
  
  // 2
  tap.require(toFail: doubleTap)

  // 3
  addGestureRecognizer(tap)
  addGestureRecognizer(doubleTap)
}

下面一步步的看:

  • 1) 首先,您创建两个手势识别器并告诉他们调用哪些方法。 你还告诉双击它需要两次点击。
  • 2) 接下来,您进行单击等待以确保不会发生双击。 如果您不这样做,将始终立即调用单击方法。
  • 3) 然后,将手势识别器添加到视频视图中。

要完成任务,请转到init(clip :)并在底部添加以下方法调用。

addGestureRecognizers()

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

现在,您可以将音量调高了!

6. Trying Not to Steal the Show - 不要让别的因素影响你的展示

最后需要注意的是,如果您打算创建一个包含视频的应用,那么考虑一下您的应用对用户的影响非常重要。

是的,我知道,这听起来非常明显。 但是你有多少次使用的应用程序可以启动静音视频但关闭你的音乐?

打开一些音乐然后运行应用程序。 当你这样做时,你会注意到你的音乐已关闭,即使视频循环没有发出任何噪音!

我的观点是,您应该允许您的用户关闭他们自己的音乐,而不是做出如此大胆的假设。 幸运的是,通过调整AVAudioSession的设置来解决这个问题并不是很难。

转到AppDelegate.swift并将以下导入添加到文件的顶部。

import AVFoundation

接下来,在application(_:didFinishLaunchingWithOptions:)的顶部,添加以下行。

try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient,
                                                 mode: AVAudioSessionModeMoviePlayback,
                                                 options: [.mixWithOthers])

在这里,您告诉共享AVAudioSession您希望音频属于AVAudioSessionCategoryAmbient类别。 默认值为AVAudioSessionCategorySoloAmbient,它就这样说明了关闭其他应用程序的音频。

您还指定您的应用程序正在使用音频进行“电影播放”,并且您可以将声音与来自其他来源的声音混合使用。

对于最终构建和运行,请重新启动音乐并再次启动应用程序。

祝大家国庆节快乐!!!

后记

本篇主要讲述了一个简单的视频流预览和播放示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容