472 lines
19 KiB
C#
Raw Normal View History

2026-04-24 16:57:44 +08:00
using System;
using System.Collections;
using System.Collections.Generic;
2026-05-18 08:42:33 +08:00
using Kill.Core;
2026-04-24 16:57:44 +08:00
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
namespace Kill.UI.Components
{
public class VideoCtrl : MonoBehaviour
2026-05-18 08:42:33 +08:00
{
public bool istest;
public VideoPlayer videoPlayer;
2026-04-24 16:57:44 +08:00
2026-05-18 08:42:33 +08:00
public bool needFullScreen = false;
bool isFullScreen = false;
/// <summary>
/// 0:正常播放 1:全屏播放
/// </summary>
public List<RenderTexture> renderTextures;
public RawImage windowImage;
public RawImage fullImage;
/// <summary>竖屏全屏UI竖屏视频全屏时使用在场景中配置</summary>
public RawImage portraitFullImage;
/// <summary>当前使用的全屏显示目标(根据视频宽高比选择)</summary>
RawImage currentFullImage;
2026-05-18 08:42:33 +08:00
public GameObject playButton;
public GameObject pauseButton;
public Text totalTimeText;
public Text nowTime;
public Slider slider;
bool isplaying = false;
2026-06-08 08:55:10 +08:00
bool ispause = true;
2026-05-18 08:42:33 +08:00
Action playOver;
string url;
2026-06-12 16:07:27 +08:00
public GameObject loading;
2026-05-18 08:42:33 +08:00
private void FixedUpdate()
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
if (isplaying)
{
2026-04-24 16:57:44 +08:00
2026-05-18 08:42:33 +08:00
OnVideoPlaying();
}
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
public void Init(string url, Action playOver)
2026-04-24 16:57:44 +08:00
{
2026-06-12 16:07:27 +08:00
if(loading)
loading.SetActive(true);
// 视频加载完成前隐藏窗口画面,避免显示上一个视频残留
if (windowImage != null)
windowImage.gameObject.SetActive(false);
// 停止播放状态与进度记录,避免加载期间用旧数据刷新进度条/时间
isplaying = false;
lastFrame = 0;
// 重置时间显示,避免加载期间显示上一个视频的旧时长
totalTime = 0;
2026-06-08 08:55:10 +08:00
nowTime.text=SecondToHour(0);
if (totalTimeText != null)
totalTimeText.text=SecondToHour(0);
if (slider != null)
slider.value=0;
2026-05-18 08:42:33 +08:00
this.playOver = playOver;
this.url = url;
SetVideo(url);
if (playButton != null)
{
playButton.SetActive(false);
}
if (pauseButton != null)
{
pauseButton.SetActive(false);
}
ispause = true;
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
public void PlayVideo()
{
StartCoroutine(YieldPlay());
2026-04-24 16:57:44 +08:00
2026-05-18 08:42:33 +08:00
}
2026-04-24 16:57:44 +08:00
2026-05-18 08:42:33 +08:00
public IEnumerator YieldPlay()
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
yield return new WaitForSeconds(0.1f);
ispause = false;
videoPlayer.Play();
if (playButton != null)
{
playButton.SetActive(false);
}
if (pauseButton != null)
{
pauseButton.SetActive(true);
}
isplaying = true;
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
public void PauseVideo()
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
ispause = true;
videoPlayer.Pause();
isplaying = false;
if (playButton != null)
{
playButton.SetActive(true);
}
if (pauseButton != null)
{
pauseButton.SetActive(false);
}
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
public void RePlay()
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
videoPlayer.Stop();
videoPlayer.frame = 0;
PlayVideo();
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
public void SetVideo(string url)
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
videoPlayer.url = url;
videoPlayer.prepareCompleted += VideoPrepared;
videoPlayer.Prepare();
if (playButton != null)
{
playButton.SetActive(true);
}
2026-04-24 16:57:44 +08:00
}
2026-06-08 08:55:10 +08:00
public void Stop()
{
videoPlayer.Stop();
videoPlayer.url = "";
isplaying = false;
lastFrame = 0;
2026-06-08 08:55:10 +08:00
nowTime.text=SecondToHour(0);
// 停止时清空总时长与进度条,避免残留上一个视频的显示
totalTime = 0;
if (totalTimeText != null)
totalTimeText.text=SecondToHour(0);
if (slider != null)
slider.value=0;
2026-06-08 08:55:10 +08:00
}
2026-05-18 08:42:33 +08:00
int totalTime = 0;
public void VideoPrepared(VideoPlayer player)
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
if (renderTextures != null && renderTextures.Count > 0)
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
foreach (var r in renderTextures)
Destroy(r);
renderTextures.Clear();
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
renderTextures=new List<RenderTexture>();
// 窗口视图:完整显示视频并居中,保持比例,多余部分留黑(竖屏视频占满高度、两侧留黑)
2026-05-18 08:42:33 +08:00
RenderTexture texture = new RenderTexture((int)player.width, (int)player.height, 1);
renderTextures.Add(texture);
player.targetTexture = texture;
windowImage.texture = texture;
// 视频加载完成,重新显示窗口画面
if (windowImage != null)
windowImage.gameObject.SetActive(true);
FitWindowAspect(windowImage, player.width / (float)Mathf.Max(1, (int)player.height));
2026-05-18 08:42:33 +08:00
if (needFullScreen)
2026-04-24 16:57:44 +08:00
{
// ============================================================
// 全屏视频适配经验总结(踩坑记录,避免后续重复犯错)
// ============================================================
// 目标:完整显示视频、不变形;在不同设备宽高比下都让视频占屏幕比例最大化。
//
// 1. UI 节点角色(场景已配置):
// - portraitFullImage未旋转的 RawImage专门用于竖屏视频
// - fullImage旋转了 -90° 的 RawImage专门用于横屏视频让 app 不旋转也能看横屏视频)
// - 选择规则video 高>宽 → portraitFullImage否则 → fullImage
//
// 2. 视觉屏幕 = 父 stretch 节点的实际 rect手机屏幕真实宽高
// 与子节点是否旋转无关。RectTransform 的 sizeDelta 在 UI 旋转 90° 后,
// (sizeDelta.x, sizeDelta.y) 视觉上变成 (sizeDelta.y, sizeDelta.x)。
//
// 3. 视频长边贴齐策略(按视频/屏幕比例选边):
// 判定 = 视频短边占屏幕比 = 1/videoAspect vs 屏幕宽高比
// - 1/videoAspect > 屏幕比 → 视频长边贴屏幕短边(视觉宽=parentW留黑上下
// 例:竖屏手机 750x1624 + 横屏视频 1.78 → 视觉=(750, 1335)sizeDelta=(1335, 750)
// - 1/videoAspect < 屏幕比 → 视频长边贴屏幕长边(视觉高=parentH留黑左右
// 例:接近正方形 1179x1600 + 横屏视频 1.78 → 视觉=(900, 1600)sizeDelta=(1600, 900)
// - 比例接近时(屏幕接近正方形)走"长边贴长边",让视频占用面积更大
//
// 4. 常见错误:
// - 用 cover 模式裁切 UV横屏视频在竖屏手机上会左右被裁丢失画面
// - 用 contain 让短边贴齐屏幕短边:在 750x1624 上视频会被压缩成窄长条
// - frameAspect 写反rotated90 时应该是 parentW/parentH 还是 parentH/parentW 要分清
// - 视觉屏幕写成了 rotated90 ? parentH : parentW视觉屏幕与旋转无关始终是 parentW x parentH
//
// 5. UV 不动:使用 contain 不裁切即可,无需调整 uvRect
// ============================================================
currentFullImage = (player.width < player.height && portraitFullImage != null) ? portraitFullImage : fullImage;
// 全屏纹理按视频分辨率创建,保证宽高比正确、不变形
RenderTexture fullTexture = new RenderTexture((int)player.width, (int)player.height, 1);
2026-05-18 08:42:33 +08:00
renderTextures.Add(fullTexture);
currentFullImage.texture = fullTexture;
float videoAspect = player.width / (float)Mathf.Max(1, (int)player.height);
var fullRt = currentFullImage.rectTransform;
var fullParent = fullRt.parent as RectTransform;
currentFullImage.uvRect = new Rect(0, 0, 1, 1);
if (fullParent != null && fullParent.rect.width > 0 && fullParent.rect.height > 0)
{
float parentW = fullParent.rect.width;
float parentH = fullParent.rect.height;
// 检测全屏UI是否旋转了90°
float rotZ = Mathf.Abs(Mathf.DeltaAngle(fullRt.localEulerAngles.z, 0f));
bool rotated90 = rotZ >= 45f && rotZ <= 135f;
// 视觉屏幕 = 父 stretch 覆盖区域即手机屏幕真实宽高与UI旋转无关
float screenVisualW = parentW;
float screenVisualH = parentH;
// 按视频/屏幕比例决定贴齐哪一边,确保视频占屏幕比例最大化
float visualW, visualH;
if ((1f / videoAspect) > (screenVisualW / screenVisualH))
{
// 视频长边贴屏幕短边(横向贴满,留黑上下)
visualW = screenVisualW;
visualH = visualW * videoAspect;
}
else
{
// 视频长边贴屏幕长边(纵向贴满,留黑左右)
visualH = screenVisualH;
visualW = visualH / videoAspect;
}
// RectTransform 的 sizeDeltaUI 旋转 90° 时 (x,y) = (visualH, visualW)
float rtW = rotated90 ? visualH : visualW;
float rtH = rotated90 ? visualW : visualH;
fullRt.anchorMin = new Vector2(0.5f, 0.5f);
fullRt.anchorMax = new Vector2(0.5f, 0.5f);
fullRt.pivot = new Vector2(0.5f, 0.5f);
fullRt.sizeDelta = new Vector2(rtW, rtH);
fullRt.anchoredPosition = Vector2.zero;
}
2026-05-18 08:42:33 +08:00
}
videoPlayer.frame = 0;
if (slider != null)
slider.value = 0;
// 总时长计算:优先用 length切换视频时 frameCount 可能残留旧视频的帧数,作为兜底
float duration = (float)player.length;
if (duration > 0)
{
totalTime = (int)(duration + 0.5f);
}
else if (player.frameCount > 0 && player.frameRate > 0)
2026-05-18 08:42:33 +08:00
{
totalTime = (int)(player.frameCount / player.frameRate + 0.5f);
2026-04-24 16:57:44 +08:00
}
else
{
totalTime = 0;
}
if (totalTimeText != null)
totalTimeText.text = SecondToHour(totalTime);
2026-04-24 16:57:44 +08:00
2026-05-18 08:42:33 +08:00
videoPlayer.prepareCompleted -= VideoPrepared;
player.loopPointReached += VideoOver;
if (playButton != null)
playButton.SetActive(true);
2026-06-08 08:55:10 +08:00
nowTime.text=SecondToHour(0);
2026-06-12 16:07:27 +08:00
if(loading)
loading.SetActive(false);
2026-04-24 16:57:44 +08:00
}
/// <summary>
/// 窗口显示适配在窗口内完整显示视频contain模式并居中保持比例多余部分留黑
/// 竖屏视频占满高度、两侧留黑;横屏视频占满宽度、上下留黑
/// 仅调整windowImage铺满窗口的RawImage自身的尺寸不影响窗口UI位置
/// </summary>
private void FitWindowAspect(RawImage image, float videoAspect)
{
if (image == null) return;
var rt = image.rectTransform;
var parent = rt.parent as RectTransform;
float parentW = parent != null ? parent.rect.width : Screen.width;
float parentH = parent != null ? parent.rect.height : Screen.height;
float targetW, targetH;
if (videoAspect >= parentW / parentH)
{
// 视频更宽:宽度铺满,高度按比例缩小,上下留黑
targetW = parentW;
targetH = parentW / videoAspect;
}
else
{
// 视频更高:高度铺满,宽度按比例缩小,左右留黑
targetH = parentH;
targetW = parentH * videoAspect;
}
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
rt.sizeDelta = new Vector2(targetW, targetH);
rt.anchoredPosition = Vector2.zero;
image.uvRect = new Rect(0, 0, 1, 1);
}
2026-05-18 08:42:33 +08:00
public void ShowAnimation(int dir)
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
Animation Anim = GetComponent<Animation>();
if (Anim != null)
{
if (dir == -1)
{
Anim["视频出现"].time = Anim["视频出现"].clip.length;
Anim["视频出现"].speed = -1;
Anim.Play("视频出现");
}
else
{
Anim["视频出现"].time = 0;
Anim["视频出现"].speed = 1;
Anim.Play("视频出现");
}
}
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
long lastFrame = 0;
/// <summary>
/// 播放中事件(给进度条赋值)
/// </summary>
public void OnVideoPlaying()
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
if (slider == null)
return;
// 帧数据未就绪(重新播放/切换视频的瞬间 frameCount 可能为0跳过本次刷新避免除零产生 Infinity/NaN
if (videoPlayer.frameCount <= 0)
return;
2026-05-18 08:42:33 +08:00
if (Mathf.Abs(videoPlayer.frame - lastFrame) < 1)
return;
slider.value = (videoPlayer.frame + 1) / (float)videoPlayer.frameCount;
lastFrame = videoPlayer.frame;
nowTime.text = SecondToHour((int)(totalTime * slider.value));
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
/// <summary>
/// 拖动进度条
/// </summary>
public void PointDownUp()
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
//if (slider.value != lastValue)
//{
// videoPlayer.frame = (long)(videoPlayer.frameCount * slider.value);
//}
if (slider.value >= 0.98f)
{
videoPlayer.Stop();
VideoOver(videoPlayer);
return;
2026-04-24 16:57:44 +08:00
2026-05-18 08:42:33 +08:00
}
videoPlayer.frame = (long)(videoPlayer.frameCount * slider.value) - 1;
nowTime.text = SecondToHour((int)(totalTime * slider.value));
if (!ispause)
{
videoPlayer.Play();
isplaying = true;
}
// TeacherServer.m_instance.SendVideoProcess((long)(videoPlayer.frameCount * slider.value) - 1);
}
public void PointDown()
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
videoPlayer.Pause();
isplaying = false;
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
public void Draging()
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
videoPlayer.frame = (long)(videoPlayer.frameCount * slider.value) - 1;
nowTime.text = SecondToHour((int)(totalTime * slider.value));
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
public void VideoOver(VideoPlayer vp)
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
vp.loopPointReached -= VideoOver;
isplaying = false;
if (playButton != null)
{
playButton.SetActive(true);
}
if (pauseButton != null)
{
pauseButton.SetActive(false);
}
// 全屏播放结束后自动退出全屏
if (isFullScreen)
SetFullScreen(false);
2026-06-08 08:55:10 +08:00
playOver?.Invoke();
// 播放结束不重新加载视频停止并回到开头重置UI等待再次播放
videoPlayer.Stop();
videoPlayer.frame = 0;
lastFrame = 0;
nowTime.text = SecondToHour(0);
if (slider != null) slider.value = 0;
ispause = true;
// 重新订阅播放结束事件,保证再次播放结束后仍能触发
vp.loopPointReached += VideoOver;
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
public void SetFullScreen(bool isFullScreen)
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
this.isFullScreen = isFullScreen;
// 未设置过全屏目标时兜底用默认全屏UI
if (currentFullImage == null) currentFullImage = fullImage;
2026-05-18 08:42:33 +08:00
if (isFullScreen)
{
// 视频未加载完成时提示,避免全屏操作无效(此时 renderTextures 也未就绪)
if (!videoPlayer.isPrepared || renderTextures == null || renderTextures.Count < 2)
{
ToastUI.Show("100332");
return;
}
2026-05-18 08:42:33 +08:00
#if UNITY_ANDROID
AndroidStatusBar.statusBarState = AndroidStatusBar.States.Hidden;
#endif
videoPlayer.targetTexture = renderTextures[1];
currentFullImage.transform.parent.gameObject.SetActive(true);
2026-05-18 08:42:33 +08:00
UIManager.Instance.AddBackAction(() => SetFullScreen(false));
if (ispause)
PlayVideo();
}
else
{
#if UNITY_ANDROID
AndroidStatusBar.statusBarState = AndroidStatusBar.States.TranslucentOverContent;
#endif
videoPlayer.targetTexture = renderTextures[0];
currentFullImage.transform.parent.gameObject.SetActive(false);
2026-05-18 08:42:33 +08:00
UIManager.Instance.RecoverBackAction();
}
2026-04-24 16:57:44 +08:00
}
2026-05-18 08:42:33 +08:00
/// <summary>
/// 秒转换小时
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
public string SecondToHour(float time)
2026-04-24 16:57:44 +08:00
{
2026-05-18 08:42:33 +08:00
int hour = 0;
int minute = 0;
int second = 0;
second = (int)time;
if (second >= 60)
2026-05-18 08:42:33 +08:00
{
minute = second / 60;
second = second % 60;
}
if (minute >= 60)
2026-05-18 08:42:33 +08:00
{
hour = minute / 60;
minute = minute % 60;
}
string result = "";
if (hour > 0)
{
result += hour + ":";
2026-04-24 16:57:44 +08:00
2026-05-18 08:42:33 +08:00
}
if (minute < 10)
result += "0";
result += minute + ":";
if (second < 10)
result += "0";
result += second.ToString();
return result;
2026-04-24 16:57:44 +08:00
}
}
}