#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.Playback.UI; using UVC.Util; namespace UVC.Factory.Playback { /// /// Playback 관련 서비스 클래스입니다. /// - 싱글턴 패턴으로 사용합니다. /// - 재생 데이터 요청, 다운로드, 시간 스케일 조정 등 주요 기능을 제공합니다. /// /// /// /// // 재생 시작 예시 /// 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(); /// /// /// 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; private string time; private string fileName; public Action OnExitPlayback; private float timeScale = 1.0f; /// /// 재생 시간 스케일(배속)입니다. 1.0f가 기본입니다. /// /// /// /// // 재생 속도를 2배로 변경 /// PlaybackService.Instance.TimeScale = 2.0f; /// /// 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 OnChangeTimeScale; /// /// 생성자. 일반적으로 직접 호출하지 않고 싱글턴 인스턴스를 사용합니다. /// public PlaybackService(PlaybackRepository repository) { this.repository = repository; } /// /// 서버에서 재생 가능한 날짜별 데이터 목록을 비동기로 요청합니다. /// /// 성공 시 날짜별 재생 목록 딕셔너리, 실패 시 null /// /// /// var data = await PlaybackService.Instance.RequestDataAsync(); /// if (data != null) /// { /// foreach (var date in data.Keys) /// { /// Debug.Log($"날짜: {date}"); /// } /// } /// /// public async UniTask>?> RequestDataAsync() { Dictionary>? data = await repository.RequestPlaybackDateList(); return data; } /// /// 재생을 위한 기본 정보 데이터를 비동기로 처리합니다. /// /// 날짜(예: "2024-07-29") /// 시간(예: "13") /// 파일명(예: "2024-07-29_13.sqlite") /// 분(기본값: "00") /// 초(기본값: "00") /// /// /// await PlaybackService.Instance.DispatchBaseInfoData("2024-07-29", "13", "2024-07-29_13.sqlite"); /// /// public async UniTask DispatchBaseInfoData(string date, string time, string fileName, string minute = "00", string second = "00") { 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 list = await repository.SelectBySecondBaseInfo(date, fileName, formatTime); 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); } } }); } /// /// 실시간 재생 데이터(특정 초 단위)를 비동기로 처리합니다. /// /// 0 ~ 3600 (초 단위) /// 재생 속도 /// /// /// await PlaybackService.Instance.DispatchRealTimeData(120, 1); // 120초(2분) 위치 데이터 처리 /// /// public async UniTask DispatchRealTimeData(int second, int speed) { 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);//.Add(-DateTimeUtil.UtcKoreaGap); string formatTime = DateTimeUtil.FormatTime(dateTime); List 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); } } }); } /// /// 재생을 시작합니다. UI를 표시하고 데이터를 세팅합니다. /// /// 재생 목록 아이템 데이터 /// /// /// var itemData = new UIPlaybackListItemData { date = "2024-07-29", time = "13", sqlFileName = "2024-07-29_13.sqlite" }; /// await PlaybackService.Instance.StartAsync(itemData); /// /// public async Task StartAsync(UIPlaybackListItemData data) { timeScale = 1.0f; //기본 시간 스케일 설정 UIPlayback.Instance.Show(); await UIPlayback.Instance.SetData(data.date, data.time, data.sqlFileName); } /// /// 재생을 종료합니다. (이벤트 발생) /// /// /// /// PlaybackService.Instance.Exit(); /// /// public void Exit() { OnExitPlayback?.Invoke(); } /// /// 재생 데이터 파일을 준비(다운로드 및 압축 해제)합니다. /// 이미 파일이 있으면 바로 콜백을 호출합니다. /// /// 날짜(예: "2024-12-05") /// 시간(예: "13") /// 파일명(예: "2024-12-05_0.sqlite.7z") /// 진행 상황 콜백 (진행 바이트, 전체 바이트, 퍼센트) /// 완료 콜백 (에러 메시지, 성공 시 null) /// 다운로드 요청 객체(필요시 Abort 등 제어 가능), 이미 파일이 있으면 null /// /// /// // 파일 준비 및 다운로드 예시 /// 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}"); /// } /// ); /// /// public HTTPRequest? ReadyData(string date, string time, string fileName, Action OnProgress, Action 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");//.Add(-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); //압축해제 후 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}"); ////파일 접근 문제면 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}"); //} await UniTask.RunOnThreadPool(() => { //압축해제 한 파일 이동 if (File.Exists(utcSqlFilePath)) { //동일한 파일명이 있을경우 제거후 다시 File.Copy(utcSqlFilePath, sqlFilePath); File.Delete(utcSqlFilePath); } //zip 파일 삭제 File.Delete(utcZipFilePath); }); if (OnComplete != null) OnComplete.Invoke(errorMessage); } }, (string? error) => { Debug.Log($"DownloadPlaybackData OnError:{error}"); if (OnComplete != null) OnComplete.Invoke(error); }); } } } }