二.音视频编辑-媒体组合-播放

引言

当涉及到音视频编辑时,媒体资源的提取和组合是至关重要的环节。在iOS平台上,AVFoundation框架提供了丰富而强大的功能,使得媒体资源的操作变得轻松而高效。从原始的媒体中提取片段,然后将它们巧妙地组合成一个完整的作品,这是音视频编辑过程中的常见任务之一。在这篇博客中,我们将深入探讨iOS AVFoundation框架中的媒体组合功能,探索其如何为开发者提供丰富的工具和技术,帮助他们实现创意无限的音视频编辑项目。

概述

媒体组合类关系

上图是关于媒体功能中的核心类,以及类直接的关系图。有关资源组合的功能就源于AVAsset的子类AVComposition。一个组合就是将多种媒体资源组合成一个自定义的临时排列,再将这个临时排列视为一个可呈现的独立媒体项目。就比如AVAsset对象,组合相当于包含了一个或多个给定类型的媒体轨道的容器。AVComposition中的轨道都是AVAssetTrack的子类AVCompositionTrack。一个组合轨道本身由一个或多个媒体片段组成,由AVCompositionTrackSegment类定义,代表这个组合中的实际媒体区域。

组合后的对象关系如下:

组合排列

AVComposition和AVCompositionTrack都是不可变对象,提供对资源的只读操作。这些对象提供了一个合适的接口让应用程序的一部分可以进行播放或处理。不过,当创建自己的组合时,就需要使用AVMutableComposition和AVMutableCompositionTrack所提供的可变子类。这些对象提供的类接口需要操作轨道和轨道分段,这样我们就可以创建所需的临时排列了。

基础方法

这个基础的实例会将两个视频片段中的前5秒内容提取出来,并按照组合视频轨道的顺序进行排序。还会从MP3文件中奖音频轨道整合到视频中,期间会用到Core Media框架中定义的CMTime数据类型作为时间格式,相关内容可以查看其它博客。

        let composition = AVMutableComposition()var videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!var audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)

上面的示例创建了一个AVMutableComposition并用它的addMutableTrackWithMediaType:preferredTrackID:方法添加了两个轨道对象。当创建组合轨道时,开发者必须指明它所能支持的媒体类型,并给出一个轨道标识符。设置preferredTrackID:参数为CMPersistentTrackID,这是一个32位的整数值。虽然我们可以传递任意标识符作为参数,这个标识符在我们之后需要返回轨道时会用到,不过一般来说都是赋给它一个kCMPersistentTrackID_Invalid常量。这个有着奇怪名字的常量的意思是我们需要创建一个合适轨道ID的任务委托给框架,标识符会以1..n排列。

现在我们已经实现了一个组合资源:

组合状态

下一步就是将独立的媒体片段插入到组合的轨道中。

        //1.创建资源let goldenGateAsset = AVURLAsset(url: URL(string: "1")!, options: nil)let teaGardenAsset = AVURLAsset(url: URL(string: "2")!, options: nil)let soundTrackAsset = AVURLAsset(url: URL(string: "3")!, options: nil)//2.定义插入点var cursorTime = CMTime.zero//3.定义片段时长let videoDuration = CMTime(value: 5, timescale: 1)let videoTimeRange = CMTimeRange(start: cursorTime, duration: videoDuration)//4.提取资源中的视频轨道并插入到组合中的视频轨道let goldenGateAssetTrack = goldenGateAsset.tracks(withMediaType: .video).first!do {try videoTrack.insertTimeRange(videoTimeRange, of: goldenGateAssetTrack, at: cursorTime)} catch {print("Error inserting time range: \(error)")}//5.调整插入时间cursorTime = CMTimeAdd(cursorTime, videoDuration)    //6.提取资源中的视频轨道并插入到组合中的视频轨道let teaGardenAssetTrack = teaGardenAsset.tracks(withMediaType: .video).first!do {try videoTrack.insertTimeRange(videoTimeRange, of: teaGardenAssetTrack, at: cursorTime)} catch {print("Error inserting time range: \(error)")}//7.调整插入时间和时长cursorTime = CMTime.zerolet audioDuration = composition.durationlet audioTimeRange = CMTimeRangeMake(start: cursorTime, duration: audioDuration)//8.提取音频轨道并插入到组合中的音频轨道let soundTrackAssetTrack = soundTrackAsset.tracks(withMediaType: .audio).first!do {try audioTrack?.insertTimeRange(audioTimeRange, of: soundTrackAssetTrack, at: cursorTime)} catch {print("Error inserting time range: \(error)")}
  1. 首先我们创建了3个AVAsset资源,当然这里面是模拟创建的,其中前2个表示视频,第3个表示音频。
  2. 定义了资源的插入时间点。
  3. 定义每个视频片段资源的插入时长。
  4. 提取第1个视频资源的视频轨道,默认视频资源只有一个视频轨道,插入到组合的视频轨道。
  5. 调整下一个视频资源的插入时间为上一个视频资源的结束时间点。
  6. 同样获取第2个视频资源的视频轨道,插入到组合的视频轨道。
  7. 调整插入时间为0,并设置音频的时长。
  8. 提取音频资源的音频轨道并插入到组合的音频轨道中。

