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

news2024/11/28 20:45:21

引言

当涉及到音视频编辑时,媒体资源的提取和组合是至关重要的环节。在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 音视频编辑

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

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

相关文章

Adaboost集成学习 | Matlab实现基于SVM-Adaboost支持向量机结合Adaboost集成学习时间序列预测(股票价格预测)

目录 效果一览基本介绍模型设计程序设计参考资料效果一览 基本介绍 Adaboost集成学习 | 基于SVM-Adaboost支持向量机结合Adaboost集成学习时间序列预测(股票价格预测)基于SVM(支持向量机)和AdaBoost集成学习的时间序列预测(如股票价格预测)是一种结合了两种强大机器学习算…

2_2.Linux中的远程登录服务

# 一.Openssh的功能 # 1.sshd服务的用途# #作用:可以实现通过网络在远程主机中开启安全shell的操作 Secure SHell >ssh ##客户端 Secure SHell daemon >sshd ##服务端 2.安装包# openssh-server 3.主配置文件# /etc/ssh/sshd_conf 4.…

浏览器工作原理与实践--编译器和解释器:V8是如何执行一段JavaScript代码的

前面我们已经花了很多篇幅来介绍JavaScript是如何工作的,了解这些内容能帮助你从底层理解JavaScript的工作机制,从而能帮助你更好地理解和应用JavaScript。 今天这篇文章我们就继续“向下”分析,站在JavaScript引擎V8的视角,来分析…

ROS2从入门到精通1-2:详解ROS2服务通信机制与自定义服务

目录 0 专栏介绍1 服务通信模型2 服务模型实现(C)3 服务模型实现(Python)4 自定义服务5 话题、服务通信的异同 0 专栏介绍 本专栏旨在通过对ROS2的系统学习,掌握ROS2底层基本分布式原理,并具有机器人建模和应用ROS2进行实际项目的开发和调试的工程能力。…

蓝桥备赛——贪心

题干 AC Code n, w = map(int, input().split()) # n种类, w核载重 a = [] # [[weight1, value1], [weight2, value2], ...] for _ in range(n):a.append(list(map(int, input().split()))) a.sort(key=lambda x: x[1] / x[0], reverse=True)maxVal = 0for i in a:if i[0…

可视化图表:K线图,快速搞清价格波动。

2023-08-21 21:20贝格前端工场 Hi,我是贝格前端工场的老司机,本文分享可视化图表设计的K线图设计,欢迎老铁持续关注我们。 一、K线图的含义 K线图(K Line Chart)是一种常用于股票、期货等金融市场的可视化图表&…

【详细讲解WebView的使用与后退键处理】

🎥博主:程序员不想YY啊 💫CSDN优质创作者,CSDN实力新星,CSDN博客专家 🤗点赞🎈收藏⭐再看💫养成习惯 ✨希望本文对您有所裨益,如有不足之处,欢迎在评论区提出…

前端对数据进行分组和计数处理

