Files
XRLib/Assets/Scripts/Factory/Playback/PlaybackService.cs
2025-12-08 21:06:05 +09:00

420 lines
20 KiB
C#

#nullable enable
using Best.HTTP;
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
using UVC.Data.Core;
using UVC.Data.Http;
using UVC.Factory.Component;
using UVC.Factory.Playback.UI;
using UVC.Util;
namespace UVC.Factory.Playback
{
/// <summary>
/// Playback 관련 서비스 클래스입니다.
/// - 싱글턴 패턴으로 사용합니다.
/// - 재생 데이터 요청, 다운로드, 시간 스케일 조정 등 주요 기능을 제공합니다.
///
/// <example>
/// <code>
/// // 재생 시작 예시
/// var itemData = new UIPlaybackListItemData { date = "2024-07-29", time = "13", sqlFileName = "2024-07-29_13.sqlite" };
/// await PlaybackService.Instance.StartAsync(itemData);
///
/// // 재생 목록 데이터 요청 예시
/// var data = await PlaybackService.Instance.RequestDataAsync();
///
/// // 재생 종료 예시
/// PlaybackService.Instance.Exit();
/// </code>
/// </example>
/// </summary>
public class PlaybackService
{
#region Singleton
// 싱글턴 인스턴스. PlaybackService.Instance로 접근합니다.
private static readonly PlaybackService instance = new PlaybackService(new PlaybackRepository());
public static PlaybackService Instance => instance;
static PlaybackService() { }
#endregion
// 재생 데이터가 저장되는 폴더 경로입니다.
public static readonly string PlaybackFolderPath = Path.Combine(Application.persistentDataPath, "playback");//streamingAssetsPath, "playback"); appData 폴더로 변경
private readonly PlaybackRepository repository;
private string date = string.Empty;
private string time = string.Empty;
private string fileName = string.Empty;
public Action? OnStartPlayback;
public Action? OnExitPlayback;
public Action? OnChangedTime;
private float timeScale = 1.0f;
/// <summary>
/// 재생 시간 스케일(배속)입니다. 1.0f가 기본입니다.
/// </summary>
/// <example>
/// <code>
/// // 재생 속도를 2배로 변경
/// PlaybackService.Instance.TimeScale = 2.0f;
/// </code>
/// </example>
public float TimeScale
{
get => timeScale;
internal set
{
if (value < 1f) value = 1f;
if (timeScale != value)
{
timeScale = value;
//Time.timeScale = timeScale;
OnChangeTimeScale?.Invoke(timeScale);
}
}
}
public Action<float>? OnChangeTimeScale;
/// <summary>
/// 생성자. 일반적으로 직접 호출하지 않고 싱글턴 인스턴스를 사용합니다.
/// </summary>
private PlaybackService(PlaybackRepository repository)
{
this.repository = repository;
}
/// <summary>
/// 서버에서 재생 가능한 날짜별 데이터 목록을 비동기로 요청합니다.
/// </summary>
/// <returns>성공 시 날짜별 재생 목록 딕셔너리, 실패 시 null</returns>
/// <example>
/// <code>
/// var data = await PlaybackService.Instance.RequestDataAsync();
/// if (data != null)
/// {
/// foreach (var date in data.Keys)
/// {
/// Debug.Log($"날짜: {date}");
/// }
/// }
/// </code>
/// </example>
public async UniTask<Dictionary<string, Dictionary<string, string>>?> RequestDataAsync()
{
Dictionary<string, Dictionary<string, string>>? data = await repository.RequestPlaybackDateList();
return data;
}
/// <summary>
/// 재생을 위한 기본 정보 데이터를 비동기로 처리합니다.
/// </summary>
/// <param name="date">날짜(예: "2024-07-29")</param>
/// <param name="time">시간(예: "13")</param>
/// <param name="fileName">파일명(예: "2024-07-29_13.sqlite")</param>
/// <param name="minute">분(기본값: "00")</param>
/// <param name="second">초(기본값: "00")</param>
/// <example>
/// <code>
/// await PlaybackService.Instance.DispatchBaseInfoData("2024-07-29", "13", "2024-07-29_13.sqlite");
/// </code>
/// </example>
public async UniTask DispatchBaseInfoData(string date, string time, string fileName, string minute = "00", string second = "00")
{
#if UNITY_WEBGL && !UNITY_EDITOR
// WebGL: ThreadPool 미사용, 직접 실행
this.date = date;
this.time = time;
this.fileName = fileName;
DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:{minute}:{second}.000Z");
string formatTime = DateTimeUtil.FormatTime(dateTime);
List<PlaybackSQLiteDataEntity> list = await repository.SelectBySecondBaseInfo(date, fileName, formatTime);
if (list.Count > 0)
{
OnChangedTime?.Invoke();
HttpRequestConfig httpRequestConfig = new HttpRequestConfig(string.Empty);
httpRequestConfig.SetUpdatedDataOnly(true);
httpRequestConfig.SetSplitResponseByKey(true);
httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
{
HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data, null, true);
}
}
#else
await UniTask.RunOnThreadPool(async () =>
{
//헝가리 시간임
this.date = date;
this.time = time;
this.fileName = fileName;
DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:{minute}:{second}.000Z");
string formatTime = DateTimeUtil.FormatTime(dateTime);
//baseInfo 가져오기
List <PlaybackSQLiteDataEntity> list = await repository.SelectBySecondBaseInfo(date, fileName, formatTime);
if (list.Count > 0)
{
if(OnChangedTime != null) OnChangedTime.Invoke();
HttpRequestConfig httpRequestConfig = new HttpRequestConfig("");
httpRequestConfig.SetUpdatedDataOnly(true);
httpRequestConfig.SetSplitResponseByKey(true);
httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
{
HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data, null, true);
}
}
});
#endif
}
/// <summary>
/// 실시간 재생 데이터(특정 초 단위)를 비동기로 처리합니다.
/// </summary>
/// <param name="second">0 ~ 3600 (초 단위)</param>
/// <param name="speed">재생 속도</param>
/// <example>
/// <code>
/// await PlaybackService.Instance.DispatchRealTimeData(120, 1); // 120초(2분) 위치 데이터 처리
/// </code>
/// </example>
public async UniTask DispatchRealTimeData(int second, int speed)
{
#if UNITY_WEBGL && !UNITY_EDITOR
int newSecond = second;
if (newSecond > 36000) newSecond = 36000;
DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:00:00.000Z").AddSeconds(newSecond);
string formatTime = DateTimeUtil.FormatTime(dateTime);
List<PlaybackSQLiteDataEntity> list = await repository.SelectBySecondAsync(date, fileName, formatTime, 1);
if (list.Count > 0)
{
HttpRequestConfig httpRequestConfig = new HttpRequestConfig(string.Empty);
httpRequestConfig.SetUpdatedDataOnly(true);
httpRequestConfig.SetSplitResponseByKey(true);
httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
{
HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
}
}
#else
await UniTask.RunOnThreadPool(async () =>
{
int newSecond = second;
if (newSecond > 36000) newSecond = 36000;
//utc 시간으로 변환
DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:00:00.000Z").AddSeconds(newSecond);//.AddChild(-DateTimeUtil.UtcKoreaGap);
string formatTime = DateTimeUtil.FormatTime(dateTime);
List<PlaybackSQLiteDataEntity> list = await repository.SelectBySecondAsync(date, fileName, formatTime, 1);
//Debug.Log($"DispatchRealTimeData {date} {time} {formatTime} {newSecond} {list.Count}");
if (list.Count > 0)
{
HttpRequestConfig httpRequestConfig = new HttpRequestConfig("");
httpRequestConfig.SetUpdatedDataOnly(true);
httpRequestConfig.SetSplitResponseByKey(true);
httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
{
HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
}
}
});
#endif
}
/// <summary>
/// 재생을 시작합니다. UI를 표시하고 데이터를 세팅합니다.
/// </summary>
/// <param name="data">재생 목록 아이템 데이터</param>
/// <example>
/// <code>
/// var itemData = new UIPlaybackListItemData { date = "2024-07-29", time = "13", sqlFileName = "2024-07-29_13.sqlite" };
/// await PlaybackService.Instance.StartAsync(itemData);
/// </code>
/// </example>
public async UniTask StartAsync(UIPlaybackListItemData data)
{
timeScale = 1.0f; //기본 시간 스케일 설정
UIPlayback.Instance.Show();
await UIPlayback.Instance.SetData(data.date, data.time, data.sqlFileName);
OnStartPlayback?.Invoke();
}
/// <summary>
/// 재생을 종료합니다. (이벤트 발생)
/// </summary>
/// <example>
/// <code>
/// PlaybackService.Instance.Exit();
/// </code>
/// </example>
public void Exit()
{
OnExitPlayback?.Invoke();
}
/// <summary>
/// 재생 데이터 파일을 준비(다운로드 및 압축 해제)합니다.
/// 이미 파일이 있으면 바로 콜백을 호출합니다.
/// </summary>
/// <param name="date">날짜(예: "2024-12-05")</param>
/// <param name="time">시간(예: "13")</param>
/// <param name="fileName">파일명(예: "2024-12-05_0.sqlite.7z")</param>
/// <param name="OnProgress">진행 상황 콜백 (진행 바이트, 전체 바이트, 퍼센트)</param>
/// <param name="OnComplete">완료 콜백 (에러 메시지, 성공 시 null)</param>
/// <returns>다운로드 요청 객체(필요시 Abort 등 제어 가능), 이미 파일이 있으면 null</returns>
/// <example>
/// <code>
/// // 파일 준비 및 다운로드 예시
/// PlaybackService.Instance.ReadyData(
/// "2024-12-05", "13", "2024-12-05_0.sqlite.7z",
/// (progress, total, percent) => Debug.Log($"{progress}/{total} ({percent * 100:F1}%)"),
/// (error) => {
/// if (string.IsNullOrEmpty(error))
/// Debug.Log("파일 준비 완료");
/// else
/// Debug.LogError($"오류: {error}");
/// }
/// );
/// </code>
/// </example>
public HTTPRequest? ReadyData(string date, string time, string fileName, Action<long, long, float> OnProgress, Action<string> OnComplete)
{
//date : "2024-12-05"
//fileName : "2024-12-05_0.sqlite.7z"
string playbackPath = PlaybackService.PlaybackFolderPath;
string tempPath = Path.Combine(playbackPath, "temp");//한국 시간으로 변경하기 때문에 임시 폴더 만들어서 압축 해제 후 이동
string datePath = Path.Combine(playbackPath, date);
var fileNameArr = fileName.Split(".");
string zipFilePath = Path.Combine(datePath, fileName);
string sqlFilePath = Path.Combine(datePath, fileNameArr[0] + ".sqlite");
DateTime utcDateTime = DateTimeUtil.Parse(fileNameArr[0], "yyyy-MM-dd_H");//.AddChild(-DateTimeUtil.UtcKoreaGap);
string utcDatePath = Path.Combine(playbackPath, utcDateTime.ToString("yyyy-MM-dd"));
string utcFileName = utcDateTime.ToString("yyyy-MM-dd_H") + "." + fileNameArr[1] + "." + fileNameArr[2];
var utcFileNameArr = utcFileName.Split(".");
string utcZipFilePath = Path.Combine(tempPath, utcFileName);
string utcSqlFilePath = Path.Combine(tempPath, utcFileNameArr[0] + ".sqlite");
if (!Directory.Exists(playbackPath)) Directory.CreateDirectory(playbackPath);
if (!Directory.Exists(datePath)) Directory.CreateDirectory(datePath);
if (!Directory.Exists(utcDatePath)) Directory.CreateDirectory(utcDatePath);
if (!Directory.Exists(tempPath)) Directory.CreateDirectory(tempPath);
if (File.Exists(sqlFilePath))
{
Debug.Log($"ONREADY SQP FILE");
if (OnProgress != null) OnProgress.Invoke(100, 100, 1.0f);
if (OnComplete != null) OnComplete.Invoke(null);
return null;
}
else
{
long downloadTotal = 0;
return repository.DownloadPlaybackData(utcFileName, utcZipFilePath,
(long progress, long length) =>
{
float percent = (float)progress / (float)length;
Debug.Log($"DownloadPlaybackData OnProgress:{percent}");
if (OnProgress != null) OnProgress.Invoke(progress, length, percent / 2);
downloadTotal = length;
}, async () =>
{
await UniTask.Delay(500);
Debug.Log($"DownloadPlaybackData OnComplete");
if (File.Exists(utcZipFilePath))
{
if (OnProgress != null) OnProgress.Invoke(50, 100, 0.5f);
#if UNITY_WEBGL && !UNITY_EDITOR
string errorMessage = "Not Supported on WebGL";
#else
//압축해제 후
var zipper = new Zipper();
string errorMessage = await zipper.Decompress(utcZipFilePath, tempPath, (long read, long total, float percent) =>
{
if (OnProgress != null)
{
bool isComplte = false;
float percentRate = 0.5f + percent / 2;
if (percentRate > 0.99)
{
percentRate = 0.99f;
isComplte = true;
}
OnProgress.Invoke(downloadTotal + read, downloadTotal + total, 0.5f + percent / 2);
if (isComplte)
{
Debug.Log($" DownloadReadData :{downloadTotal + read} , DownloadTotalData :{downloadTotal + total} ,DownloadPlaybackData OnProgress:{percent}");
}
}
});
Debug.Log($"zipper1 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}");
#endif
////파일 접근 문제면 2회 0.5초 후에 다시 실행.
//if (errorMessage == "Could not open input(7z) file")
//{
// await UniTask.Delay(500);
// errorMessage = await zipper.Decompress(utcZipFilePath, tempPath, (long read, long total, float percent) =>
// {
// if (OnProgress != null) OnProgress.Invoke(downloadTotal + read, downloadTotal + total, 0.5f + percent / 2);
// });
// Debug.Log($"zipper2 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}");
//}
//if (errorMessage == "Could not open input(7z) file")
//{
// await UniTask.Delay(500);
// errorMessage = await zipper.Decompress(utcZipFilePath, tempPath, (long read, long total, float percent) =>
// {
// if (OnProgress != null) OnProgress.Invoke(downloadTotal + read, downloadTotal + total, 0.5f + percent / 2);
// });
// Debug.Log($"zipper3 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}");
//}
#if UNITY_WEBGL && !UNITY_EDITOR
// WebGL: 메인 스레드에서 직접 파일 이동/삭제
if (File.Exists(utcSqlFilePath))
{
if (File.Exists(sqlFilePath)) File.Delete(sqlFilePath);
File.Copy(utcSqlFilePath, sqlFilePath);
File.Delete(utcSqlFilePath);
}
if (File.Exists(utcZipFilePath)) File.Delete(utcZipFilePath);
#else
await UniTask.RunOnThreadPool(() =>
{
//압축해제 한 파일 이동
if (File.Exists(utcSqlFilePath))
{
//동일한 파일명이 있을경우 제거후 다시
File.Copy(utcSqlFilePath, sqlFilePath);
File.Delete(utcSqlFilePath);
}
//zip 파일 삭제
File.Delete(utcZipFilePath);
});
#endif
if (OnComplete != null) OnComplete.Invoke(errorMessage);
}
},
(string error) =>
{
Debug.Log($"DownloadPlaybackData OnError:{error}");
if (OnComplete != null) OnComplete.Invoke(error);
});
}
}
}
}