using System; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; using UnityEngine.Video; namespace Unity.VRTemplate { /// /// Связывает UI-слайдер с VideoPlayer: показывает прогресс, позволяет перематывать /// видео перетаскиванием ползунка и переключать воспроизведение/паузу кнопкой. /// [RequireComponent(typeof(VideoPlayer))] public class CustomVideoTimeScrubControl : MonoBehaviour { /// Состояние видео при старте. public enum StartPlaybackState { Playing, // проигрывание Stopped, // стоп (пауза на первом кадре) } [Header("UI")] [SerializeField, Tooltip("Слайдер, управляющий видео.")] Slider m_Slider; [SerializeField, Tooltip("Объект кнопки воспроизведения/паузы.")] GameObject m_ButtonPlayOrPause; [SerializeField, Tooltip("Image-компонент иконки на кнопке воспроизведения/паузы.")] Image m_ButtonPlayOrPauseIcon; [SerializeField, Tooltip("Спрайт «играть».")] Sprite m_IconPlay; [SerializeField, Tooltip("Спрайт «пауза».")] Sprite m_IconPause; [SerializeField, Tooltip("Текст текущего и общего времени видео (MM:SS / MM:SS).")] TextMeshProUGUI m_VideoTimeText; [Header("Поведение")] [SerializeField, Tooltip("С каким состоянием запускать видео при включении объекта.")] StartPlaybackState m_StartState = StartPlaybackState.Playing; [Header("Внешние события (можно назначать методы в инспекторе)")] [SerializeField, Tooltip("Нажатие кнопки воспроизведения/паузы.")] UnityEvent m_OnPlayPausePressed; [SerializeField, Tooltip("Перетаскивание ползунка слайдера.")] UnityEvent m_OnSliderDrag; [SerializeField, Tooltip("Отпускание ползунка слайдера.")] UnityEvent m_OnSliderRelease; [SerializeField] private VideoAutoVisibilityController m_VisibilityController; VideoPlayer m_VideoPlayer; bool m_IsPlaying; // логическое состояние воспроизведения (его отражает иконка кнопки) bool m_IsDragging; // идёт ли перемотка ползунком bool m_SeekPending; // ждём завершения перемотки кадра, чтобы слайдер не «прыгал» int m_LastShownCurrentSeconds = -1; int m_LastShownTotalSeconds = -1; /// Воспроизводится ли видео сейчас (для внешних скриптов). public bool IsPlaying => m_IsPlaying; void Awake() { m_VideoPlayer = GetComponent(); m_VideoPlayer.playOnAwake = false; // воспроизведением управляем сами } void OnEnable() { m_Slider.onValueChanged.AddListener(OnSliderValueChanged); m_VideoPlayer.seekCompleted += OnSeekCompleted; m_Slider.gameObject.SetActive(true); m_Slider.SetValueWithoutNotify(0f); if (m_ButtonPlayOrPause != null) m_ButtonPlayOrPause.SetActive(true); m_LastShownCurrentSeconds = -1; m_LastShownTotalSeconds = -1; // Готовим клип, затем применяем стартовое состояние (в т.ч. показ первого кадра). m_VideoPlayer.prepareCompleted += OnVideoPrepared; m_VideoPlayer.Prepare(); } void OnDisable() { m_Slider.onValueChanged.RemoveListener(OnSliderValueChanged); m_VideoPlayer.seekCompleted -= OnSeekCompleted; m_VideoPlayer.prepareCompleted -= OnVideoPrepared; } void Update() { // Пока ползунок тянут или идёт перемотка — не перетираем значение слайдера позицией плеера. if (!m_IsDragging && !m_SeekPending && m_VideoPlayer.frameCount > 0) m_Slider.SetValueWithoutNotify((float)m_VideoPlayer.frame / m_VideoPlayer.frameCount); UpdateVideoTimeText(); } // ===== Внешние точки входа (назначаются в инспекторе Unity) ===== /// Кнопка воспроизведения/паузы — Button.OnClick. public void PlayOrPauseVideo() { Debug.Log($"[Video] PlayOrPauseVideo called. IsPlaying = {m_IsPlaying}"); if (m_IsPlaying) { Debug.Log("[Video] Stopping video"); Stop(); } else { Debug.Log("[Video] Starting video"); Play(); } Debug.Log("[Video] Invoking OnPlayPausePressed event"); m_OnPlayPausePressed?.Invoke(); } /// Перетаскивание ползунка — EventTrigger.Drag на слайдере. public void OnDrag() { if (!m_IsDragging) { m_IsDragging = true; m_VideoPlayer.Pause(); // пауза на время перемотки, m_IsPlaying не меняем } m_OnSliderDrag?.Invoke(); } /// Отпускание ползунка — EventTrigger.PointerUp (или EndDrag) на слайдере. public void OnRelease() { m_IsDragging = false; if (m_IsPlaying) m_VideoPlayer.Play(); // возвращаемся к воспроизведению, если оно было активно до перемотки m_OnSliderRelease?.Invoke(); } /// Нажатие на ползунок/дорожку — EventTrigger.PointerDown на слайдере. public void OnPointerDown() { if (m_IsDragging) return; m_IsDragging = true; m_VideoPlayer.Pause(); // пауза сразу при касании, m_IsPlaying не меняем } // ===== Внутренняя логика ===== void OnSliderValueChanged(float normalizedValue) { // Сюда попадают только пользовательские изменения значения: // программные обновления идут через SetValueWithoutNotify и слушателя не вызывают. SeekToNormalizedTime(normalizedValue); UpdateVideoTimeText(); } void SeekToNormalizedTime(float normalizedValue) { if (m_VideoPlayer.frameCount == 0) return; m_SeekPending = true; m_VideoPlayer.frame = (long)(m_VideoPlayer.frameCount * Mathf.Clamp01(normalizedValue)); } void OnSeekCompleted(VideoPlayer source) { m_SeekPending = false; } void OnVideoPrepared(VideoPlayer source) { source.prepareCompleted -= OnVideoPrepared; source.frame = 0; if (m_StartState == StartPlaybackState.Playing) Play(); else Stop(); // покажет первый кадр и встанет на паузу UpdateVideoTimeText(); } void Play() { m_IsPlaying = true; m_VideoPlayer.Play(); if (m_ButtonPlayOrPauseIcon != null && m_IconPause != null) m_ButtonPlayOrPauseIcon.sprite = m_IconPause; } void Stop() { m_IsPlaying = false; m_VideoPlayer.Pause(); if (m_ButtonPlayOrPauseIcon != null && m_IconPlay != null) m_ButtonPlayOrPauseIcon.sprite = m_IconPlay; m_VisibilityController?.OnVideoStopped(); } void UpdateVideoTimeText() { if (m_VideoTimeText == null) return; double length = m_VideoPlayer.length; double current = m_IsDragging ? m_Slider.value * length : m_VideoPlayer.time; // Перерисовываем строку только при смене секунды — экономим аллокации в Update. int currentSeconds = (int)current; int totalSeconds = (int)length; if (currentSeconds == m_LastShownCurrentSeconds && totalSeconds == m_LastShownTotalSeconds) return; m_LastShownCurrentSeconds = currentSeconds; m_LastShownTotalSeconds = totalSeconds; m_VideoTimeText.SetText($"{FormatTime(current)} / {FormatTime(length)}"); } static string FormatTime(double seconds) { if (double.IsNaN(seconds) || seconds < 0d) seconds = 0d; var time = TimeSpan.FromSeconds(seconds); return $"{(int)time.TotalMinutes:D2}:{time.Seconds:D2}"; } } }