js对数组数据的处理,添加属性,合并表格数据。 let data[{id:1,group_id:111},{id:2,group_id:111},{id:3,group_id:111},{id:4,group_id:222},{id:5,group_id:222} ]let tempDatadata; tempDatatempData.reduce((arr,item)>{let findarr.find(i>i…

webpack搭建开发环境

webpack搭建开发环境 一.webpack开发模式二.webpack打包模式三.webpack打包模式应用四.Webpack 前端注入环境变量五.Webpack 开发环境调错 source map六. Webpack 设置解析别名路径七.优化-CDN的使用八.多页面打包九.优化-分割公共代码一.webpack开发模式 作用:启动 Web 服务…

原生js实现循环滚动效果

原生js实现如下图循环滚动效果 核心代码 <div class"scroll"><div class"blist" id"scrollContainer"><div class"bitem"></div>......<div class"bitem"></div></div> </di…

Web框架开发-Form组件和ajax实现注册

一、注册相关的知识点 1、Form组件 我们一般写Form的时候都是把它写在views视图里面,那么他和我们的视图函数也不影响,我们可以吧它单另拿出来,在应用下面建一个forms.py的文件来存放 2、局部钩子函数 1 2 3 4 5 6 7 # 局部钩子函数 def clean_username(self): userna…

Servlet Response的常用方法 缓存和乱码处理

前言 Servlet Response相关的信息&#xff0c;在service方法中使用的是HttpServletResponse&#xff0c;它继承自ServletResponse&#xff0c;扩展了Http协议相关的内容&#xff0c;下面简单记录一下它的基本用法。 一、response组成内容 以下是一个常见response响应的内容&…

【二叉树】Leetcode 114. 二叉树展开为链表【中等】

二叉树展开为链表 给你二叉树的根结点 root &#xff0c;请你将它展开为一个单链表&#xff1a; 展开后的单链表应该同样使用 TreeNode &#xff0c;其中 right 子指针指向链表中下一个结点&#xff0c;而左子指针始终为 null 。展开后的单链表应该与二叉树 先序遍历 顺序相同…

Linux(CentOS7)配置系统服务以及开机自启动

目录 前言 两种方式 /etc/systemd/system/ 进入 /etc/systemd/system/ 文件夹 创建 nginx.service 文件 重新加载 systemd 配置文件 ​编辑 配置开机自启 /etc/init.d/ 进入 /etc/init.d/ 文件夹 创建 mysql 文件 编写脚本内容 添加/删除系统服务 配置开机自启 …

如何使用Java语言发票查验接口实现发票真伪查验、票据ocr

随着时代潮流的发展&#xff0c;企业也在寻找更加便捷、高效的办公模式&#xff0c;尤其是针对财务工作人员而言&#xff0c;繁琐的发票录入、查验工作占据了财务人员的大部分时间。对此&#xff0c;翔云提供了发票识别接口、发票查验接口&#xff0c;那么企业应当如何将这些接…

笔记本三屏异显方案——更新中,是否能够在FPGA上实现,淘宝购物的价格太贵

三屏是&#xff08;笔记本电脑屏幕&#xff0c;两个显示器屏幕&#xff09;&#xff0c;异显是采用屏幕的扩展功能&#xff0c;这样能够左边看视频文章&#xff0c;右边control cv代码。 一、 电脑有一个HDMI口的时候&#xff0c;只需要买一个TypeC&#xff08;雷电接口&#x…

AI大模型在金融行业的应用场景和落地路径

作者&#xff1a;林建明 来源&#xff1a;IT阅读排行榜 本文摘编自《AIGC重塑金融&#xff1a;AI大模型驱动的金融变革与实践》&#xff0c;机械工业出版社出版这是最好的时代&#xff0c;也是最坏的时代。尽管大模型技术在金融领域具有巨大的应用潜力&#xff0c;但其应用也面…

STM32八种I/O口模式

STM32八种I/O口模式 文章目录 STM32八种I/O口模式前言一、stm32八种I/O类型二、区别1.模拟输入2.浮空输入3.上拉输入4.下拉输入5.推挽输出6.开漏输出7.复用推挽输出8.复用推挽输出 总结 前言 作为两年嵌入式软件攻城狮&#xff0c;还没仔细去理解过STM32的GPIO的八种使用模式&…

【蓝桥杯第十三届省赛B】(部分详解)

九进制转十进制 #include <iostream> #include<math.h> using namespace std; int main() {cout << 2*pow(9,3)0*pow(9,2)2*pow(9,1)2*pow(9,0) << endl;return 0; }顺子日期 #include <iostream> using namespace std; int main() {// 请在此…

Vue.js基础指令

(在讲指令之前,可以先了解插值表达式,如果已经知道,当我没说) 一.插值表达式 1.数据绑定最常见的形式就是双大括号的文本插值,Mustache上属性的值替代。只要绑定的数据对象上属性发生了改变,插值处的内容都会更新。,message 是将数据解析成纯文本的,也就是说,就算中…