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

iOS Swift5 相册选取视频按30秒切片出现时长异常问题求助

iOS视频按固定30秒分割异常解决方案

核心问题点

  • 裁剪时长计算错误:你当前在cropVideo方法中构造CMTimeRange时,直接把endTime作为片段时长传入,正确的片段时长应该是endTime - startTime。当裁剪非首段视频时,起始时间大于0,就会出现片段时长远超过30秒的问题。
  • 多导出任务并发冲突:你的isVideoGettinGEdited变量是用来控制导出串行执行的,但你调用导出方法前没有将该变量设为true,导致多个导出任务同时执行,出现资源竞争、时长计算混乱的问题。
  • 文件路径构造不规范:你用URL(string:)构造本地文件路径、用absoluteString操作本地文件,不符合iOS文件路径规范,可能导致文件读写失败。

修改后的完整代码

import UIKit
import AVKit
import MobileCoreServices
import Photos
class ViewController: UIViewController { 
    @IBOutlet weak var videoView: UIImageView!
    @IBOutlet var imageView: UIImageView!
    var player: AVPlayer!
    var avpController = AVPlayerViewController()
    var isVideoGettinGEdited = false
    // 新增变量存储待导出的任务参数
    var exportQueue: [(url: URL, startTime: Double, endTime: Double, fileName: String)] = []
    
    @IBAction func didTapButton(){     
        let picker = UIImagePickerController()
        picker.delegate = self
        picker.sourceType = .savedPhotosAlbum
        picker.mediaTypes = [kUTTypeMovie as String]
        picker.allowsEditing = false
        present(picker, animated: true, completion: nil)
    }  
}
extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {  
        if let image = info[.editedImage] as? UIImage{
            imageView.image = image
        } else {
            guard
                let mediaType = info[.mediaType] as? String,
                mediaType == (kUTTypeMovie as String),
                let url = info[.mediaURL] as? URL
            else { return }
            let videoAsset = AVURLAsset(url: url)
            let durationTime = ceil(CMTimeGetSeconds(videoAsset.duration))
            exportQueue.removeAll()
            
            // 生成所有待导出的片段参数
            var currentStart = 0.0
            while currentStart < durationTime {
                let currentEnd = min(currentStart + 30, durationTime)
                let fileName = "Output-\(Int(currentStart/30)+1).mp4"
                exportQueue.append((url: url, startTime: currentStart, endTime: currentEnd, fileName: fileName))
                currentStart = currentEnd
            }
            
            // 开始串行导出
            startNextExport()
        }
        picker.dismiss(animated: true, completion: nil)
    }
    
    // 新增串行导出控制方法
    func startNextExport() {
        guard !exportQueue.isEmpty, !isVideoGettinGEdited else { return }
        isVideoGettinGEdited = true
        let task = exportQueue.removeFirst()
        cropVideo(atURL: task.url, startTime: task.startTime, endTime: task.endTime, fileName: task.fileName)
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }
}
extension ViewController {
    func cropVideo(atURL url:URL, startTime:Double, endTime:Double, fileName:String) {
        let asset = AVURLAsset(url: url)
        guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
            isVideoGettinGEdited = false
            startNextExport()
            return
        }
        // 修正本地路径构造方式
        let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
        var outputURL = URL(fileURLWithPath: docPath)
        let fileManager = FileManager.default
        do {
            try fileManager.createDirectory(at: outputURL, withIntermediateDirectories: true)
        } catch {
            print(error)
        }
        outputURL.appendPathComponent(fileName)
        // 修正文件删除逻辑
        if fileManager.fileExists(atPath: outputURL.path) {
            do {
                try fileManager.removeItem(at: outputURL)
            } catch {
                print(error)
            }
        }
        
        exportSession.outputURL = outputURL
        exportSession.shouldOptimizeForNetworkUse = true
        exportSession.outputFileType = .mp4
        // 修正时长计算:片段时长 = 结束时间 - 开始时间
        let start = CMTimeMakeWithSeconds(startTime, preferredTimescale: 600)
        let duration = CMTimeMakeWithSeconds(endTime - startTime, preferredTimescale: 600)
        let range = CMTimeRangeMake(start: start, duration: duration)
        exportSession.timeRange = range
        
        exportSession.exportAsynchronously {
            defer {
                self.isVideoGettinGEdited = false
                self.startNextExport()
            }
            switch exportSession.status {
            case .completed:
                PHPhotoLibrary.shared().performChanges({
                    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL)
                }) { completed, error in
                    DispatchQueue.main.async {
                        self.view.isUserInteractionEnabled = true
                        if completed {
                            print("\(fileName) 已保存到相册")
                        } else if let error = error {
                            print("\(fileName) 保存失败:\(error)")
                        }
                    }
                }
            case .failed:
                print("导出失败:\(exportSession.error?.localizedDescription ?? "未知错误")")
            default:
                break
            }
        }
    }
    
    //MARK:- saveVideoFromURL
    func saveVideoFromURL(_ videoURL:String) {
        self.view.isUserInteractionEnabled = false
        DispatchQueue.global(qos: .background).async {
            if let url = URL(string: videoURL),
                let urlData = NSData(contentsOf: url) {
                let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0];
                print("url path component = \(url.lastPathComponent)")
                let filePath="\(documentsPath)/\(url.lastPathComponent)"
                    urlData.write(toFile: filePath, atomically: true)
                    PHPhotoLibrary.shared().performChanges({
                        PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: filePath))
                    }) { completed, error in
                        DispatchQueue.main.async {
                            self.view.isUserInteractionEnabled = true
                            if completed {
                                print("Video has been saved to your photos.")
                            } else if let error = error {
                                print("Failed to save video:\(error)")
                            }
                        }
                    }
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者Nasser FN

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 19:57:02