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

引言

当涉及到音视频编辑时,媒体资源的提取和组合是至关重要的环节。在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.zero
        let audioDuration = composition.duration
        let 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: AVPlayerItem
    init(playerItem: AVPlayerItem? = nil) {
        super.init()
        self.playerItem = playerItem
        self.player = AVPlayer(playerItem: playerItem)
        if let player = player {
            playerView = PHPlayerView(player: player)
        }
        addObserverForPlayerItem()
    }

替换当前AVPlayerItem方法:

    /// AVPlayer的同名方法,替换当前播的资源
    ///
    /// - Parameters:
    ///   - playerItem: AVPlayerItem
    func replaceCurrentItem(playerItem:AVPlayerItem?) {
        guard let player = self.player else { return }
        self.playerItem = playerItem
        player.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.main
        timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: queue, using: {[weak self] time in
            guard 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 AVFoundation

protocol PHComposition {
    
    /// 协议方法-生成AVPlayerItem
    ///
    /// - Returns: 返回一个可播放的AVPlayerItem
    func makePlayerItem() -> AVPlayerItem?

    /// 协议方法-生成AVAssetExportSession
    ///
    /// - Returns: 返回一个可导出的AVAssetExportSession
    func makeAssetExportSession() -> AVAssetExportSession?

}

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

//  负责创建 视频的可播放资源和可导出资源

import UIKit
import AVFoundation

class PHBaseComposition: NSObject,PHComposition {
    
    //只读composition
    private var compostion:AVComposition?
    
    //自定义初始化
    init(compostion: AVComposition? = nil) {
        self.compostion = compostion
    }
    
    //MARK: PHComposition - 生成 AVPlayerItem
    func makePlayerItem() -> AVPlayerItem? {
        if let compostion = compostion {
            let playerItem =  AVPlayerItem(asset: compostion)
            return playerItem
        }
        return nil
    }
    
    //MARK: PHComposition - 生成 AVAssetExportSession
    func makeAssetExportSession() -> AVAssetExportSession? {
        return nil
    }
}

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

import UIKit

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

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

import UIKit
import AVFoundation

class PHBaseCompositionBuilder: NSObject,PHCompositionBuilder {
    
    /// 时间线
    var timeLine:PHTimeLine!
    /// composition
    private var composition = AVMutableComposition()
    
    init(timeLine: PHTimeLine!) {
        self.timeLine = timeLine
    }
    
    //MARK: PHCompositionBuilder - 生成 PHComposition
    func buildComposition() -> PHComposition? {
        addCompositionTrack(mediaType: .video, mediaItems: timeLine.videoItmes)
        return PHBaseComposition(compostion: self.composition)
    }
    
    /// 私有方法-添加媒体资源轨道
    /// - Parameters:
    ///   - mediaType: 媒体类型
    ///   - mediaItems: 媒体媒体资源数组
    /// - Returns: 返回一个可播放的AVPlayerItem
    private func addCompositionTrack(mediaType:AVMediaType,mediaItems:[PHMediaItem]?) {
        if PHIsEmpty(array: mediaItems) {
            return
        }
        let trackID = kCMPersistentTrackID_Invalid
        guard let compositionTrack = composition.addMutableTrack(withMediaType: mediaType, preferredTrackID: trackID) else { return }
        //设置起始时间
        var cursorTime = CMTime.zero
        guard 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 = true
        resourcePickerView.layer.cornerRadius = 5.0
        resourcePickerView.layer.borderColor = UIColor.white.cgColor
        resourcePickerView.layer.borderWidth = 1.0
        resourcePickerView.showDialog()
        resourcePickerView.addMediaItemBlock = { [weak self] mediaItem in
            guard 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 = false
            return
        }
        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.5
        self.collectionView?.contentOffset = CGPointMake(origin_offsetX + time * item_size.width, 0.0)
    }
    
    func playbackComplete() {
        self.playerButton.isSelected = false
    }
    
}

结语

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SV学习笔记(二)

接口 什么是接口? 接口 主要用作验证 ,国外有些团队会使用sv进行设计,那么接口就会用作设计。验证环境中,接口可以 使连接变得简洁而不易出错 。interface和module的使用性质很像, 可以定义端口,也可以定…

文心一言 VS 讯飞星火 VS chatgpt (230)-- 算法导论17.1 1题

一、如果栈操作包括 MULTIPUSH 操作,它将 k 个数据项压入栈中,那么栈操作的摊还代价的界还是 O(1) 吗? 文心一言: 在栈的基本操作中,我们通常考虑的是单个元素的压入(push)和弹出(pop&#x…

升级程序到Java21的记录二(修改程序源代码)

背景 :参看 《升级程序到Java21的记录一(先升级jdk到21)》, Jdk升级完毕,下面我们该秀修改程序源代码了, 程序最初使用的springboot2.6.8 以及jdk17。为了使用springboot 3.0(3.0开始有支持虚拟线程的相关…

抖音运营技巧

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

Day31|贪心算法part01:理论基础、455.分发饼干、376. 摆动序列、53. 最大子序和

理论基础 记得贪心没有规律即可!解不出来就看题解。 455. 分发饼干 先把学生和饼干都排序(Arrays.sort只能升序),然后都从后往前遍历,把最大的饼干给需求最大的孩子(贪心) class Solution {…

4核8G服务器配置性能怎么样?4核8G12M配置服务器能干啥?

腾讯云4核8G服务器多少钱?腾讯云4核8G轻量应用服务器12M带宽租用价格646元15个月,活动页面 txybk.com/go/txy 活动链接打开如下图所示: 腾讯云4核8G服务器优惠价格 这台4核8G服务器是轻量应用服务器,详细配置为:轻量4核…

内网安全之-kerberos协议

kerberos协议是由麻省理工学院提出的一种网络身份验证协议,提供了一种在开放的非安全网络中认证识别用户身份信息的方法。它旨在通过使用秘钥加密技术为客户端/服务端应用提供强身份验证,使用kerberos这个名字是因为需要三方的共同参与才能完成一次认证流…

【C++】stack和queue

个人主页 : zxctscl 如有转载请先通知 文章目录 1. stack的介绍和使用1.1 stack的介绍1.2 stack的使用1.3 stack的模拟实现 2. queue的介绍和使用2.1 queue的介绍2.2 queue的使用2.3 queue的模拟实现 3. 容器适配器3.1 概念3.2 STL标准库中stack和queue的底层结构3.…

@RequestParam和@PathVariable的区别

同样都是接收URL中的参数,RequestParam和PathVariable有什么区别呢?

随手集☞Spring知识盘点

概述 定义 Spring框架的提出者是程序员Rod Johnson,他在2002年最早提出了这个框架的概念,随后创建了这个框架。Spring框架的目标是简化企业级Java应用程序的开发,通过提供一套全面的工具和功能,使开发者能够更加高效地构建高质量…

Prometheus+grafana环境搭建MongoDB(docker+二进制两种方式安装)(五)

由于所有组件写一篇幅过长,所以每个组件分一篇方便查看,前四篇mongodb的exporter坑也挺多总结一下各种安装方式,方便后续考古。 Prometheusgrafana环境搭建方法及流程两种方式(docker和源码包)(一)-CSDN博客 Prometheusgrafana环境搭建rabb…

5分钟润色一篇论文:ChatGPT意味着什么?Nature连发两篇文章探讨

2023年随着OpenAI开发者大会的召开,最重磅更新当属GPTs,多模态API,未来自定义专属的GPT。微软创始人比尔盖茨称ChatGPT的出现有着重大历史意义,不亚于互联网和个人电脑的问世。360创始人周鸿祎认为未来各行各业如果不能搭上这班车…

服务器硬件构成与性能要点:CPU、内存、硬盘、RAID、网络接口卡等关键组件的基础知识总结

文章目录 服务器硬件基础知识CPU(中央处理器)内存(RAM)硬盘RAID(磁盘阵列)网络接口卡(NIC)电源散热器主板显卡光驱 服务器硬件基础知识 服务器是一种高性能计算机,用于在…

第1章:芯片及引脚介绍

芯片及引脚介绍 1: 芯片介绍1.1:芯片系列1.2 :STM32F103C8T6型号的介绍 2:引脚2.1:寄存器2.2:最小系统板 3:最小系统板的引脚3.1:特殊引脚3.2:普通引脚3.3:最…

Linux之信号

1.常见信号 虽然最开始的编号是1,最后的编号是64,但是并不是有64个信号,没有32和33号信号,也就是说,一共有62个信号,前31个信号是标准信号(非实时信号),后31个信号是实时…

Android自定义view;实现掌阅打开书籍动画效果

这里利用自定义view的方式来处理,初始化数据,camera通过setLocation调整相机的位置,但是Camera 的位置单位是英寸,英寸和像素的换算单位在 Skia 中被写成了72 像素,8 x 72 576,所以它的默认位置是 (0, 0, …

Linux基础篇:Linux网络yum源——以配置阿里云yum源为例

Linux网络yum源——以阿里云为例 一、网络yum源介绍 Linux中的YUM(Yellowdog Updater, Modified)源是一个软件包管理器,它可以自动处理依赖关系并安装、更新、卸载软件包。YUM源是一个包含软件包的远程仓库,它可以让用户轻松地安…

用户账号和组账号及管理

用户账号和组账号 Linux中每个用户是通过 User Id (UID)来唯一标识的 新建用户 1-60000 自动分配 0-65535 端口号,系统是靠uid来区分用户身份的,用户的uid 为0 就是超级管理员 1.用户账号的类型 超级管理员:权限最高的用户,roo…

Flutter Web 的未来,Wasm Native 即将到来

早在去年 Google I/O 发布 Flutter 3.10 的时候就提到过, Flutter Web 的未来会是 Wasm Native ,当时 Flutter 团队就表示,Flutter Web 的定位不是设计为通用 Web 的框架,类似的 Web 框架现在有很多,而 Flutter 的定位…

[lesson06]内联函数分析

内联函数分析 常量与宏回顾 C中的const常量可以替代宏常数定义,如: C中是否有解决方案替代宏代码片段? 内联函数 C中推荐使用内联函数替代宏代码片段 C中使用inline关键字声明内联函数 内联函数声明时inline关键字必须和函数定义结合在…