问题
我有一个视频,我想将其放置在比视频本身更大的画布中。我使用
AVMutableVideoComposition
工具并通过增加 renderSize
属性来完成此操作。
无论我做什么,我都无法在调整大小的画布上获得透明度。我尝试使用蒙版,设置
backgroundColors
等等CALayers
,但我无法让透明度粘住。
复制
我创建了这个存储库,您可以在其中运行测试并亲自查看生成的视频:https://github.com/Jasperav/TransparencyVideo/tree/main/TransparencyVideoTests
下面是如何导入和导出视频的代码(与上面的存储库中的代码相同):
呼叫方
let video = Bundle(for: TransparencyVideoTests.self).url(forResource: "transparency", withExtension: ".mov")!
let fileManager = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
await VideoEditor().export(url: video, outputDir: fileManager.appending(path: "temp.mov"))
视频编辑器
import AppKit
import AVFoundation
import Foundation
import Photos
import QuartzCore
import OSLog
let logger = Logger()
class VideoEditor {
func export(
url: URL,
outputDir: URL
) async {
let asset = AVURLAsset(url: url)
let extract = try! await extractData(videoAsset: asset)
try! await exportVideo(outputPath: outputDir, asset: asset, videoComposition: extract)
}
private func exportVideo(outputPath: URL, asset: AVAsset, videoComposition: AVMutableVideoComposition) async throws {
let fileExists = FileManager.default.fileExists(atPath: outputPath.path())
logger.debug("Output dir: \(outputPath), exists: \(fileExists), render size: \(String(describing: videoComposition.renderSize))")
if fileExists {
do {
try FileManager.default.removeItem(atPath: outputPath.path())
} catch {
logger.error("remove file failed")
}
}
let dir = outputPath.deletingLastPathComponent().path()
logger.debug("Will try to create dir: \(dir)")
try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
var isDirectory = ObjCBool(false)
guard FileManager.default.fileExists(atPath: dir, isDirectory: &isDirectory), isDirectory.boolValue else {
logger.error("Could not create dir, or dir is a file")
fatalError()
}
guard let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHEVCHighestQualityWithAlpha) else {
logger.error("generate export failed")
fatalError()
}
exporter.outputURL = outputPath
exporter.outputFileType = .mov
exporter.shouldOptimizeForNetworkUse = false
exporter.videoComposition = videoComposition
await exporter.export()
logger.debug("Status: \(String(describing: exporter.status)), error: \(exporter.error)")
if exporter.status != .completed {
fatalError()
}
}
private func extractData(videoAsset: AVURLAsset) async throws -> AVMutableVideoComposition {
guard let videoTrack = try await videoAsset.loadTracks(withMediaType: .video).first else {
fatalError()
}
guard let audioTrack = try await videoAsset.loadTracks(withMediaType: .audio).first else {
fatalError()
}
let composition = AVMutableComposition(urlAssetInitializationOptions: nil)
guard let compositionVideoTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: videoTrack.trackID) else {
fatalError()
}
guard let compostiionAudioTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: audioTrack.trackID) else {
fatalError()
}
let duration = try await videoAsset.load(.duration)
try compositionVideoTrack.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: duration), of: videoTrack, at: CMTime.zero)
try compostiionAudioTrack.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: duration), of: audioTrack, at: CMTime.zero)
let naturalSize = try await videoTrack.load(.naturalSize)
let preferredTransform = try await videoTrack.load(.preferredTransform)
let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = CMTimeRange(start: CMTime.zero, end: duration)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
let videoComposition = AVMutableVideoComposition()
videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
mainInstruction.layerInstructions = [layerInstruction]
videoComposition.instructions = [mainInstruction]
// Increases the canvas
videoComposition.renderSize = .init(width: naturalSize.width + 500, height: naturalSize.height + 500)
return videoComposition
}
}
上传结果后
temp.mov
(请参阅控制台日志记录),显示黑边(来自https://rotato.app/tools/transparent-video):
如何确保背景填充透明颜色?
修改您的
VideoEditor
课程。这只是一种快速而肮脏的方法
import AVFoundation
class VideoEditor {
func export(url: URL, outputDir: URL) async {
let asset = AVURLAsset(url: url)
let videoComposition = try await createVideoComposition(asset: asset)
try! await exportVideo(outputPath: outputDir, asset: asset, videoComposition: videoComposition)
}
private func exportVideo(outputPath: URL, asset: AVAsset, videoComposition: AVVideoComposition) async throws {
// Same as before
// Configure export session with video composition
}
private func createVideoComposition(asset: AVAsset) async throws -> AVVideoComposition {
// Extract video track
guard let videoTrack = asset.tracks(withMediaType: .video).first else {
throw NSError(domain: "VideoEditor", code: 1, userInfo: [NSLocalizedDescriptionKey: "Video track not found"])
}
// Create video composition instruction
let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
// Create layer instruction for video track
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
// Configure transformation for video layer
let naturalSize = videoTrack.naturalSize
let transform = CGAffineTransform(translationX: 250, y: 250) // Translate to center of increased canvas
layerInstruction.setTransform(transform, at: .zero)
// Set up video composition
let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = CGSize(width: naturalSize.width + 500, height: naturalSize.height + 500)
videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
mainInstruction.layerInstructions = [layerInstruction]
videoComposition.instructions = [mainInstruction]
// Set background color to transparent
videoComposition.backgroundColor = UIColor.clear.cgColor
return videoComposition
}
}
然后显式设置
backgroundColor
的AVMutableVideoComposition to UIColor.clear.cgColor
属性,将视频的背景色设置为透明