Swift - 将音频 URL 转换为视频 URL 无法在照片库中播放

Swift -Converted Audio URL to Video URL Doesn't Play in Photos Library

我有一个使用 AVAudioRecorder 创建的音频 url (.m4a)。我想在 Instagram 上分享该音频,所以我将音频转换为视频。问题是在转换之后,当我使用 UIActivityViewController 将视频 url 保存到文件应用程序时,我可以重播视频、查看时间(例如 7 秒)并毫无问题地听到音频。出现带有声音图标的黑屏。

但是当我使用 UIActivityViewController 将视频保存到照片库时,视频显示了 7 秒但没有播放,视频全是灰色,并且没有显示声音图标。

为什么视频在“文件”应用程序中成功 saving/playing 但在照片库中保存而不播放?

let asset: AVURLAsset = AVURLAsset(url: audioURL)

let mixComposition = AVMutableComposition()
guard let compositionTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: CMPersistentTrackID()) else { return }

let track = asset.tracks(withMediaType: .audio)
guard let assetTrack = track.first else { return }

do {
            
    try compositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: assetTrack.timeRange.duration), of: assetTrack, at: .zero)
            
} catch {
    print(error.localizedDescription)
}

guard let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetPassthrough) else { return }

let dirPath = NSTemporaryDirectory().appending("\(UUID().uuidString).mov")
let outputFileURL = URL(fileURLWithPath: dirPath)

exporter.outputFileType = .mov
exporter.outputURL = outputFileURL
exporter.shouldOptimizeForNetworkUse = true

exporter.exportAsynchronously {
    switch exporter.status {

    // ...
    guard let videoURL = exporter.outputURL else { return }


        // present UIActivityViewController to save videoURL and then save it to the Photos Library via 'Save Video`
    }
}

因此,尽管我的问题中的代码确实将音频文件转换为视频文件,但仍然没有 video track。我知道这是事实,因为在我从我的问题中得到出口商的 videoURL 之后,我试图向它添加一个 并且在水印代码中它一直在

上崩溃
let videoTrack = asset.tracks(withMediaType: AVMediaType.video)[0]

基本上我问题中的代码将音频转换为视频但不添加视频轨道。

我假设发生的事情是当 Files 应用程序 读取文件时,它知道它是一个 .mov 或 .mp4 文件,然后 它会即使视频轨道丢失也播放音频轨道.

相反,当 Photos 应用程序 读取文件时,它也知道它是 .mov 或 .mp4 文件,但 如果没有视频轨道, 它不会播放任何东西.

我必须结合这 2 个答案才能让音频在照片应用中作为视频播放。

1st- 我将我的应用程序图标(您可以添加任何图像)作为 1 张图像添加到图像数组中,以使用来自 [=17= 的代码制作视频轨道] @scootermg 回答。

来自@scootermg 的答案的代码很方便地位于@dldnh GitHub 的 1 个文件中。在他的代码中,在 ImageAnimator class 函数中,在 render 函数中,我没有保存到库,而是在 completionHandler 中返回了 videoWriter's output URL

2nd- 我使用@回答的 Swift Merge audio and video files into one video 中的代码将我刚刚制作的应用程序图标视频与我问题中的音频 url 组合在一起TungFam

在 TungFam 回答的 mixCompostion 中,我使用音频 url 的资产持续时间作为视频的长度。

do {
    try mutableCompositionVideoTrack[0].insertTimeRange(CMTimeRangeMake(start: .zero,
                                                                            duration: aAudioAssetTrack.timeRange.duration),
                                                            of: aVideoAssetTrack,
                                                            at: .zero)

    try mutableCompositionAudioTrack[0].insertTimeRange(CMTimeRangeMake(start: .zero,
                                                                            duration: aAudioAssetTrack.timeRange.duration),
                                                            of: aAudioAssetTrack,
                                                            at: .zero)

    if let aAudioOfVideoAssetTrack = aAudioOfVideoAssetTrack {
        try mutableCompositionAudioOfVideoTrack[0].insertTimeRange(CMTimeRangeMake(start: .zero,
                                                                                       duration: aAudioAssetTrack.timeRange.duration),
                                                                       of: aAudioOfVideoAssetTrack,
                                                                       at: .zero)
    }
} catch {
    print(error.localizedDescription)
}

正如 Lance 正确指出的那样,问题是虽然导出了 .mov.mp4 格式的文件,但没有视频,只是播放音频。

再多读一点,例如,.mp4 只是一种数字多媒体容器格式,可以很好地用于音频,因此可以将音频文件保存为 .mp4 / .mov。

需要的是向 AVMutableComposition 添加一个空视频轨道才能成功。 Lance 已经发布了一个很好的解决方案,效果非常好,并且比我提出的依赖于空白 1 秒视频的替代解决方案 self sustained 更多。

工作原理概述

  1. 您将获得一个长度为 1 秒的空白视频文件,其分辨率为您想要的分辨率,例如 1920 x 1080
  2. 您从该视频资产中检索视频轨道
  3. 从您的音频文件中检索音轨
  4. 创建一个 AVMutableComposition 用于合并音频和视频轨道
  5. 配置 AVMutableCompositionTrack 音轨并将其添加到主 AVMutableComposition
  6. 使用视频轨道配置AVMutableVideoComposition
  7. 使用 AVAssetExportSession 导出带有 AVMutableCompositionAVMutableVideoComposition
  8. 的最终视频

代码

在下面的大部分代码中,您会看到多个 guard 语句。您可以创建一个守卫,但是,了解此类任务发生故障的位置可能很有用,因为导出失败的原因可能有多种。

配置音轨