这样我们的组合就构建完成了:

完成的组合

使用示例

下面我们将着色创建一个视频编辑的应用程序,接下来的博客也将围绕这个程序不断的添加和完善视频编辑的功能。

项目介绍

应用程序将包含两个不同的部分,一个是视频播放器,我们只需在之前博客的视频播放器中稍作改动,一个是可以选择媒体和允许媒体排列组合的视频编辑部分,重点会放在视频编辑的部分。

播放器

播放器和视频播放相关博客的播放器大致相同,只是原来传入播放器的是视频地址,而现在传入的需要是一个完整的AVPlayerItem。因此需要重写了init方法,并且添加另一个用于替换当前播放AVPlayerItem的方法。

init方法:

    override init() {super.init()self.player = AVPlayer(playerItem: playerItem)if let player = player {playerView = PHPlayerView(player: player)}addObserverForPlayerItem()}/// 自定义初始化方法////// - Parameters:///   - playerItem: AVPlayerIteminit(playerItem: AVPlayerItem? = nil) {super.init()self.playerItem = playerItemself.player = AVPlayer(playerItem: playerItem)if let player = player {playerView = PHPlayerView(player: player)}addObserverForPlayerItem()}

替换当前AVPlayerItem方法:

    /// AVPlayer的同名方法,替换当前播的资源////// - Parameters:///   - playerItem: AVPlayerItemfunc replaceCurrentItem(playerItem:AVPlayerItem?) {guard let player = self.player else { return }self.playerItem = playerItemplayer.replaceCurrentItem(with: playerItem)addObserverForPlayerItem()}/// 为AVPlayerItem添加监听func addObserverForPlayerItem() {guard let playerItem = playerItem else { return }playerItem.addObserver(self, forKeyPath: status_keypath, context: &playerItemContext)}

另外我们将播放进度的监听由原来的0.5改为了1/60秒,因为我们需要使用它来同步动画,而不仅仅是显示当前时间。

    /// 监听播放进度func addPlayerItemTimeObserver() {guard let player = player else { return }let interval = CMTimeMakeWithSeconds(1/60.0, preferredTimescale: Int32(NSEC_PER_SEC))let queue = DispatchQueue.maintimeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: queue, using: {[weak self] time inguard let self = self else { return }guard let playerItem = self.playerItem else { return }guard let delegate = self.delegate else { return }let currentTime = CMTimeGetSeconds(time)let duration = CMTimeGetSeconds(playerItem.duration)delegate.setCuttentTime(time: currentTime, duration: duration)})}
编辑器

编辑器由两部分构成,媒体资源选择器,和媒体资源编辑区域。

博客的示例项目中,我们只获取了视频媒体资源,音频媒体资源的做法与视频完全相同,只是传入的mediaType为audio。

媒体资源选择器为一个简单的列表,点击加号后会根据所选择的媒体资源创建一个PHMediaItem,PHMediaItem是一个基类,它的子类又分为PHVideoItem和PHAudioItem,后续的功能我们也许会用到PHAudioItem,但目前我们只需要使用PHVideoItem即可。

媒体资源选择器

媒体资源编辑器就是页面除播放器以外的下半部分,有显示媒体选择器的按钮,和控制播放器播放和暂停的按钮,以及一个显示媒体剪辑状态的时间轴区域。

媒体编辑器

创建组合

我们的核心任务就是通过页面上的一些操作来创建一个媒体组合,首先声明一个PHComposition协议,协议中定义了两个方法分别用来生成组合的可播放版本和可导出版本。

