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

Swift中旋转URL来源视频及AVAssetWriter写入后旋转视频的方法

嘿,我来帮你搞定这两个Swift视频旋转的问题,全部用当前最新的AVFoundation API,绝对不会用到废弃的Objective-C旧方法~

1. 如何在Swift中旋转来自URL的视频?

要旋转本地或网络URL的视频,我们可以借助AVFoundation的AVAsset、AVMutableVideoComposition和AVAssetExportSession实现,这是目前最稳定且通用的方案。具体步骤如下:

  • 第一步:加载目标视频资源
    先通过视频URL初始化AVAsset,这是我们处理视频的基础:

    let inputURL = URL(fileURLWithPath: "你的视频本地路径") // 网络URL直接传即可
    let asset = AVAsset(url: inputURL)
    
  • 第二步:配置视频旋转规则
    接下来要创建AVMutableVideoComposition来定义视频的渲染逻辑,核心是给视频轨道添加旋转的仿射变换。这里以顺时针旋转90度为例,还要注意旋转后视频的宽高会互换,得调整渲染尺寸:

    guard let videoTrack = asset.tracks(withMediaType: .video).first else {
        print("没找到视频轨道,无法处理")
        return
    }
    
    let videoComposition = AVMutableVideoComposition()
    // 旋转后宽高互换,所以renderSize要改成原视频的高×宽
    videoComposition.renderSize = CGSize(width: videoTrack.naturalSize.height, height: videoTrack.naturalSize.width)
    videoComposition.frameDuration = videoTrack.minFrameDuration
    
    // 创建渲染指令
    let instruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
    
    // 给视频轨道添加旋转变换
    let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
    // 顺时针转90度 + 平移修正,避免视频被裁剪
    let rotationTransform = CGAffineTransform(rotationAngle: .pi/2)
        .translatedBy(x: videoTrack.naturalSize.height, y: 0)
    layerInstruction.setTransform(rotationTransform, at: .zero)
    
    instruction.layerInstructions = [layerInstruction]
    videoComposition.instructions = [instruction]
    

    要是你需要其他旋转角度,直接替换rotationAngle的值就行:

    • 逆时针90度:-.pi/2,对应的平移是translatedBy(x: 0, y: videoTrack.naturalSize.width)
    • 180度:.pi,对应的平移是translatedBy(x: videoTrack.naturalSize.width, y: videoTrack.naturalSize.height)
  • 第三步:导出旋转后的视频
    最后用AVAssetExportSession把处理后的视频导出到指定路径:

    // 生成输出URL,比如放在Documents目录
    let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let outputURL = documentsDir.appendingPathComponent("rotated_video_\(Date().timeIntervalSince1970).mov")
    
    guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
        print("创建导出会话失败")
        return
    }
    exportSession.outputURL = outputURL
    exportSession.outputFileType = .mov // 也可以用.mp4,根据需求调整
    exportSession.videoComposition = videoComposition
    
    // 异步导出,避免阻塞主线程
    exportSession.exportAsynchronously {
        DispatchQueue.main.async {
            switch exportSession.status {
            case .completed:
                print("视频旋转完成!输出路径:\(outputURL)")
                // 这里可以做后续操作,比如展示给用户或保存到相册
            case .failed:
                print("导出失败:\(exportSession.error?.localizedDescription ?? "未知错误")")
            case .cancelled:
                print("导出被取消")
            default:
                break
            }
        }
    }
    
2. AVAssetWriter完成写入后旋转已生成视频的解决方案

你提到的在AVAssetWriter完成回调里旋转已生成视频的场景,完全可以复用上面的旋转逻辑,只需要把回调里的输出URL作为旋转的输入源就行。下面是适配你场景的完整代码示例:

假设你的AVAssetWriter完成回调是这样的,我们在里面加入旋转逻辑:

// 你的AVAssetWriter完成处理程序
assetWriter.finishWriting { [weak self] in
    guard let self = self else { return }
    guard assetWriter.status == .completed else {
        print("AVAssetWriter写入失败:\(assetWriter.error?.localizedDescription ?? "未知错误")")
        return
    }

    // 拿到写入完成后的视频URL
    let writtenVideoURL = assetWriter.outputURL!
    // 调用封装好的旋转方法
    self.rotateVideo(inputURL: writtenVideoURL) { success, rotatedURL in
        guard success, let url = rotatedURL else {
            print("视频旋转失败")
            return
        }
        // 旋转完成!这里可以创建播放器展示给用户,或者让用户保存
        print("旋转后的视频已准备好:\(url)")
        
        // 示例:用AVPlayerViewController展示视频
        let player = AVPlayer(url: url)
        let playerVC = AVPlayerViewController()
        playerVC.player = player
        self.present(playerVC, animated: true) {
            player.play()
        }
    }
}

// 封装的视频旋转方法,方便复用
private func rotateVideo(inputURL: URL, completion: @escaping (Bool, URL?) -> Void) {
    let asset = AVAsset(url: inputURL)
    guard let videoTrack = asset.tracks(withMediaType: .video).first else {
        completion(false, nil)
        return
    }

    let videoComposition = AVMutableVideoComposition()
    // 这里还是以顺时针旋转90度为例,你可以按需修改角度
    videoComposition.renderSize = CGSize(width: videoTrack.naturalSize.height, height: videoTrack.naturalSize.width)
    videoComposition.frameDuration = videoTrack.minFrameDuration

    let instruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)

    let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
    let rotationTransform = CGAffineTransform(rotationAngle: .pi/2)
        .translatedBy(x: videoTrack.naturalSize.height, y: 0)
    layerInstruction.setTransform(rotationTransform, at: .zero)

    instruction.layerInstructions = [layerInstruction]
    videoComposition.instructions = [instruction]

    // 生成输出URL
    let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let outputURL = documentsDir.appendingPathComponent("post_rotated_video_\(Date().timeIntervalSince1970).mov")

    guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
        completion(false, nil)
        return
    }
    exportSession.outputURL = outputURL
    exportSession.outputFileType = .mov
    exportSession.videoComposition = videoComposition

    exportSession.exportAsynchronously {
        DispatchQueue.main.async {
            switch exportSession.status {
            case .completed:
                completion(true, outputURL)
            case .failed, .cancelled:
                completion(false, nil)
            default:
                completion(false, nil)
            }
        }
    }
}

额外小贴士

  • 如果你追求速度,也可以只修改视频的旋转元数据(不修改像素内容),但这种方式依赖播放器对元数据的支持,有些第三方播放器可能不会自动应用旋转。如果要兼容所有场景,还是建议上面的视频合成方式。
  • 导出预设presetName可以选AVAssetExportPresetMediumQuality,导出速度更快,适合对画质要求不高的场景。
  • 记得处理权限:比如写入Documents目录没问题,但要保存到相册的话,需要申请PHPhotoLibrary的权限。

内容的提问来源于stack exchange,提问作者Chewie The Chorkie

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 12:17:27