“虞渠成” eea5878552 fix(VideoCtrl): 重构全屏视频适配逻辑,修复适配bug
补充详细的全屏视频适配踩坑注释,重构适配算法,实现按屏幕比例最大化显示视频且不变形,修复了原代码中UI旋转、锚点和uvRect计算错误的问题
2026-09-17 14:59:20 +08:00

472 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. 视频长边贴齐策略(按视频/屏幕比例选边):
// 判定 = 视频短边占屏幕比 = 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);
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;
}
}
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;
}
}
}