DreamineVMS.Web 1.0.0.0
DreamineVMS 에이전트가 제공하는 HLS 카메라 영상을 브라우저에서 관리·재생하는 원격 CCTV 웹 서비스입니다.
로딩중...
검색중...
일치하는것 없음
HlsSegmentStore.cs
이 파일의 문서화 페이지로 가기
1using System.IO;
2
4
13public sealed class HlsSegmentStore
14{
23 private const int RetainedSegmentCount = 45;
32 private static readonly TimeSpan RetainedSegmentAge = TimeSpan.FromMinutes(2);
33
42 private readonly string _root;
43
60 public HlsSegmentStore(IWebHostEnvironment env)
61 {
62 _root = Path.Combine(env.ContentRootPath, "App_Data", "hls");
63 Directory.CreateDirectory(_root);
64 }
65
98 public string GetCameraDir(string tenantId, string cameraId)
99 {
100 var dir = Path.Combine(_root, Sanitize(tenantId), Sanitize(cameraId));
101 Directory.CreateDirectory(dir);
102 return dir;
103 }
104
153 public async Task SaveSegmentAsync(string tenantId, string cameraId, string filename, Stream data)
154 {
155 if (!IsAllowedFilename(filename)) return;
156 var dir = GetCameraDir(tenantId, cameraId);
157 var path = Path.Combine(dir, filename);
158
159 // 임시 파일에 쓴 뒤 원자적 rename → 읽기 중 충돌 방지
160 var tmp = path + ".tmp";
161 await using (var fs = new FileStream(tmp, FileMode.Create, FileAccess.Write, FileShare.Read))
162 {
163 await data.CopyToAsync(fs);
164 }
165
166 File.Move(tmp, path, overwrite: true);
167
168 if (filename.EndsWith(".m3u8", StringComparison.OrdinalIgnoreCase))
170 }
171
212 public (Stream? Stream, string ContentType) GetFile(string tenantId, string cameraId, string filename)
213 {
214 if (!IsAllowedFilename(filename)) return (null, "");
215 var path = Path.Combine(_root, Sanitize(tenantId), Sanitize(cameraId), filename);
216 if (!File.Exists(path)) return (null, "");
217 var ct = filename.EndsWith(".m3u8", StringComparison.OrdinalIgnoreCase)
218 ? "application/vnd.apple.mpegurl"
219 : "video/mp2t";
220 try
221 {
222 // MemoryStream으로 읽어 파일 핸들 즉시 해제 → 쓰기와 동시 접근 허용
223 using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
224 var memory = new MemoryStream((int)Math.Min(fs.Length, int.MaxValue));
225 fs.CopyTo(memory);
226 memory.Position = 0;
227 return (memory, ct);
228 }
229 catch { return (null, ""); }
230 }
231
256 public void ClearCameraHls(string tenantId, string cameraId)
257 {
258 try
259 {
260 var dir = Path.Combine(_root, Sanitize(tenantId), Sanitize(cameraId));
261 if (!Directory.Exists(dir)) return;
262 foreach (var f in Directory.GetFiles(dir))
263 try { File.Delete(f); } catch { }
264 }
265 catch { }
266 }
267
268 // 라이브 시청자가 네트워크 지연으로 뒤처져도 필요한 세그먼트를 받을 수 있도록
269 // 개수와 시간을 모두 고려해서 완만하게 정리합니다.
286 private static void CleanupOldSegments(string dir)
287 {
288 try
289 {
290 var thresholdUtc = DateTime.UtcNow - RetainedSegmentAge;
291 var segments = Directory.GetFiles(dir, "*.ts")
292 .OrderBy(File.GetLastWriteTimeUtc)
293 .ToArray();
294
295 foreach (var f in segments.Take(Math.Max(0, segments.Length - RetainedSegmentCount)))
296 {
297 try { File.Delete(f); } catch { }
298 }
299
300 foreach (var f in segments.Where(f => File.GetLastWriteTimeUtc(f) < thresholdUtc))
301 {
302 try { File.Delete(f); } catch { }
303 }
304 }
305 catch { }
306 }
307
332 private static string Sanitize(string s) =>
333 string.Concat(s.Where(c => char.IsLetterOrDigit(c) || c is '-' or '_'));
334
359 private static bool IsAllowedFilename(string f) =>
360 f.EndsWith(".m3u8", StringComparison.OrdinalIgnoreCase) ||
361 f.EndsWith(".ts", StringComparison.OrdinalIgnoreCase);
362}
Stream? string ContentType GetFile(string tenantId, string cameraId, string filename)
string GetCameraDir(string tenantId, string cameraId)
async Task SaveSegmentAsync(string tenantId, string cameraId, string filename, Stream data)
void ClearCameraHls(string tenantId, string cameraId)