“虞渠成” 0f14d60f2f feat: 完善设备连接流程与多资源更新
1.  新增视频加载中的提示文案与图标资源
2.  调整服务器API地址为正式环境
3.  重构连接设备页面逻辑,优化设备注册绑定流程
4.  更新教学视频与文案为中文版本
5.  增加视频播放前的就绪校验逻辑
2026-08-21 09:49:21 +08:00

449 lines
17 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)
{
// 根据视频宽高比选择全屏UI竖屏视频用竖屏全屏UI否则用默认全屏UI
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;
// 显示区域铺满屏幕保持视频比例超出部分裁剪cover模式尽量铺满屏幕
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°横屏视频旋转90°后铺满竖屏手机屏幕
float rotZ = Mathf.Abs(Mathf.DeltaAngle(fullRt.localEulerAngles.z, 0f));
bool rotated90 = rotZ >= 45f && rotZ <= 135f;
if (rotated90)
{
// 旋转90°的UI矩形宽高互换旋转后视觉铺满屏幕
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(parentH, parentW);
}
else
{
fullRt.anchorMin = Vector2.zero;
fullRt.anchorMax = Vector2.one;
fullRt.pivot = new Vector2(0.5f, 0.5f);
fullRt.sizeDelta = Vector2.zero;
}
// 按视频比例裁剪cover旋转后视频宽对应屏幕高显示比例需取反
float frameAspect = rotated90 ? parentH / parentW : parentW / parentH;
if (Mathf.Abs(frameAspect - videoAspect) >= 0.001f)
{
if (videoAspect > frameAspect)
{
// 视频相对更宽:裁剪左右
float uvW = frameAspect / videoAspect;
currentFullImage.uvRect = new Rect((1 - uvW) / 2f, 0, uvW, 1);
}
else
{
// 视频相对更窄:裁剪上下
float uvH = videoAspect / frameAspect;
currentFullImage.uvRect = new Rect(0, (1 - uvH) / 2f, 1, uvH);
}
}
}
}
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;
}
}
}