2026-10-16 09:43:15 +08:00

477 lines
19 KiB
C#
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 System.Collections;
using System.Collections.Generic;
using Kill.Core;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
namespace Kill.UI.Components
{
public class VideoCtrl : MonoBehaviour
{
public bool istest;
public VideoPlayer videoPlayer;
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;
public GameObject playButton;
public GameObject pauseButton;
public Text totalTimeText;
public Text nowTime;
public Slider slider;
bool isplaying = false;
bool ispause = true;
Action playOver;
string url;
public GameObject loading;
private void FixedUpdate()
{
if (isplaying)
{
OnVideoPlaying();
}
}
public void Init(string url, Action playOver)
{
if(loading)
loading.SetActive(true);
// 视频加载完成前隐藏窗口画面,避免显示上一个视频残留
if (windowImage != null)
windowImage.gameObject.SetActive(false);
// 停止播放状态与进度记录,避免加载期间用旧数据刷新进度条/时间
isplaying = false;
lastFrame = 0;
// 重置时间显示,避免加载期间显示上一个视频的旧时长
totalTime = 0;
nowTime.text=SecondToHour(0);
if (totalTimeText != null)
totalTimeText.text=SecondToHour(0);
if (slider != null)
slider.value=0;
this.playOver = playOver;
this.url = url;
SetVideo(url);
if (playButton != null)
{
playButton.SetActive(false);
}
if (pauseButton != null)
{
pauseButton.SetActive(false);
}
ispause = true;
}
public void PlayVideo()
{
StartCoroutine(YieldPlay());
}
public IEnumerator YieldPlay()
{
yield return new WaitForSeconds(0.1f);
ispause = false;
videoPlayer.Play();
if (playButton != null)
{
playButton.SetActive(false);
}
if (pauseButton != null)
{
pauseButton.SetActive(true);
}
isplaying = true;
}
public void PauseVideo()
{
ispause = true;
videoPlayer.Pause();
isplaying = false;
if (playButton != null)
{
playButton.SetActive(true);
}
if (pauseButton != null)
{
pauseButton.SetActive(false);
}
}
public void RePlay()
{
videoPlayer.Stop();
videoPlayer.frame = 0;
PlayVideo();
}
public void SetVideo(string url)
{
videoPlayer.url = url;
videoPlayer.prepareCompleted += VideoPrepared;
videoPlayer.Prepare();
if (playButton != null)
{
playButton.SetActive(true);
}
}
public void Stop()
{
videoPlayer.Stop();
videoPlayer.url = "";
isplaying = false;
lastFrame = 0;
nowTime.text=SecondToHour(0);
// 停止时清空总时长与进度条,避免残留上一个视频的显示
totalTime = 0;
if (totalTimeText != null)
totalTimeText.text=SecondToHour(0);
if (slider != null)
slider.value=0;
}
int totalTime = 0;
public void VideoPrepared(VideoPlayer player)
{
if (renderTextures != null && renderTextures.Count > 0)
{
foreach (var r in renderTextures)
Destroy(r);
renderTextures.Clear();
}
renderTextures=new List<RenderTexture>();
// 窗口视图:完整显示视频并居中,保持比例,多余部分留黑(竖屏视频占满高度、两侧留黑)
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));
if (needFullScreen)
{
// ============================================================
// 全屏视频适配经验总结(踩坑记录,避免后续重复犯错)
// ============================================================
// 目标:完整显示视频、不变形;在不同设备宽高比下都让视频占屏幕比例最大化。
//
// 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. contain 适配(按视频"视觉宽高比"与屏幕宽高比比较选边):
// 视觉宽高比:UI 未旋转时 = videoAspect;UI 旋转90°后 = 1/videoAspect
// - 视觉宽高比 > 屏幕宽高比 → 视觉宽铺满,视觉高 = 宽/视觉宽高比(留黑上下)
// 例:竖屏手机 750x1624 + 横屏视频 1.78(旋转90°)→ 视觉=(750, 1335),sizeDelta=(1335, 750)
// 例:竖屏手机 750x1624 + 竖屏视频 9:16(未旋转)→ 视觉=(750, 1333)
// - 视觉宽高比 < 屏幕宽高比 → 视觉高铺满,视觉宽 = 高*视觉宽高比(留黑左右)
// 例:接近正方形 1179x1600 + 横屏视频 1.78(旋转90°)→ 视觉=(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);
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;
// 视频在"视觉坐标系"下的宽高比:
// UI 未旋转时 = 视频原始宽高比 videoAspect(竖屏视频走未旋转的 portraitFullImage)
// UI 旋转90°后 = 1/videoAspect(横屏视频旋转后视觉上变竖屏)
float visualVideoAspect = rotated90 ? (1f / videoAspect) : videoAspect;
float screenAspect = screenVisualW / screenVisualH;
if (visualVideoAspect > screenAspect)
{
// 视频视觉上更宽:视觉宽铺满(留黑上下)
visualW = screenVisualW;
visualH = visualW / visualVideoAspect;
}
else
{
// 视频视觉上更高:视觉高铺满(留黑左右)
visualH = screenVisualH;
visualW = visualH * visualVideoAspect;
}
// RectTransform 的 sizeDelta:UI 旋转 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;
}
}
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)
{
totalTime = (int)(player.frameCount / player.frameRate + 0.5f);
}
else
{
totalTime = 0;
}
if (totalTimeText != null)
totalTimeText.text = SecondToHour(totalTime);
videoPlayer.prepareCompleted -= VideoPrepared;
player.loopPointReached += VideoOver;
if (playButton != null)
playButton.SetActive(true);
nowTime.text=SecondToHour(0);
if(loading)
loading.SetActive(false);
}
/// <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);
}
public void ShowAnimation(int dir)
{
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("视频出现");
}
}
}
long lastFrame = 0;
/// <summary>
/// 播放中事件(给进度条赋值)
/// </summary>
public void OnVideoPlaying()
{
if (slider == null)
return;
// 帧数据未就绪(重新播放/切换视频的瞬间 frameCount 可能为0),跳过本次刷新,避免除零产生 Infinity/NaN
if (videoPlayer.frameCount <= 0)
return;
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));
}
/// <summary>
/// 拖动进度条
/// </summary>
public void PointDownUp()
{
//if (slider.value != lastValue)
//{
// videoPlayer.frame = (long)(videoPlayer.frameCount * slider.value);
//}
if (slider.value >= 0.98f)
{
videoPlayer.Stop();
VideoOver(videoPlayer);
return;
}
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()
{
videoPlayer.Pause();
isplaying = false;
}
public void Draging()
{
videoPlayer.frame = (long)(videoPlayer.frameCount * slider.value) - 1;
nowTime.text = SecondToHour((int)(totalTime * slider.value));
}
public void VideoOver(VideoPlayer vp)
{
vp.loopPointReached -= VideoOver;
isplaying = false;
if (playButton != null)
{
playButton.SetActive(true);
}
if (pauseButton != null)
{
pauseButton.SetActive(false);
}
// 全屏播放结束后自动退出全屏
if (isFullScreen)
SetFullScreen(false);
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;
}
public void SetFullScreen(bool isFullScreen)
{
this.isFullScreen = isFullScreen;
// 未设置过全屏目标时兜底用默认全屏UI
if (currentFullImage == null) currentFullImage = fullImage;
if (isFullScreen)
{
// 视频未加载完成时提示,避免全屏操作无效(此时 renderTextures 也未就绪)
if (!videoPlayer.isPrepared || renderTextures == null || renderTextures.Count < 2)
{
ToastUI.Show("100332");
return;
}
#if UNITY_ANDROID
AndroidStatusBar.statusBarState = AndroidStatusBar.States.Hidden;
#endif
videoPlayer.targetTexture = renderTextures[1];
currentFullImage.transform.parent.gameObject.SetActive(true);
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);
UIManager.Instance.RecoverBackAction();
}
}
/// <summary>
/// 秒转换小时
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
public string SecondToHour(float time)
{
int hour = 0;
int minute = 0;
int second = 0;
second = (int)time;
if (second >= 60)
{
minute = second / 60;
second = second % 60;
}
if (minute >= 60)
{
hour = minute / 60;
minute = minute % 60;
}
string result = "";
if (hour > 0)
{
result += hour + ":";
}
if (minute < 10)
result += "0";
result += minute + ":";
if (second < 10)
result += "0";
result += second.ToString();
return result;
}
}
}