Files
virtual-university-tour/sources/Assets/Scripting/CustomVideoTimeScrubControl.cs
T
2026-07-12 18:53:05 +04:00

251 lines
9.8 KiB
C#
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}";
}
}
}