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 秒的空白视频文件,其分辨率为您想要的分辨率,例如 1920 x 1080
- 您从该视频资产中检索视频轨道
- 从您的音频文件中检索音轨
- 创建一个
AVMutableComposition
用于合并音频和视频轨道
- 配置
AVMutableCompositionTrack
音轨并将其添加到主 AVMutableComposition
- 使用视频轨道配置
AVMutableVideoComposition
- 使用
AVAssetExportSession
导出带有 AVMutableComposition
和 AVMutableVideoComposition
的最终视频
代码
在下面的大部分代码中,您会看到多个 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
设置为电影节目,例如 AVAssetExportPresetHighestQuality
或 AVAssetExportPresetLowQuality
,而不是 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
}
}
}
}
最后的想法
我有一个使用 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 秒的空白视频文件,其分辨率为您想要的分辨率,例如 1920 x 1080
- 您从该视频资产中检索视频轨道
- 从您的音频文件中检索音轨
- 创建一个
AVMutableComposition
用于合并音频和视频轨道 - 配置
AVMutableCompositionTrack
音轨并将其添加到主AVMutableComposition
- 使用视频轨道配置
AVMutableVideoComposition
- 使用
AVAssetExportSession
导出带有AVMutableComposition
和AVMutableVideoComposition
的最终视频
代码
在下面的大部分代码中,您会看到多个 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
设置为电影节目,例如 AVAssetExportPresetHighestQuality
或 AVAssetExportPresetLowQuality
,而不是 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
}
}
}
}
最后的想法