import UIKit
import AVFoundationprotocol PHComposition {/// 协议方法-生成AVPlayerItem////// - Returns: 返回一个可播放的AVPlayerItemfunc makePlayerItem() -> AVPlayerItem?/// 协议方法-生成AVAssetExportSession////// - Returns: 返回一个可导出的AVAssetExportSessionfunc makeAssetExportSession() -> AVAssetExportSession?}

创建一个遵循PHComposition协议的类,并提供协议方法的实现。

//  负责创建 视频的可播放资源和可导出资源import UIKit
import AVFoundationclass PHBaseComposition: NSObject,PHComposition {//只读compositionprivate var compostion:AVComposition?//自定义初始化init(compostion: AVComposition? = nil) {self.compostion = compostion}//MARK: PHComposition - 生成 AVPlayerItemfunc makePlayerItem() -> AVPlayerItem? {if let compostion = compostion {let playerItem =  AVPlayerItem(asset: compostion)return playerItem}return nil}//MARK: PHComposition - 生成 AVAssetExportSessionfunc makeAssetExportSession() -> AVAssetExportSession? {return nil}
}

创建一个组合的构建器,同样我们创建一个协议,负责来创建遵循PHComposition协议的对象。

import UIKitprotocol PHCompositionBuilder {/// 协议方法-生成一个遵循PHComposition协议的对象////// - Returns: 返回一个最新PHComposition协议的对象func buildComposition() -> PHComposition?
}

这个协议的具体方法由PHBaseCompositionBuilder来实现,代码如下。

import UIKit
import AVFoundationclass PHBaseCompositionBuilder: NSObject,PHCompositionBuilder {/// 时间线var timeLine:PHTimeLine!/// compositionprivate var composition = AVMutableComposition()init(timeLine: PHTimeLine!) {self.timeLine = timeLine}//MARK: PHCompositionBuilder - 生成 PHCompositionfunc buildComposition() -> PHComposition? {addCompositionTrack(mediaType: .video, mediaItems: timeLine.videoItmes)return PHBaseComposition(compostion: self.composition)}/// 私有方法-添加媒体资源轨道/// - Parameters:///   - mediaType: 媒体类型///   - mediaItems: 媒体媒体资源数组/// - Returns: 返回一个可播放的AVPlayerItemprivate func addCompositionTrack(mediaType:AVMediaType,mediaItems:[PHMediaItem]?) {if PHIsEmpty(array: mediaItems) {return}let trackID = kCMPersistentTrackID_Invalidguard let compositionTrack = composition.addMutableTrack(withMediaType: mediaType, preferredTrackID: trackID) else { return }//设置起始时间var cursorTime = CMTime.zeroguard let mediaItems = mediaItems else { return }for item in mediaItems {//这里默认时间都是从0开始guard let asset = item.asset else { continue }guard let assetTrack = asset.tracks(withMediaType: mediaType).first  else { continue }do {try compositionTrack.insertTimeRange(item.timeRange, of: assetTrack, at: cursorTime)} catch {print("addCompositionTrack error")}cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration)}}
}
  1. PHBaseCompositionBuilder在初始化的时候会默认创建一个AVMutableComposition对象用于媒体编辑操作。
  2. 获取timeLine对象中的所有视频资源,调用addCompositionTrack方法进行拼接。
  3. addCompositionTrack方法中首先判断了传入的资源数组是否为空。
  4. 当媒体资源数组不为空的时候,从composition中获取对应的媒体轨道。
  5. 设置起始时间,遍历媒体资源数组,从每个资源中获取对应的媒体轨道并添加到组合媒体轨道中。
  6. 修改下一个媒体资源的插入起始时间。

实现播放组合媒体

选择媒体资源

点击页面上的加号按钮,显示媒体选择列表,点击列表后会将选择的媒体资源创建为PHMediaItem并添加到当前的timeLine对应的资源数组下。

    //MARK: 显示选择视频视图@objc func showItemPickerView() {let resourcePickerView = PHResourcePickerView(frame: CGRect(x: 0, y: UIScreen.main.bounds.height * 0.5, width: 150.0, height: 200.0))resourcePickerView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1.0)resourcePickerView.layer.masksToBounds = trueresourcePickerView.layer.cornerRadius = 5.0resourcePickerView.layer.borderColor = UIColor.white.cgColorresourcePickerView.layer.borderWidth = 1.0resourcePickerView.showDialog()resourcePickerView.addMediaItemBlock = { [weak self] mediaItem inguard let self = self else { return }if var videoItmes = timeLine.videoItmes {videoItmes.append(mediaItem as! PHVideoItem)timeLine.videoItmes = videoItmes} else {var videoItmes = [PHVideoItem]()videoItmes.append(mediaItem as! PHVideoItem)timeLine.videoItmes = videoItmes}self.collectionView?.reloadData()self.needReplay = true}}
点击播放按钮

点击播放按钮后判断是否有可播放资源,再进行播放。播放分为两种情况,从头开始播放和暂停后的继续播放。

    //MARK: 播放按钮点击@objc func playerButtonOnlick(button:UIButton) {if PHIsEmpty(array: timeLine.videoItmes) {playerButton.isSelected = falsereturn}button.isSelected = !button.isSelected//回调guard let delegate = self.delegate else { return }if button.isSelected {if needReplay {player()needReplay = false} else {delegate.play()}} else {delegate.pause()}}func player() {guard let delegate = self.delegate else { return }let compositionBuilder = PHBaseCompositionBuilder(timeLine: timeLine)let composition = compositionBuilder.buildComposition()let playerItem = composition?.makePlayerItem()delegate.replaceCurrentItem(playerItem: playerItem)}
同步播放进度

PHEditorView视频编辑器遵循了PHControlDelegate协议,这里我们只关注setCuttentTime和playbackComplete方法。

setCuttentTime方法用来同步编辑器时间轴的进度。

playbackComplete用来同步播放按钮的状态。

extension PHEditorView:PHControlDelegate{func playpause(currentTime: TimeInterval) {}func playstart(duration: TimeInterval) {}func setCuttentTime(time: TimeInterval, duration: TimeInterval) {let origin_offsetX = -UIScreen.main.bounds.width * 0.5self.collectionView?.contentOffset = CGPointMake(origin_offsetX + time * item_size.width, 0.0)}func playbackComplete() {self.playerButton.isSelected = false}}

结语

在示例项目中,我们仅仅涉及了视频媒体资源,并默认这些资源都是单轨道的。然而,在实际的应用开发中,我们可能会面对更加复杂的情况,涉及到多种类型的媒体资源,以及多轨道的组合。iOS AVFoundation框架为我们提供了强大的工具和灵活的接口,让我们能够处理各种各样的媒体资源,并将它们巧妙地组合成为精彩纷呈的作品。通过深入理解和灵活运用AVFoundation框架,我们可以实现更加复杂和令人惊叹的音视频编辑应用,为用户带来全新的体验和享受。在今后的开发过程中,让我们继续探索和挖掘AVFoundation框架的潜力,创造出更加优秀和创新的音视频编辑应用!

项目地址:PHEditorPlayer: AV Foundation 音视频编辑

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/794653.shtml

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

相关文章

51之定时器与中断系统

目录 1.定时器与中断系统简介 1.1中断系统 1.2定时器 1.2.1定时器简介 1.2.2定时器大致原理及其配置 1.2.3定时器所需的所有配置总介 2.定时器0实现LED闪烁 3.使用软件生成定时器初始化程序 1.定时器与中断系统简介 1.1中断系统 首先,我们需要来了解一下什么…

深入浅出 -- 系统架构之单体到分布式架构的演变

一、传统模式的技术改革 在很多年以前,其实没有严格意义上的前后端工程师之分,每个后端就是前端,同理,前端也可以是后端,即Ajax、jQuery技术未盛行前的年代。 起初,大部分前端界面很简单,显示的…

AcWing1402.星空之夜

【题目链接】1402. 星空之夜 - AcWing题库 夜空深处,闪亮的星星以星群的形式出现在人们眼中,形态万千。 一个星群是指一组非空的在水平,垂直或对角线方向相邻的星星的集合。 一个星群不能是一个更大星群的一部分。 星群可能是相似的。 如…

【蓝桥杯】GCD与LCM

一.概述 最大公约数(GCD)和最小公倍数(Least Common Multiple,LCM) 在C中,可以使用 std::__gcd(a, b)来计算最大公约数 1.欧几里德算法/辗转相除法 int gcd(int a,int b){return b?gcd(b, a%b):a; } 2…

Tensorboard以及Transforms初步学习

一.前情提要 1.本文是代码结合知识点,注释即为知识点 2.主要详细讲解Tensorboard以及Transforms代码以及基础知识 3.若想深入学习,建议阅读 P3. Python学习中的两大法宝函数(当然也可以用在PyTorch)_哔哩哔哩_bilibili 二.简述…

抖音运营技巧

1、视频时长 抖音的作品是否能够继续被推荐,取决于综合数据,包括完播率、点赞率、评论率、转发率和收藏率等。其中,完播率是最容易控制的因素。对于新号来说,在没有粉丝的初期,发布过长的视频可能会导致无人观看。因此…

金田金业带你一文了解现货黄金开户注意事项

现货黄金作为一种受欢迎的投资品种,吸引了越来越多的投资者。然而,在进行现货黄金开户时,有一些重要的注意事项需要牢记。本文将为您详细介绍现货黄金开户的九大关键要点,确保您能够在投资过程中做出明智的决策。 第一&#xff0c…

Java- maven下载jar包,提示找不到,Could not find artifact

1、执行下面命令行 mvn install:install-file -Dfile/home/quangang/桌面/isv-sdk-2.0.jar -DgroupIdcom.jd -DartifactIdisv-sdk -Dversion2.0 -Dpackangjar 2、然后这里要加上jar包

No dashboards are active for the current data set.

再次记录一下这个离谱的问题 之前出现这个问题是因为目录没写对 今天遇到这个问题的原因是目录是对的,跟目录是否带有中文也没关系 是writer写入的时候写的是空的,离谱的是写入是空的情况下也会生成events日志文件,看起来好像成功写入了一样&…

Spring源码解析上

spring源码解析 整体架构 defaultListableBeanFactory xmlBeanDefinitionReader 创建XmlBeanFactory 对资源文件进行加载–Resource 利用LoadBeandefinitions(resource)方法加载配置中的bean loadBeandefinitions加载步骤 doLoadBeanDefinition xml配置模式 validationMode 获…

2024-04-05 问AI: 介绍一下深度学习中的Leaky ReLU函数

文心一言 Leaky ReLU(Leaky Rectified Linear Unit)函数是深度学习领域中的一种激活函数,主要用于解决ReLU(Rectified Linear Unit)函数在负值区域为零的问题。ReLU函数在输入为正时保持原样,而在输入为负…

nodejs实现TCP端口转发并截包的小工具

近期我正致力于开发一个基于 Go-CQHTTP 的 QQ 机器人应用程序项目,该项目现已成功实现了 Go-CQHTTP 的容器化部署,利用 Docker 技术确保其运行环境的一致性与便捷性。随着项目推进,接下来的工作重心转向部署配套的签名服务器(qsig…

软考111-上午题-【计算机网络】-URL和DNS

一、URL解析 org:各类组织结构(非盈利团队) 1-1、顶级域 顶级域名是域名的最后一个部分,即是域名最后一点之后的字母,例如:www.baidu.com这个域名中,顶级域是.com(或.COM&#xff…

数据结构系列-队列的结构和队列的实现

🌈个人主页:羽晨同学 💫个人格言:“成为自己未来的主人~” 队列 队列的概念及结构 队列:只允许在一端进行插入数据操作,在另一端进行删除删除数据操作的特殊线性表,队列具有先进先出FIFO,…

全面解析找不到msvcr110.dll,无法继续执行代码的解决方法

MSVCR110.dll的丢失可能导致某些应用程序无法启动。当用户试图打开依赖于该特定版本DLL文件的软件时,可能会遭遇“找不到指定模块”的错误提示,使得程序启动进程戛然而止。这种突如其来的故障不仅打断了用户的正常工作流程,也可能导致重要数据…

基于SpringBoot+微信小程序的农产品销售平台

一、项目背景介绍: 随着人们收入的不断增加、生活水平的普遍提高,对生活质量的要求也日益凸显。而作为关乎每个人的生命、健康安全的食品卫生、质量无疑更被人们所重视。所以,… 2. 其他国家的绿色有机食品所占其国家食品市场比重比较大,如德国在99年便已达到40%,美…

Mac反编译APK

文章目录 第一种方式: brew installapktool 使用说明dex2jar 使用说明 第二种方式: 下载安装包apktool 使用说明 (根据官方介绍没有操作成功,后续成功再更新这里)dex2jar 使用说明 安装 JD-GUI 查看jar包中的class文件JD-GUI 使用说明 第一种方式: brew install 安装过程可能很…

使用 mitmproxy 抓包 grpc

昨天在本地执行 grpc 的 quick start(python版本的),我了解 grpc 内部使用的是 HTTP2,所以我就想着抓包来试试,下面就来记录一下这个过程中的探索。 注意:我的电脑上面安装了 Fiddler Classic,…

微信小程序生命周期管理:从数据初始化到事件绑定

作为一个独立的应用开发平台,微信小程序提供了自己的生命周期机制,与我们熟悉的Vue.js框架有一些差异。掌握小程序生命周期的特点和使用技巧,对于开发高质量的小程序应用至关重要。深入理解和掌握小程序生命周期的使用技巧,将有助于我们构建出更加健壮和可维护的小程序应用。 小…