123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- //
- // TSVideoPlayerVC.swift
- // AIEmoji
- //
- // Created by 100Years on 2025/4/17.
- //
- import UIKit
- import AVKit
- import SnapKit
- class VideoProgressSlider: UISlider {
- // 增加触摸区域的范围(比可视区域大)
- private let touchAreaPadding: CGFloat = 20
-
- // 增加点击响应区域
- override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
- let width = self.bounds.size.width
- let tapPoint = touch.location(in: self)
- let tapValue = Float(tapPoint.x / width)
-
- self.setValue(tapValue, animated: true)
- self.sendActions(for: .valueChanged)
-
- return true
- }
-
- // 扩大触摸区域
- override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
- let bounds = self.bounds.insetBy(dx: -touchAreaPadding, dy: -touchAreaPadding)
- return bounds.contains(point)
- }
-
- // 扩大滑块的实际触摸区域(不影响视觉大小)
- override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
- let visualThumbSize: CGFloat = 10 // 视觉上的大小
- let touchThumbSize: CGFloat = 20 // 实际触摸区域大小
-
- let defaultRect = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)
-
- // 视觉上保持15x15大小
- let visualRect = CGRect(
- x: defaultRect.origin.x + (defaultRect.width - visualThumbSize)/2,
- y: defaultRect.origin.y + (defaultRect.height - visualThumbSize)/2,
- width: visualThumbSize,
- height: visualThumbSize
- )
-
- // 实际触摸区域更大(30x30)
- let touchRect = CGRect(
- x: defaultRect.origin.x + (defaultRect.width - touchThumbSize)/2,
- y: defaultRect.origin.y + (defaultRect.height - touchThumbSize)/2,
- width: touchThumbSize,
- height: touchThumbSize
- )
-
- // 保存实际触摸区域用于点击检测
- self.touchThumbRect = touchRect
-
- return visualRect
- }
-
- // 存储实际触摸区域
- private var touchThumbRect: CGRect = .zero
-
- // 轨道高度
- private let trackHeight: CGFloat = 3
-
- override func trackRect(forBounds bounds: CGRect) -> CGRect {
- var rect = super.trackRect(forBounds: bounds)
- rect.size.height = trackHeight
- return rect
- }
- }
- 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 344444444444implemented")
- }
-
- // 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)
- }
- }
|