using System; using System.IO; using ytLive.Helpers; using ytLive.Services; namespace ytLive.ViewModels; /// /// TASK 65, slice 4: the creator-facing half of replay clips — the setting, the ring's lifetime, and F10. /// /// Clips are a **static feature**: 10 or 15 seconds, keep 3/4/5, no editor and no gallery. Off by /// default, and when off nothing runs — no ring, no reader thread, no encoder output — so the feature /// costs nothing until someone asks for it. /// public partial class MainViewModel { private ClipRing? _clipRing; private ClipLibrary? _clipLibrary; public bool ClipsEnabled { get => _clipsEnabled; set { if (!SetProperty(ref _clipsEnabled, value)) return; _layoutStore.SaveClipsEnabled(value); ApplyClipSettings(); } } private bool _clipsEnabled; /// 10 or 15 seconds; anything else in the store is coerced by the loader. public int ClipSeconds { get => _clipSeconds; set { var clamped = value == 15 ? 15 : 10; if (!SetProperty(ref _clipSeconds, clamped)) return; _layoutStore.SaveClipSeconds(clamped); ApplyClipSettings(); } } private int _clipSeconds = 10; public int ClipKeep { get => _clipKeep; set { var clamped = Math.Clamp(value, 3, 5); if (!SetProperty(ref _clipKeep, clamped)) return; _layoutStore.SaveClipKeep(clamped); } } private int _clipKeep = 3; /// String-faced pickers, because the drawer binds a ComboBox and the model keeps ints. public string[] ClipLengthOptions { get; } = { "10 seconds", "15 seconds" }; public string ClipLengthText { get => ClipSeconds == 15 ? "15 seconds" : "10 seconds"; set { ClipSeconds = value == "15 seconds" ? 15 : 10; OnPropertyChanged(nameof(ClipLengthText)); } } public string[] ClipKeepOptions { get; } = { "3 clips", "4 clips", "5 clips" }; public string ClipKeepText { get => $"{ClipKeep} clips"; set { if (int.TryParse(value.Split(' ')[0], out var keep)) ClipKeep = keep; OnPropertyChanged(nameof(ClipKeepText)); } } private void LoadClipSettings() { _clipsEnabled = _layoutStore.LoadClipsEnabled(); _clipSeconds = _layoutStore.LoadClipSeconds(); _clipKeep = _layoutStore.LoadClipKeep(); } /// The ring exists only while a session runs and clips are on. Sized from the chosen length /// and the tier's bitrate, because memory is the cost and bytes are the unit. private void ApplyClipSettings() { if (!_clipsEnabled || !(IsLive || IsRecording)) { _clipRing = null; return; } var quality = Models.OutputProfile.Resolve(SelectedQuality, QualityOptions, HasProOutput); var bytes = ClipLibrary.RingBytesFor(_clipSeconds, (int)Math.Round(quality.Bitrate * 1000)); if (_clipRing is null || _clipRingCapacity != bytes) { _clipRing = new ClipRing(bytes); _clipRingCapacity = bytes; } } private long _clipRingCapacity; private void StartClipRing() { _clipLibrary = new ClipLibrary(() => _recordFolder ?? DefaultRecordFolder()); _clipRing = null; _clipRingCapacity = 0; ApplyClipSettings(); OnPropertyChanged(nameof(ClipsEnabled)); OnPropertyChanged(nameof(ClipSeconds)); OnPropertyChanged(nameof(ClipKeep)); } private void StopClipRing() { _clipRing = null; _clipRingCapacity = 0; } /// F10. Snapshots the ring, writes a file, and prunes to the retention count. When nothing /// has been buffered yet it says so instead of writing a file that will not open. public void SaveReplayClip() { if (!_clipsEnabled) { _notifications.Info("Replay clips are off", "Turn them on in Settings to save a clip."); return; } var bytes = _clipRing?.Snapshot() ?? Array.Empty(); if (bytes.Length == 0) { _notifications.Warning("Nothing to clip yet", "The replay buffer fills while you stream — wait a few seconds, then press again."); return; } var library = _clipLibrary ??= new ClipLibrary(() => _recordFolder ?? DefaultRecordFolder()); var path = library.Save(bytes, DateTime.Now, _clipKeep); if (path == null) { _notifications.Error("Couldn't save the clip", "The file could not be written. Check free space."); return; } AppLog.Write($"Replay clip saved: {path}"); _notifications.Success("Clip saved", Path.GetFileName(path)); ClipSaved?.Invoke(path); } /// Raised with the saved path so the replay layer can play it immediately. public event Action? ClipSaved; }