TSAIListVideoPlayerVC.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. //
  2. // TSVideoPlayerVC.swift
  3. // AIEmoji
  4. //
  5. // Created by 100Years on 2025/4/17.
  6. //
  7. import UIKit
  8. import AVKit
  9. import SnapKit
  10. class VideoProgressSlider: UISlider {
  11. // 增加触摸区域的范围(比可视区域大)
  12. private let touchAreaPadding: CGFloat = 20
  13. // 增加点击响应区域
  14. override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
  15. let width = self.bounds.size.width
  16. let tapPoint = touch.location(in: self)
  17. let tapValue = Float(tapPoint.x / width)
  18. self.setValue(tapValue, animated: true)
  19. self.sendActions(for: .valueChanged)
  20. return true
  21. }
  22. // 扩大触摸区域
  23. override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
  24. let bounds = self.bounds.insetBy(dx: -touchAreaPadding, dy: -touchAreaPadding)
  25. return bounds.contains(point)
  26. }
  27. // 扩大滑块的实际触摸区域(不影响视觉大小)
  28. override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
  29. let visualThumbSize: CGFloat = 10 // 视觉上的大小
  30. let touchThumbSize: CGFloat = 20 // 实际触摸区域大小
  31. let defaultRect = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)
  32. // 视觉上保持15x15大小
  33. let visualRect = CGRect(
  34. x: defaultRect.origin.x + (defaultRect.width - visualThumbSize)/2,
  35. y: defaultRect.origin.y + (defaultRect.height - visualThumbSize)/2,
  36. width: visualThumbSize,
  37. height: visualThumbSize
  38. )
  39. // 实际触摸区域更大(30x30)
  40. let touchRect = CGRect(
  41. x: defaultRect.origin.x + (defaultRect.width - touchThumbSize)/2,
  42. y: defaultRect.origin.y + (defaultRect.height - touchThumbSize)/2,
  43. width: touchThumbSize,
  44. height: touchThumbSize
  45. )
  46. // 保存实际触摸区域用于点击检测
  47. self.touchThumbRect = touchRect
  48. return visualRect
  49. }
  50. // 存储实际触摸区域
  51. private var touchThumbRect: CGRect = .zero
  52. // 轨道高度
  53. private let trackHeight: CGFloat = 3
  54. override func trackRect(forBounds bounds: CGRect) -> CGRect {
  55. var rect = super.trackRect(forBounds: bounds)
  56. rect.size.height = trackHeight
  57. return rect
  58. }
  59. }
  60. class TSAIListVideoPlayerVC: UIViewController {
  61. // MARK: - Properties
  62. private var player: AVPlayer?
  63. private var playerLayer: AVPlayerLayer?
  64. private var timeObserverToken: Any?
  65. private var isPlaying = false
  66. private let videoURL: URL
  67. // MARK: - UI Components
  68. private lazy var playerContainerView: UIView = {
  69. let view = UIView()
  70. view.backgroundColor = .black
  71. return view
  72. }()
  73. private lazy var playPauseButton: UIButton = {
  74. let button = UIButton()
  75. button.setImage(UIImage(named: "play"), for: .normal)
  76. button.tintColor = .white
  77. // button.addTarget(self, action: #selector(playPauseTapped), for: .touchUpInside)
  78. button.isUserInteractionEnabled = false
  79. return button
  80. }()
  81. private lazy var progressSlider: UISlider = {
  82. let slider = UISlider()
  83. slider.minimumTrackTintColor = UIColor.themeColor
  84. slider.maximumTrackTintColor = .white.withAlphaComponent(0.2)
  85. // slider.thumbTintColor = UIColor.white
  86. // slider.setMinimumTrackImage(UIImage(color: UIColor.themeColor, size: CGSize(width: 1, height: 3)), for: .normal)
  87. // slider.setMaximumTrackImage(UIImage(color: .white.withAlphaComponent(0.2), size: CGSize(width: 1, height: 3)), for: .normal)
  88. slider.setThumbImage(UIImage.circle(diameter: 10, color: .white), for: .normal)
  89. slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
  90. slider.addTarget(self, action: #selector(sliderTouchEnded(_:)), for: .touchUpInside)
  91. slider.addTarget(self, action: #selector(sliderTouchEnded(_:)), for: .touchUpOutside)
  92. return slider
  93. }()
  94. private lazy var currentTimeLabel: UILabel = {
  95. let label = UILabel.createLabel(text: "00:00",font:.font(size: 12),textColor: .white)
  96. return label
  97. }()
  98. private lazy var durationLabel: UILabel = {
  99. let label = UILabel.createLabel(text: "00:00",font:.font(size: 12),textColor: .white)
  100. return label
  101. }()
  102. private lazy var controlsContainerView: UIView = {
  103. let view = UIView()
  104. // view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
  105. // view.layer.cornerRadius = 8
  106. return view
  107. }()
  108. // MARK: - Initialization
  109. init(videoURL: URL) {
  110. self.videoURL = videoURL
  111. super.init(nibName: nil, bundle: nil)
  112. }
  113. required init?(coder: NSCoder) {
  114. fatalError("init(coder:) has not been 344444444444implemented")
  115. }
  116. // MARK: - Lifecycle
  117. override func viewDidLoad() {
  118. super.viewDidLoad()
  119. setupUI()
  120. setupPlayer()
  121. }
  122. override func viewDidLayoutSubviews() {
  123. super.viewDidLayoutSubviews()
  124. playerLayer?.frame = playerContainerView.bounds
  125. }
  126. deinit {
  127. removePeriodicTimeObserver()
  128. }
  129. // MARK: - Setup
  130. private func setupUI() {
  131. view.backgroundColor = .black
  132. playerContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(playPauseTapped)))
  133. view.addSubview(playerContainerView)
  134. playerContainerView.addSubview(playPauseButton)
  135. playerContainerView.snp.makeConstraints { make in
  136. make.edges.equalToSuperview()
  137. }
  138. playPauseButton.snp.makeConstraints { make in
  139. make.centerX.equalToSuperview()
  140. make.centerY.equalToSuperview()//.offset(-50)
  141. make.width.height.equalTo(56)
  142. }
  143. playerContainerView.addSubview(controlsContainerView)
  144. controlsContainerView.snp.makeConstraints { make in
  145. make.leading.trailing.equalTo(0)
  146. make.bottom.equalTo(-80-k_Height_safeAreaInsetsBottom())
  147. }
  148. controlsContainerView.addSubview(progressSlider)
  149. controlsContainerView.addSubview(currentTimeLabel)
  150. controlsContainerView.addSubview(durationLabel)
  151. progressSlider.snp.makeConstraints { make in
  152. make.leading.equalTo(16)
  153. make.trailing.equalTo(-16)
  154. make.top.equalTo(0)
  155. make.height.equalTo(10)
  156. }
  157. let label = UILabel.createLabel(text: "/",font: .font(size: 11),textColor: .white)
  158. controlsContainerView.addSubview(label)
  159. label.snp.makeConstraints { make in
  160. make.top.equalTo(progressSlider.snp.bottom).offset(6)
  161. make.centerX.equalToSuperview()
  162. make.height.equalTo(13)
  163. make.bottom.equalToSuperview()
  164. }
  165. currentTimeLabel.snp.makeConstraints { make in
  166. make.height.equalTo(13)
  167. make.centerY.equalTo(label)
  168. make.trailing.equalTo(label.snp.leading)
  169. }
  170. durationLabel.snp.makeConstraints { make in
  171. make.height.equalTo(13)
  172. make.centerY.equalTo(label)
  173. make.leading.equalTo(label.snp.trailing)
  174. }
  175. }
  176. private func setupPlayer() {
  177. player = AVPlayer(url: videoURL)
  178. playerLayer = AVPlayerLayer(player: player)
  179. playerLayer?.videoGravity = .resizeAspect
  180. if let playerLayer = playerLayer {
  181. playerContainerView.layer.insertSublayer(playerLayer, at: 0)
  182. }
  183. // Add time observer to update progress
  184. addPeriodicTimeObserver()
  185. // Observe when the video ends
  186. NotificationCenter.default.addObserver(
  187. self,
  188. selector: #selector(playerDidFinishPlaying),
  189. name: .AVPlayerItemDidPlayToEndTime,
  190. object: player?.currentItem
  191. )
  192. // Get video duration
  193. let duration = player?.currentItem?.asset.duration.seconds ?? 0
  194. durationLabel.text = formatTime(seconds: Float(duration))
  195. }
  196. func setControlsBottom(bottem:CGFloat){
  197. controlsContainerView.snp.updateConstraints { make in
  198. make.bottom.equalTo(bottem)
  199. }
  200. }
  201. // MARK: - Player Controls
  202. @objc private func playPauseTapped() {
  203. if isPlaying {
  204. playPause()
  205. } else {
  206. playPlay()
  207. }
  208. }
  209. @objc private func playPlay() {
  210. player?.play()
  211. playPauseButton.isHidden = true
  212. // playPauseButton.setImage(UIImage(named: "pause"), for: .normal)
  213. isPlaying = true
  214. }
  215. @objc func playPause() {
  216. player?.pause()
  217. playPauseButton.isHidden = false
  218. // playPauseButton.setImage(UIImage(named: "play"), for: .normal)
  219. isPlaying = false
  220. }
  221. @objc private func playerDidFinishPlaying() {
  222. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1){
  223. self.playerDidFinish()
  224. }
  225. }
  226. func playerDidFinish() {
  227. player?.seek(to: CMTime.zero)
  228. playPauseButton.isHidden = false
  229. // playPauseButton.setImage(UIImage(named: "play"), for: .normal)
  230. isPlaying = false
  231. progressSlider.value = 0
  232. currentTimeLabel.text = "00:00"
  233. }
  234. // MARK: - Progress Slider
  235. @objc private func sliderValueChanged(_ sender: UISlider) {
  236. playPause()
  237. guard let duration = player?.currentItem?.duration else { return }
  238. let totalSeconds = CMTimeGetSeconds(duration)
  239. let value = Float64(sender.value) * totalSeconds
  240. let seekTime = CMTime(value: Int64(value), timescale: 1)
  241. currentTimeLabel.text = formatTime(seconds: Float(value))
  242. }
  243. @objc private func sliderTouchEnded(_ sender: UISlider) {
  244. guard let duration = player?.currentItem?.duration else { return }
  245. let totalSeconds = CMTimeGetSeconds(duration)
  246. let value = Float64(sender.value) * totalSeconds
  247. let seekTime = CMTime(value: Int64(value), timescale: 1)
  248. player?.seek(to: seekTime, toleranceBefore: .zero, toleranceAfter: .zero)
  249. playPlay()
  250. }
  251. // MARK: - Time Observer
  252. private func addPeriodicTimeObserver() {
  253. let interval = CMTime(seconds: 0.05, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
  254. timeObserverToken = player?.addPeriodicTimeObserver(
  255. forInterval: interval,
  256. queue: .main
  257. ) { [weak self] time in
  258. guard let self = self else { return }
  259. let timeElapsed = Float(time.seconds)
  260. if let duration = self.player?.currentItem?.duration {
  261. let durationSeconds = Float(CMTimeGetSeconds(duration))
  262. self.progressSlider.value = Float(timeElapsed / durationSeconds)
  263. self.currentTimeLabel.text = self.formatTime(seconds: timeElapsed)
  264. }
  265. }
  266. }
  267. private func removePeriodicTimeObserver() {
  268. if let token = timeObserverToken {
  269. player?.removeTimeObserver(token)
  270. timeObserverToken = nil
  271. }
  272. }
  273. // MARK: - Helper Methods
  274. private func formatTime(seconds: Float) -> String {
  275. let minutes = Int(seconds) / 60
  276. let seconds = Int(seconds) % 60
  277. return String(format: "%02d:%02d", minutes, seconds)
  278. }
  279. }