LNVideoUploadView.swift 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. //
  2. // LNUploadVideioView.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/2/27.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. import AVFoundation
  11. protocol LNVideoUploadViewDelegate: AnyObject {
  12. func onVideoUploadView(view: LNVideoUploadView, didUploadVideo url: String, imageUrl: String)
  13. func onVideoUploadViewUploadFailed(view: LNVideoUploadView)
  14. func onVideoUploadViewStartUpload(view: LNVideoUploadView)
  15. func onVideoUploadViewDidClickDelete(view: LNVideoUploadView)
  16. }
  17. extension LNVideoUploadViewDelegate {
  18. func onVideoUploadView(view: LNVideoUploadView, didUploadVideo url: String, imageUrl: String) {}
  19. func onVideoUploadViewUploadFailed(view: LNVideoUploadView) { }
  20. func onVideoUploadViewStartUpload(view: LNVideoUploadView) {}
  21. func onVideoUploadViewDidClickDelete(view: LNVideoUploadView) {}
  22. }
  23. class LNVideoUploadView: UIImageView {
  24. private var curTask: String?
  25. private(set) var sourceUrl: URL?
  26. private var cacheUrl: URL?
  27. private(set) var videoSize: CGSize?
  28. private(set) var videoUrl: String?
  29. private(set) var imageUrl: String?
  30. private let durationLabel = UILabel()
  31. private let playButton = UIButton()
  32. private let deleteButton = UIButton()
  33. weak var delegate: LNVideoUploadViewDelegate?
  34. override init(image: UIImage? = nil) {
  35. super.init(image: image)
  36. setupViews()
  37. }
  38. func preview(url: URL) -> CGSize {
  39. reset()
  40. guard FileManager.default.fileExists(atPath: url.path) else {
  41. return .zero
  42. }
  43. sourceUrl = url
  44. let asset = AVAsset(url: url)
  45. let videoDuration = CMTimeGetSeconds(asset.duration)
  46. durationLabel.text = videoDuration.timeCountDisplay
  47. DispatchQueue.global().async { [weak self] in
  48. guard let self else { return }
  49. guard sourceUrl == url else { return }
  50. // 创建缩略图生成器
  51. let imageGenerator = AVAssetImageGenerator(asset: asset)
  52. // 设置生成的图片比例(保持原视频比例)
  53. imageGenerator.appliesPreferredTrackTransform = true
  54. // 获取视频首帧作为预览图
  55. let time = CMTimeMakeWithSeconds(0, preferredTimescale: 600)
  56. do {
  57. // 生成指定时间点的图片
  58. let cgImage = try imageGenerator.copyCGImage(at: time, actualTime: nil)
  59. let thumbnailImage = UIImage(cgImage: cgImage)
  60. // 更新UI(必须在主线程)
  61. runOnMain {
  62. self.image = thumbnailImage
  63. }
  64. } catch {
  65. print("生成视频缩略图失败:\(error.localizedDescription)")
  66. // 生成失败时显示占位图
  67. runOnMain {
  68. self.image = UIImage(systemName: "film")
  69. }
  70. }
  71. }
  72. guard let videoTrack = asset.tracks(withMediaType: .video).first else {
  73. return .zero
  74. }
  75. // 3. 获取视频的自然尺寸(原始尺寸)
  76. let naturalSize = videoTrack.naturalSize
  77. // 4. 处理视频的旋转角度(避免尺寸方向错误)
  78. let transform = videoTrack.preferredTransform
  79. videoSize = naturalSize
  80. // 如果视频有旋转(90/270度),需要交换宽高
  81. if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 {
  82. // 90度旋转
  83. videoSize = CGSize(width: naturalSize.height, height: naturalSize.width)
  84. } else if transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 {
  85. // 270度旋转
  86. videoSize = CGSize(width: naturalSize.height, height: naturalSize.width)
  87. }
  88. return videoSize!
  89. }
  90. func startUpload() {
  91. guard let sourceUrl, let image else {
  92. delegate?.onVideoUploadViewUploadFailed(view: self)
  93. return
  94. }
  95. delegate?.onVideoUploadViewStartUpload(view: self)
  96. if cacheUrl != nil {
  97. uploadVideo(image: image)
  98. } else {
  99. LNVideoCompressor.compressVideo(sourceURL: sourceUrl) { [weak self] url in
  100. guard let self else {
  101. return
  102. }
  103. cacheUrl = url
  104. uploadVideo(image: image)
  105. }
  106. }
  107. }
  108. func cancelUpload() {
  109. if let curTask {
  110. LNFileUploader.shared.cancelUpload(taskID: curTask)
  111. }
  112. }
  113. func reset() {
  114. cancelUpload()
  115. image = nil
  116. videoUrl = nil
  117. cacheUrl = nil
  118. imageUrl = nil
  119. sourceUrl = nil
  120. curTask = nil
  121. durationLabel.text = ""
  122. }
  123. required init?(coder: NSCoder) {
  124. fatalError("init(coder:) has not been implemented")
  125. }
  126. }
  127. extension LNVideoUploadView {
  128. private func uploadVideo(image: UIImage) {
  129. guard let cacheUrl,
  130. let data = try? Data(contentsOf: cacheUrl),
  131. let imageData = image.jpegData(compressionQuality: 0.7)
  132. else {
  133. delegate?.onVideoUploadViewUploadFailed(view: self)
  134. return
  135. }
  136. DispatchQueue.global().async { [weak self] in
  137. guard let self else { return }
  138. let group = DispatchGroup()
  139. if videoUrl == nil {
  140. group.enter()
  141. let suffix = "\(Int(videoSize?.width ?? 0))x\(Int(videoSize?.height ?? 0)).mp4"
  142. curTask = LNFileUploader.shared.startUpload(
  143. type: .video, fileData: data,
  144. suffix: suffix, progressHandler: nil)
  145. { [weak self] url, err in
  146. if let self {
  147. curTask = nil
  148. if let url {
  149. videoUrl = url
  150. } else if let err {
  151. showToast(err)
  152. }
  153. }
  154. group.leave()
  155. }
  156. }
  157. if imageUrl == nil {
  158. group.enter()
  159. let suffix = "\(Int(image.size.width))x\(Int(image.size.height)).jpeg"
  160. LNFileUploader.shared.startUpload(
  161. type: .other, fileData: imageData,
  162. suffix: suffix, progressHandler: nil)
  163. { [weak self] url, err in
  164. if let self {
  165. if let url {
  166. imageUrl = url
  167. } else if let err {
  168. showToast(err)
  169. }
  170. }
  171. group.leave()
  172. }
  173. }
  174. group.notify(queue: .main) { [weak self] in
  175. guard let self else { return }
  176. if let videoUrl, let imageUrl {
  177. delegate?.onVideoUploadView(view: self, didUploadVideo: videoUrl, imageUrl: imageUrl)
  178. } else {
  179. delegate?.onVideoUploadViewUploadFailed(view: self)
  180. }
  181. }
  182. }
  183. }
  184. }
  185. extension LNVideoUploadView {
  186. private func setupViews() {
  187. isUserInteractionEnabled = true
  188. contentMode = .scaleAspectFill
  189. layer.cornerRadius = 7
  190. clipsToBounds = true
  191. let cover = UIView()
  192. cover.backgroundColor = .black.withAlphaComponent(0.2)
  193. addSubview(cover)
  194. cover.snp.makeConstraints { make in
  195. make.edges.equalToSuperview()
  196. }
  197. let clear = buildClearButton()
  198. addSubview(clear)
  199. clear.snp.makeConstraints { make in
  200. make.top.equalToSuperview().offset(6)
  201. make.trailing.equalToSuperview().offset(-6)
  202. make.width.height.equalTo(16)
  203. }
  204. durationLabel.font = .body_xs
  205. durationLabel.textColor = .text_1
  206. addSubview(durationLabel)
  207. durationLabel.snp.makeConstraints { make in
  208. make.leading.equalToSuperview().offset(6)
  209. make.bottom.equalToSuperview().offset(-6)
  210. }
  211. playButton.setImage(.icVoicePlayWithBlackBg, for: .normal)
  212. playButton.isUserInteractionEnabled = false
  213. addSubview(playButton)
  214. playButton.snp.makeConstraints { make in
  215. make.center.equalToSuperview()
  216. }
  217. }
  218. private func buildClearButton() -> UIView {
  219. let config = UIImage.SymbolConfiguration(pointSize: 7)
  220. deleteButton.setImage(.init(systemName: "xmark", withConfiguration: config), for: .normal)
  221. deleteButton.tintColor = .white
  222. deleteButton.backgroundColor = .black.withAlphaComponent(0.5)
  223. deleteButton.layer.cornerRadius = 8
  224. deleteButton.addAction(UIAction(handler: { [weak self] _ in
  225. guard let self else { return }
  226. reset()
  227. delegate?.onVideoUploadViewDidClickDelete(view: self)
  228. }), for: .touchUpInside)
  229. return deleteButton
  230. }
  231. }