LNVideoPlayerView.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. //
  2. // LNVideoPlayerView.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/3/2.
  6. //
  7. import Foundation
  8. import UIKit
  9. import AVKit
  10. import SnapKit
  11. import Combine
  12. protocol LNVideoPlayerViewDelegate: NSObject {
  13. func onVideoDidLoad(view: LNVideoPlayerView)
  14. func onVideoDidStart(view: LNVideoPlayerView)
  15. func onVideoDidStop(view: LNVideoPlayerView)
  16. func onVideoDidPause(view: LNVideoPlayerView)
  17. func onVideoMutedChanged(view: LNVideoPlayerView)
  18. func onVideoProgressChanged(view: LNVideoPlayerView, cur: Float64, total: Float64)
  19. }
  20. extension LNVideoPlayerViewDelegate {
  21. func onVideoDidLoad(view: LNVideoPlayerView) { }
  22. func onVideoDidStart(view: LNVideoPlayerView) { }
  23. func onVideoDidStop(view: LNVideoPlayerView) { }
  24. func onVideoDidPause(view: LNVideoPlayerView) { }
  25. func onVideoMutedChanged(view: LNVideoPlayerView) { }
  26. func onVideoProgressChanged(view: LNVideoPlayerView, cur: Float64, total: Float64) { }
  27. }
  28. class LNVideoPlayerView: UIView {
  29. static let updateInterval: Double = 0.05
  30. private let videoPlayer = AVPlayer()
  31. private let videoLayer = AVPlayerLayer()
  32. private let coverImageView = UIImageView()
  33. private let touchView = UIView()
  34. private let playButton = UIButton()
  35. private var timeObserver: Any?
  36. var playButtonSize: CGFloat = 42 {
  37. didSet {
  38. playButton.snp.updateConstraints { make in
  39. make.width.height.equalTo(playButtonSize)
  40. }
  41. }
  42. }
  43. var loop: Bool = false
  44. weak var delegate: LNVideoPlayerViewDelegate?
  45. private(set) var duration: TimeInterval = 0
  46. private(set) var videoSize: CGSize = .zero
  47. private(set) var curSource: String?
  48. var showCover: Bool = true {
  49. didSet {
  50. coverImageView.isHidden = !showCover
  51. }
  52. }
  53. var isPlaying: Bool {
  54. videoPlayer.rate > 0 && videoPlayer.error == nil && videoPlayer.currentItem?.status == .readyToPlay
  55. }
  56. var isMuted: Bool {
  57. videoPlayer.isMuted
  58. }
  59. override init(frame: CGRect) {
  60. super.init(frame: frame)
  61. setupViews()
  62. setupObservsers()
  63. }
  64. func loadVideo(_ url: String, coverUrl: String?) {
  65. if curSource == url { return }
  66. curSource = url
  67. stop()
  68. guard let videoUrl = URL(string: url) else {
  69. duration = 0
  70. videoSize = .zero
  71. coverImageView.image = UIImage(systemName: "film")
  72. videoPlayer.replaceCurrentItem(with: nil)
  73. return
  74. }
  75. DispatchQueue.global().async { [weak self] in
  76. guard let self else { return }
  77. guard curSource == url else { return }
  78. let asset = AVAsset(url: videoUrl)
  79. let videoDuration = CMTimeGetSeconds(asset.duration)
  80. duration = TimeInterval(videoDuration)
  81. if let coverUrl {
  82. coverImageView.sd_setImage(with: URL(string: coverUrl))
  83. } else {
  84. let imageGenerator = AVAssetImageGenerator(asset: asset)
  85. imageGenerator.appliesPreferredTrackTransform = true
  86. let time = CMTimeMakeWithSeconds(0, preferredTimescale: 600)
  87. do {
  88. let cgImage = try imageGenerator.copyCGImage(at: time, actualTime: nil)
  89. let thumbnailImage = UIImage(cgImage: cgImage)
  90. DispatchQueue.main.async {
  91. self.coverImageView.image = thumbnailImage
  92. }
  93. } catch {
  94. DispatchQueue.main.async {
  95. self.coverImageView.image = UIImage(systemName: "film")
  96. }
  97. }
  98. }
  99. videoPlayer.pause()
  100. let item = AVPlayerItem(url: videoUrl)
  101. videoPlayer.replaceCurrentItem(with: item)
  102. guard let videoTrack = asset.tracks(withMediaType: .video).first else {
  103. return
  104. }
  105. let naturalSize = videoTrack.naturalSize
  106. let transform = videoTrack.preferredTransform
  107. videoSize = naturalSize
  108. if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 {
  109. // 90度旋转
  110. videoSize = CGSize(width: naturalSize.height, height: naturalSize.width)
  111. } else if transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 {
  112. // 270度旋转
  113. videoSize = CGSize(width: naturalSize.height, height: naturalSize.width)
  114. }
  115. DispatchQueue.main.async { [weak self] in
  116. guard let self else { return }
  117. guard curSource == url else { return }
  118. delegate?.onVideoDidLoad(view: self)
  119. }
  120. }
  121. }
  122. func start() {
  123. videoPlayer.play()
  124. playButton.isHidden = true
  125. coverImageView.isHidden = true
  126. delegate?.onVideoDidStart(view: self)
  127. }
  128. func pause() {
  129. videoPlayer.pause()
  130. playButton.isHidden = false
  131. delegate?.onVideoDidPause(view: self)
  132. }
  133. func seekTo(_ to: Float, handler: ((Bool) -> Void)? = nil) {
  134. let time = CMTimeMakeWithSeconds(Float64(to), preferredTimescale: 1)
  135. videoPlayer.seek(to: time) { finish in
  136. handler?(finish)
  137. }
  138. }
  139. func stop() {
  140. videoPlayer.pause()
  141. videoPlayer.seek(to: .zero)
  142. coverImageView.isHidden = !showCover
  143. playButton.isHidden = false
  144. delegate?.onVideoDidStop(view: self)
  145. }
  146. func mute() {
  147. videoPlayer.isMuted = true
  148. delegate?.onVideoMutedChanged(view: self)
  149. }
  150. func unmute() {
  151. videoPlayer.isMuted = false
  152. delegate?.onVideoMutedChanged(view: self)
  153. }
  154. func setScaleMode(_ model: AVLayerVideoGravity) {
  155. videoLayer.videoGravity = model
  156. if model == .resizeAspect {
  157. coverImageView.contentMode = .scaleAspectFit
  158. } else {
  159. coverImageView.contentMode = .scaleAspectFill
  160. }
  161. }
  162. required init?(coder: NSCoder) {
  163. fatalError("init(coder:) has not been implemented")
  164. }
  165. }
  166. extension LNVideoPlayerView {
  167. private func setupViews() {
  168. backgroundColor = .black
  169. clipsToBounds = true
  170. videoLayer.player = videoPlayer
  171. videoLayer.videoGravity = .resizeAspect
  172. layer.addSublayer(videoLayer)
  173. publisher(for: \.bounds).removeDuplicates().sink { [weak self] newValue in
  174. guard let self else { return }
  175. videoLayer.frame = newValue
  176. }.store(in: &cancellables)
  177. touchView.onTap { [weak self] in
  178. guard let self else { return }
  179. if isPlaying {
  180. pause()
  181. } else {
  182. start()
  183. }
  184. }
  185. addSubview(touchView)
  186. touchView.snp.makeConstraints { make in
  187. make.edges.equalToSuperview()
  188. }
  189. coverImageView.contentMode = .scaleAspectFit
  190. addSubview(coverImageView)
  191. coverImageView.snp.makeConstraints { make in
  192. make.edges.equalToSuperview()
  193. }
  194. playButton.setBackgroundImage(.icVoicePlayWithBlackBg, for: .normal)
  195. playButton.addAction(UIAction(handler: { [weak self] _ in
  196. guard let self else { return }
  197. if isPlaying {
  198. pause()
  199. } else {
  200. start()
  201. }
  202. }), for: .touchUpInside)
  203. addSubview(playButton)
  204. playButton.snp.makeConstraints { make in
  205. make.center.equalToSuperview()
  206. make.width.height.equalTo(playButtonSize)
  207. }
  208. }
  209. private func setupObservsers() {
  210. let time = CMTime(seconds: Self.updateInterval, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
  211. timeObserver = videoPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main)
  212. { [weak self] currentTime in
  213. guard let self, let playerItem = videoPlayer.currentItem else { return }
  214. let currentSeconds = CMTimeGetSeconds(currentTime)
  215. let totalSeconds = CMTimeGetSeconds(playerItem.duration)
  216. guard !currentSeconds.isNaN, !totalSeconds.isNaN, totalSeconds > 0 else { return }
  217. if currentSeconds == totalSeconds {
  218. if loop {
  219. // 循环播放
  220. videoPlayer.pause()
  221. videoPlayer.seek(to: .zero)
  222. videoPlayer.play()
  223. } else {
  224. // 自动停止
  225. stop()
  226. }
  227. } else {
  228. delegate?.onVideoProgressChanged(view: self, cur: currentSeconds, total: totalSeconds)
  229. }
  230. }
  231. }
  232. }