|
@@ -0,0 +1,272 @@
|
|
|
+//
|
|
|
+// TSVideoPlayerVC.swift
|
|
|
+// AIEmoji
|
|
|
+//
|
|
|
+// Created by 100Years on 2025/4/17.
|
|
|
+//
|
|
|
+
|
|
|
+
|
|
|
+import UIKit
|
|
|
+import AVKit
|
|
|
+import SnapKit
|
|
|
+
|
|
|
+class TSAIListVideoPlayerVC: UIViewController {
|
|
|
+
|
|
|
+ // MARK: - Properties
|
|
|
+ private var player: AVPlayer?
|
|
|
+ private var playerLayer: AVPlayerLayer?
|
|
|
+ private var timeObserverToken: Any?
|
|
|
+ private var isPlaying = false
|
|
|
+
|
|
|
+ private let videoURL: URL
|
|
|
+
|
|
|
+ // MARK: - UI Components
|
|
|
+ private lazy var playerContainerView: UIView = {
|
|
|
+ let view = UIView()
|
|
|
+ view.backgroundColor = .black
|
|
|
+ return view
|
|
|
+ }()
|
|
|
+
|
|
|
+ private lazy var playPauseButton: UIButton = {
|
|
|
+ let button = UIButton()
|
|
|
+ button.setImage(UIImage(named: "play"), for: .normal)
|
|
|
+ button.tintColor = .white
|
|
|
+// button.addTarget(self, action: #selector(playPauseTapped), for: .touchUpInside)
|
|
|
+ button.isUserInteractionEnabled = false
|
|
|
+ return button
|
|
|
+ }()
|
|
|
+
|
|
|
+ private lazy var progressSlider: UISlider = {
|
|
|
+ let slider = UISlider()
|
|
|
+ slider.minimumTrackTintColor = UIColor.themeColor
|
|
|
+ slider.maximumTrackTintColor = .white.withAlphaComponent(0.2)
|
|
|
+// slider.thumbTintColor = UIColor.white
|
|
|
+// slider.setMinimumTrackImage(UIImage(color: UIColor.themeColor, size: CGSize(width: 1, height: 3)), for: .normal)
|
|
|
+// slider.setMaximumTrackImage(UIImage(color: .white.withAlphaComponent(0.2), size: CGSize(width: 1, height: 3)), for: .normal)
|
|
|
+ slider.setThumbImage(UIImage.circle(diameter: 10, color: .white), for: .normal)
|
|
|
+
|
|
|
+ slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
|
|
|
+ slider.addTarget(self, action: #selector(sliderTouchEnded(_:)), for: .touchUpInside)
|
|
|
+ slider.addTarget(self, action: #selector(sliderTouchEnded(_:)), for: .touchUpOutside)
|
|
|
+ return slider
|
|
|
+ }()
|
|
|
+
|
|
|
+ private lazy var currentTimeLabel: UILabel = {
|
|
|
+ let label = UILabel.createLabel(text: "00:00",font:.font(size: 12),textColor: .white)
|
|
|
+ return label
|
|
|
+ }()
|
|
|
+
|
|
|
+ private lazy var durationLabel: UILabel = {
|
|
|
+ let label = UILabel.createLabel(text: "00:00",font:.font(size: 12),textColor: .white)
|
|
|
+ return label
|
|
|
+ }()
|
|
|
+
|
|
|
+ private lazy var controlsContainerView: UIView = {
|
|
|
+ let view = UIView()
|
|
|
+// view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
|
|
+// view.layer.cornerRadius = 8
|
|
|
+ return view
|
|
|
+ }()
|
|
|
+
|
|
|
+ // MARK: - Initialization
|
|
|
+ init(videoURL: URL) {
|
|
|
+ self.videoURL = videoURL
|
|
|
+ super.init(nibName: nil, bundle: nil)
|
|
|
+ }
|
|
|
+
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Lifecycle
|
|
|
+ override func viewDidLoad() {
|
|
|
+ super.viewDidLoad()
|
|
|
+ setupUI()
|
|
|
+ setupPlayer()
|
|
|
+ }
|
|
|
+
|
|
|
+ override func viewDidLayoutSubviews() {
|
|
|
+ super.viewDidLayoutSubviews()
|
|
|
+ playerLayer?.frame = playerContainerView.bounds
|
|
|
+ }
|
|
|
+
|
|
|
+ deinit {
|
|
|
+ removePeriodicTimeObserver()
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Setup
|
|
|
+ private func setupUI() {
|
|
|
+ view.backgroundColor = .black
|
|
|
+ playerContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(playPauseTapped)))
|
|
|
+ view.addSubview(playerContainerView)
|
|
|
+ playerContainerView.addSubview(playPauseButton)
|
|
|
+
|
|
|
+ playerContainerView.snp.makeConstraints { make in
|
|
|
+ make.edges.equalToSuperview()
|
|
|
+ }
|
|
|
+
|
|
|
+ playPauseButton.snp.makeConstraints { make in
|
|
|
+ make.centerX.equalToSuperview()
|
|
|
+ make.centerY.equalToSuperview()//.offset(-50)
|
|
|
+ make.width.height.equalTo(56)
|
|
|
+ }
|
|
|
+
|
|
|
+ playerContainerView.addSubview(controlsContainerView)
|
|
|
+ controlsContainerView.snp.makeConstraints { make in
|
|
|
+ make.leading.trailing.equalTo(0)
|
|
|
+ make.bottom.equalTo(-80-k_Height_safeAreaInsetsBottom())
|
|
|
+ }
|
|
|
+
|
|
|
+ controlsContainerView.addSubview(progressSlider)
|
|
|
+ controlsContainerView.addSubview(currentTimeLabel)
|
|
|
+ controlsContainerView.addSubview(durationLabel)
|
|
|
+
|
|
|
+ progressSlider.snp.makeConstraints { make in
|
|
|
+ make.leading.equalTo(16)
|
|
|
+ make.trailing.equalTo(-16)
|
|
|
+ make.top.equalTo(0)
|
|
|
+ make.height.equalTo(10)
|
|
|
+ }
|
|
|
+
|
|
|
+ let label = UILabel.createLabel(text: "/",font: .font(size: 11),textColor: .white)
|
|
|
+ controlsContainerView.addSubview(label)
|
|
|
+ label.snp.makeConstraints { make in
|
|
|
+ make.top.equalTo(progressSlider.snp.bottom).offset(6)
|
|
|
+ make.centerX.equalToSuperview()
|
|
|
+ make.height.equalTo(13)
|
|
|
+ make.bottom.equalToSuperview()
|
|
|
+ }
|
|
|
+
|
|
|
+ currentTimeLabel.snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(13)
|
|
|
+ make.centerY.equalTo(label)
|
|
|
+ make.trailing.equalTo(label.snp.leading)
|
|
|
+ }
|
|
|
+
|
|
|
+ durationLabel.snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(13)
|
|
|
+ make.centerY.equalTo(label)
|
|
|
+ make.leading.equalTo(label.snp.trailing)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func setupPlayer() {
|
|
|
+ player = AVPlayer(url: videoURL)
|
|
|
+ playerLayer = AVPlayerLayer(player: player)
|
|
|
+ playerLayer?.videoGravity = .resizeAspect
|
|
|
+
|
|
|
+ if let playerLayer = playerLayer {
|
|
|
+ playerContainerView.layer.insertSublayer(playerLayer, at: 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add time observer to update progress
|
|
|
+ addPeriodicTimeObserver()
|
|
|
+
|
|
|
+ // Observe when the video ends
|
|
|
+ NotificationCenter.default.addObserver(
|
|
|
+ self,
|
|
|
+ selector: #selector(playerDidFinishPlaying),
|
|
|
+ name: .AVPlayerItemDidPlayToEndTime,
|
|
|
+ object: player?.currentItem
|
|
|
+ )
|
|
|
+
|
|
|
+ // Get video duration
|
|
|
+ let duration = player?.currentItem?.asset.duration.seconds ?? 0
|
|
|
+ durationLabel.text = formatTime(seconds: Float(duration))
|
|
|
+ }
|
|
|
+
|
|
|
+ func setControlsBottom(bottem:CGFloat){
|
|
|
+ controlsContainerView.snp.updateConstraints { make in
|
|
|
+ make.bottom.equalTo(bottem)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Player Controls
|
|
|
+ @objc private func playPauseTapped() {
|
|
|
+ if isPlaying {
|
|
|
+ playPause()
|
|
|
+ } else {
|
|
|
+ playPlay()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func playPlay() {
|
|
|
+ player?.play()
|
|
|
+ playPauseButton.isHidden = true
|
|
|
+// playPauseButton.setImage(UIImage(named: "pause"), for: .normal)
|
|
|
+ isPlaying = true
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc func playPause() {
|
|
|
+ player?.pause()
|
|
|
+ playPauseButton.isHidden = false
|
|
|
+// playPauseButton.setImage(UIImage(named: "play"), for: .normal)
|
|
|
+ isPlaying = false
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func playerDidFinishPlaying() {
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1){
|
|
|
+ self.playerDidFinish()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func playerDidFinish() {
|
|
|
+ player?.seek(to: CMTime.zero)
|
|
|
+ playPauseButton.isHidden = false
|
|
|
+// playPauseButton.setImage(UIImage(named: "play"), for: .normal)
|
|
|
+ isPlaying = false
|
|
|
+ progressSlider.value = 0
|
|
|
+ currentTimeLabel.text = "00:00"
|
|
|
+ }
|
|
|
+ // MARK: - Progress Slider
|
|
|
+ @objc private func sliderValueChanged(_ sender: UISlider) {
|
|
|
+ playPause()
|
|
|
+ guard let duration = player?.currentItem?.duration else { return }
|
|
|
+ let totalSeconds = CMTimeGetSeconds(duration)
|
|
|
+ let value = Float64(sender.value) * totalSeconds
|
|
|
+ let seekTime = CMTime(value: Int64(value), timescale: 1)
|
|
|
+ currentTimeLabel.text = formatTime(seconds: Float(value))
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func sliderTouchEnded(_ sender: UISlider) {
|
|
|
+ guard let duration = player?.currentItem?.duration else { return }
|
|
|
+ let totalSeconds = CMTimeGetSeconds(duration)
|
|
|
+ let value = Float64(sender.value) * totalSeconds
|
|
|
+ let seekTime = CMTime(value: Int64(value), timescale: 1)
|
|
|
+ player?.seek(to: seekTime, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
|
+ playPlay()
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Time Observer
|
|
|
+ private func addPeriodicTimeObserver() {
|
|
|
+ let interval = CMTime(seconds: 0.05, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
|
+
|
|
|
+ timeObserverToken = player?.addPeriodicTimeObserver(
|
|
|
+ forInterval: interval,
|
|
|
+ queue: .main
|
|
|
+ ) { [weak self] time in
|
|
|
+ guard let self = self else { return }
|
|
|
+ let timeElapsed = Float(time.seconds)
|
|
|
+
|
|
|
+ if let duration = self.player?.currentItem?.duration {
|
|
|
+ let durationSeconds = Float(CMTimeGetSeconds(duration))
|
|
|
+ self.progressSlider.value = Float(timeElapsed / durationSeconds)
|
|
|
+ self.currentTimeLabel.text = self.formatTime(seconds: timeElapsed)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func removePeriodicTimeObserver() {
|
|
|
+ if let token = timeObserverToken {
|
|
|
+ player?.removeTimeObserver(token)
|
|
|
+ timeObserverToken = nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Helper Methods
|
|
|
+ private func formatTime(seconds: Float) -> String {
|
|
|
+ let minutes = Int(seconds) / 60
|
|
|
+ let seconds = Int(seconds) % 60
|
|
|
+ return String(format: "%02d:%02d", minutes, seconds)
|
|
|
+ }
|
|
|
+}
|