// // LNUploadVideioView.swift // Gami // // Created by OneeChan on 2026/2/27. // import Foundation import UIKit import SnapKit import AVFoundation protocol LNVideoUploadViewDelegate: AnyObject { func onVideoUploadView(view: LNVideoUploadView, didUploadVideo url: String, imageUrl: String) func onVideoUploadViewUploadFailed(view: LNVideoUploadView) func onVideoUploadViewStartUpload(view: LNVideoUploadView) func onVideoUploadViewDidClickDelete(view: LNVideoUploadView) } extension LNVideoUploadViewDelegate { func onVideoUploadView(view: LNVideoUploadView, didUploadVideo url: String, imageUrl: String) {} func onVideoUploadViewUploadFailed(view: LNVideoUploadView) { } func onVideoUploadViewStartUpload(view: LNVideoUploadView) {} func onVideoUploadViewDidClickDelete(view: LNVideoUploadView) {} } class LNVideoUploadView: UIImageView { private var curTask: String? private(set) var sourceUrl: URL? private var cacheUrl: URL? private(set) var videoSize: CGSize? private(set) var videoUrl: String? private(set) var imageUrl: String? private let durationLabel = UILabel() private let playButton = UIButton() private let deleteButton = UIButton() weak var delegate: LNVideoUploadViewDelegate? override init(image: UIImage? = nil) { super.init(image: image) setupViews() } func preview(url: URL) -> CGSize { reset() guard FileManager.default.fileExists(atPath: url.path) else { return .zero } sourceUrl = url let asset = AVAsset(url: url) let videoDuration = CMTimeGetSeconds(asset.duration) durationLabel.text = videoDuration.timeCountDisplay DispatchQueue.global().async { [weak self] in guard let self else { return } guard sourceUrl == url else { return } // 创建缩略图生成器 let imageGenerator = AVAssetImageGenerator(asset: asset) // 设置生成的图片比例(保持原视频比例) imageGenerator.appliesPreferredTrackTransform = true // 获取视频首帧作为预览图 let time = CMTimeMakeWithSeconds(0, preferredTimescale: 600) do { // 生成指定时间点的图片 let cgImage = try imageGenerator.copyCGImage(at: time, actualTime: nil) let thumbnailImage = UIImage(cgImage: cgImage) // 更新UI(必须在主线程) runOnMain { self.image = thumbnailImage } } catch { print("生成视频缩略图失败:\(error.localizedDescription)") // 生成失败时显示占位图 runOnMain { self.image = UIImage(systemName: "film") } } } guard let videoTrack = asset.tracks(withMediaType: .video).first else { return .zero } // 3. 获取视频的自然尺寸(原始尺寸) let naturalSize = videoTrack.naturalSize // 4. 处理视频的旋转角度(避免尺寸方向错误) let transform = videoTrack.preferredTransform videoSize = naturalSize // 如果视频有旋转(90/270度),需要交换宽高 if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 { // 90度旋转 videoSize = CGSize(width: naturalSize.height, height: naturalSize.width) } else if transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 { // 270度旋转 videoSize = CGSize(width: naturalSize.height, height: naturalSize.width) } return videoSize! } func startUpload() { guard let sourceUrl, let image else { delegate?.onVideoUploadViewUploadFailed(view: self) return } delegate?.onVideoUploadViewStartUpload(view: self) if cacheUrl != nil { uploadVideo(image: image) } else { LNVideoCompressor.compressVideo(sourceURL: sourceUrl) { [weak self] url in guard let self else { return } cacheUrl = url uploadVideo(image: image) } } } func cancelUpload() { if let curTask { LNFileUploader.shared.cancelUpload(taskID: curTask) } } func reset() { cancelUpload() image = nil videoUrl = nil cacheUrl = nil imageUrl = nil sourceUrl = nil curTask = nil durationLabel.text = "" } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension LNVideoUploadView { private func uploadVideo(image: UIImage) { guard let cacheUrl, let data = try? Data(contentsOf: cacheUrl), let imageData = image.jpegData(compressionQuality: 0.7) else { delegate?.onVideoUploadViewUploadFailed(view: self) return } DispatchQueue.global().async { [weak self] in guard let self else { return } let group = DispatchGroup() if videoUrl == nil { group.enter() let suffix = "\(Int(videoSize?.width ?? 0))x\(Int(videoSize?.height ?? 0)).mp4" curTask = LNFileUploader.shared.startUpload( type: .video, fileData: data, suffix: suffix, progressHandler: nil) { [weak self] url, err in if let self { curTask = nil if let url { videoUrl = url } else if let err { showToast(err) } } group.leave() } } if imageUrl == nil { group.enter() let suffix = "\(Int(image.size.width))x\(Int(image.size.height)).jpeg" LNFileUploader.shared.startUpload( type: .other, fileData: imageData, suffix: suffix, progressHandler: nil) { [weak self] url, err in if let self { if let url { imageUrl = url } else if let err { showToast(err) } } group.leave() } } group.notify(queue: .main) { [weak self] in guard let self else { return } if let videoUrl, let imageUrl { delegate?.onVideoUploadView(view: self, didUploadVideo: videoUrl, imageUrl: imageUrl) } else { delegate?.onVideoUploadViewUploadFailed(view: self) } } } } } extension LNVideoUploadView { private func setupViews() { isUserInteractionEnabled = true contentMode = .scaleAspectFill layer.cornerRadius = 7 clipsToBounds = true let cover = UIView() cover.backgroundColor = .black.withAlphaComponent(0.2) addSubview(cover) cover.snp.makeConstraints { make in make.edges.equalToSuperview() } let clear = buildClearButton() addSubview(clear) clear.snp.makeConstraints { make in make.top.equalToSuperview().offset(6) make.trailing.equalToSuperview().offset(-6) make.width.height.equalTo(16) } durationLabel.font = .body_xs durationLabel.textColor = .text_1 addSubview(durationLabel) durationLabel.snp.makeConstraints { make in make.leading.equalToSuperview().offset(6) make.bottom.equalToSuperview().offset(-6) } playButton.setImage(.icVoicePlayWithBlackBg, for: .normal) playButton.isUserInteractionEnabled = false addSubview(playButton) playButton.snp.makeConstraints { make in make.center.equalToSuperview() } } private func buildClearButton() -> UIView { let config = UIImage.SymbolConfiguration(pointSize: 7) deleteButton.setImage(.init(systemName: "xmark", withConfiguration: config), for: .normal) deleteButton.tintColor = .white deleteButton.backgroundColor = .black.withAlphaComponent(0.5) deleteButton.layer.cornerRadius = 8 deleteButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } reset() delegate?.onVideoUploadViewDidClickDelete(view: self) }), for: .touchUpInside) return deleteButton } }