private func configureAudioTrack(_ audioURL: URL,
                                 inComposition composition: AVMutableComposition) -> AVMutableCompositionTrack?
{
    // Initialize an AVURLAsset with your audio file
    let audioAsset: AVURLAsset = AVURLAsset(url: audioURL)
    
    let trackTimeRange = CMTimeRange(start: .zero,
                                     duration: audioAsset.duration)
    
    // Get the audio track from the audio asset
    guard let sourceAudioTrack = audioAsset.tracks(withMediaType: .audio).first
    else
    {
        manageError(nil, withMessage: "Error retrieving audio track from source file")
        return nil
    }
    
    // Insert a new video track to the AVMutableComposition
    guard let audioTrack = composition.addMutableTrack(withMediaType: .audio,
                                                       preferredTrackID: CMPersistentTrackID())
    else
    {
        // manage your error
        return nil
    }
    
    do {
        // Inset the contents of the audio source into the new audio track
        try audioTrack.insertTimeRange(trackTimeRange,
                                       of: sourceAudioTrack,
                                       at: .zero)
    }
    catch {
        // manage your error
    }
    
    return audioTrack
}

配置视频轨道

private func configureVideoTrack(inComposition composition: AVMutableComposition) -> AVMutableCompositionTrack?
{
    // Initialize a video asset with the empty video file
    guard let blankMoviePathURL = Bundle.main.url(forResource: "blank",
                                                  withExtension: ".mp4"), 
    let videoAsset = AVAsset(url: blankMoviePathURL)
    else
    {
        // manage errors
        return nil
    }
    
    // Get the video track from the empty video
    guard let sourceVideoTrack = videoAsset.tracks(withMediaType: .video).first
    else
    {
        // manage errors
        return nil
    }
    
    // Insert a new video track to the AVMutableComposition
    guard let videoTrack = composition.addMutableTrack(withMediaType: .video,
                                                       preferredTrackID: kCMPersistentTrackID_Invalid)
    else
    {
        // manage errors
        return nil
    }
    
    let trackTimeRange = CMTimeRange(start: .zero,
                                     duration: composition.duration)
    
    do {
        
        // Inset the contents of the video source into the new audio track
        try videoTrack.insertTimeRange(trackTimeRange,
                                       of: sourceVideoTrack,
                                       at: .zero)
        
    }
    catch {
        // manage errors
    }
    
    return videoTrack
}

配置视频合成

// Configure the video properties like resolution and fps
private func createVideoComposition(with videoCompositionTrack: AVMutableCompositionTrack) -> AVMutableVideoComposition
{
    let videoComposition = AVMutableVideoComposition()
    
    // Set the fps
    videoComposition.frameDuration = CMTime(value: 1,
                                            timescale: 25)
    
    // Video dimensions
    videoComposition.renderSize = CGSize(width: 1920, height: 1080)
    
    // Specify the duration of the video composition
    let instruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRange(start: .zero, duration: .indefinite)
    
    // Add the video composition track to a new layer
    let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompositionTrack)
    let transform = videoCompositionTrack.preferredTransform
    layerInstruction.setTransform(transform, at: .zero)
    
    // Apply the layer configuration instructions
    instruction.layerInstructions = [layerInstruction]
    videoComposition.instructions = [instruction]
    
    return videoComposition
}

配置 AVAssetExportSession

private func configureAVAssetExportSession(with composition: AVMutableComposition,
                                           videoComposition: AVMutableVideoComposition) -> AVAssetExportSession?
{
    // Configure export session
    guard let exporter = AVAssetExportSession(asset: composition,
                                              presetName: AVAssetExportPresetHighestQuality)
    else
    {
        // Manage your errors
        return nil
    }
    
    // Configure where the exported file will be stored
    let documentsURL = FileManager.default.urls(for: .documentDirectory,
                                                in: .userDomainMask)[0]
    
    let fileName = "\(UUID().uuidString).mov"
    let dirPath = documentsURL.appendingPathComponent(fileName)
    let outputFileURL = dirPath
    
    // Apply exporter settings
    exporter.videoComposition = videoComposition
    exporter.outputFileType = .mov
    exporter.outputURL = outputFileURL
    exporter.shouldOptimizeForNetworkUse = true
    
    return exporter
}

在这里,一件重要的事情是不要将出口商的 present quality 设置为电影节目,例如 AVAssetExportPresetHighestQualityAVAssetExportPresetLowQuality,而不是 AVAssetExportPresetPassthrough根据文档,

A preset to export the asset in its current format, unless otherwise prohibited.

所以你仍然会得到一个音频 mp4 或 mov 文件,因为当前的合成格式是音频。我没有对此进行广泛测试,但这是来自一些测试。

最后,你可以像这样把上面所有的功能结合起来:

func generateMovie(with audioURL: URL)
{
    delegate?.audioMovieExporterDidStart(self)
    
    let composition = AVMutableComposition()
    
    // Configure the audio and video tracks in the new composition
    guard let _ = configureAudioTrack(audioURL, inComposition: composition),
          let videoCompositionTrack = configureVideoTrack(inComposition: composition)
    else
    {
        // manage error
        return
    }
    
    let videoComposition = createVideoComposition(with: videoCompositionTrack)
    
    if let exporter = configureAVAssetExportSession(with: composition,
                                                    videoComposition: videoComposition)
    {
        exporter.exportAsynchronously
        {
            switch exporter.status {
                
                case .completed:
                    guard let videoURL = exporter.outputURL
                    else
                    {
                        // manage errors
                        return
                    }
                    
                    // notify someone the video is ready at videoURL
                    
                default:
                    // manege error
            }
        }
    }
}

最后的想法

  1. 您可以试驾工作示例 here
  2. 如果您想使用它,我将其转换为一个简单的库,您可以在其中配置方向、fps 甚至为视频设置背景颜色 - 可在 same link
  3. 如果您只想要空白视频,可以从 here
  4. 获取它们