420 lines
20 KiB
C#
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|