251 lines
9.8 KiB
C#
251 lines
9.8 KiB
C#
|
|
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}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|