You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift扩展视频画布后,如何实现背景透明导出?

解决AVMutableVideoComposition扩展画布后背景不透明的问题

核心问题分析

你当前的代码存在两个关键问题导致背景无法透明:

  1. 导出时使用的是原视频asset而非你创建的AVMutableComposition,导致画布扩展的设置没有被正确应用
  2. 未明确将AVMutableVideoComposition的背景设置为透明,且原视频轨道未调整到扩展画布的正确位置

解决方案步骤

1. 修正导出逻辑,使用自定义Composition

你的extractData方法创建了AVMutableComposition但未在导出时使用,需要将其与AVMutableVideoComposition一起返回,并用该Composition初始化导出会话。

2. 设置透明背景

给AVMutableVideoComposition明确设置透明背景色:

videoComposition.backgroundColor = NSColor.clear.cgColor

3. 调整原视频在扩展画布中的位置

通过AVMutableVideoCompositionLayerInstruction给原视频轨道添加平移变换,将其放置在扩展画布的合适位置(例如居中)。

修改后的完整代码

VideoEditor类修改

import AppKit
import AVFoundation
import Foundation
import OSLog

let logger = Logger()

class VideoEditor {
    func export(
        url: URL,
        outputDir: URL
    ) async {
        let asset = AVURLAsset(url: url)
        // 返回composition和videoComposition
        let (composition, videoComposition) = try! await extractData(videoAsset: asset)
    
        try! await exportVideo(outputPath: outputDir, composition: composition, videoComposition: videoComposition)
    }

    private func exportVideo(outputPath: URL, composition: 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()
        }

        // 使用自定义composition初始化导出会话
        guard let exporter = AVAssetExportSession(asset: composition, 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 -> (AVMutableComposition, 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: .video, preferredTrackID: videoTrack.trackID) else {
            fatalError()
        }
        guard let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: audioTrack.trackID) else {
            fatalError()
        }

        let duration = try await videoAsset.load(.duration)

        try compositionVideoTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: duration), of: videoTrack, at: .zero)
        try compositionAudioTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: duration), of: audioTrack, at: .zero)

        let naturalSize = try await videoTrack.load(.naturalSize)
        let preferredTransform = try await videoTrack.load(.preferredTransform)
        let mainInstruction = AVMutableVideoCompositionInstruction()

        mainInstruction.timeRange = CMTimeRange(start: .zero, end: duration)

        let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack)
        let videoComposition = AVMutableVideoComposition()
 
        videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
        // 设置透明背景
        videoComposition.backgroundColor = NSColor.clear.cgColor

        mainInstruction.layerInstructions = [layerInstruction]
        videoComposition.instructions = [mainInstruction]
        
        // 扩展画布尺寸
        let newRenderSize = CGSize(width: naturalSize.width + 500, height: naturalSize.height + 500)
        videoComposition.renderSize = newRenderSize

        // 计算偏移量,将原视频居中放置
        let offsetX = (newRenderSize.width - naturalSize.width) / 2
        let offsetY = (newRenderSize.height - naturalSize.height) / 2
        // 组合原变换与平移变换
        let finalTransform = preferredTransform.concatenating(CGAffineTransform(translationX: offsetX, y: offsetY))
        layerInstruction.setTransform(finalTransform, at: .zero)

        return (composition, videoComposition)
    }
}

关键说明

  • 必须使用自定义的AVMutableComposition初始化导出会话,否则videoComposition的画布扩展设置无法生效
  • 明确设置backgroundColor为NSColor.clear.cgColor,确保扩展区域背景透明
  • 通过变换将原视频轨道调整到扩展画布的目标位置(示例中为居中),避免原视频位置异常
  • 导出预设AVAssetExportPresetHEVCHighestQualityWithAlpha和输出格式.mov是支持Alpha通道的正确选择,无需修改

内容的提问来源于stack exchange,提问作者J. Doe

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 15:07:17