Files

251 lines
9.8 KiB
C#
Raw Permalink Normal View History

2026-07-12 18:53:05 +04:00
using System;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using UnityEngine.Video;
namespace Unity.VRTemplate
{
/// <summary>
/// Связывает UI-слайдер с VideoPlayer: показывает прогресс, позволяет перематывать
/// видео перетаскиванием ползунка и переключать воспроизведение/паузу кнопкой.
/// </summary>
[RequireComponent(typeof(VideoPlayer))]
public class CustomVideoTimeScrubControl : MonoBehaviour
{
/// <summary>Состояние видео при старте.</summary>
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;
/// <summary>Воспроизводится ли видео сейчас (для внешних скриптов).</summary>
public bool IsPlaying => m_IsPlaying;
void Awake()
{
m_VideoPlayer = GetComponent<VideoPlayer>();
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) =====
/// <summary>Кнопка воспроизведения/паузы — Button.OnClick.</summary>
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();
}
/// <summary>Перетаскивание ползунка — EventTrigger.Drag на слайдере.</summary>
public void OnDrag()
{
if (!m_IsDragging)
{
m_IsDragging = true;
m_VideoPlayer.Pause(); // пауза на время перемотки, m_IsPlaying не меняем
}
m_OnSliderDrag?.Invoke();
}
/// <summary>Отпускание ползунка — EventTrigger.PointerUp (или EndDrag) на слайдере.</summary>
public void OnRelease()
{
m_IsDragging = false;
if (m_IsPlaying)
m_VideoPlayer.Play(); // возвращаемся к воспроизведению, если оно было активно до перемотки
m_OnSliderRelease?.Invoke();
}
/// <summary>Нажатие на ползунок/дорожку — EventTrigger.PointerDown на слайдере.</summary>
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}";
}
}
}