using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using ytLive.Models; namespace ytLive.Services; /// /// Manages YouTube live stream lifecycle — create broadcasts, /// bind stream keys, monitor health. /// public class YouTubeStreamService { private readonly YouTubeAuthService _auth; private readonly HttpClient _http; private const string ApiBase = "https://www.googleapis.com/youtube/v3"; public YouTubeStreamService(YouTubeAuthService auth, HttpClient? http = null) { _auth = auth; _http = http ?? new HttpClient(); } private async Task EnsureToken() { if (_auth.CurrentChannel == null) return false; if (_auth.CurrentChannel.TokenExpiry <= DateTime.UtcNow.AddMinutes(5)) return await _auth.RefreshToken(); return true; } public async Task CreateBroadcast(string title, string description, DateTime scheduledStartTime) { if (!await EnsureToken()) return null; var broadcast = new { snippet = new { title, description, scheduledStartTime = scheduledStartTime.ToString("o"), categoryId = "22" // People & Blogs }, status = new { // Private-only by enforcement (ship step 7) — the Go Live dialog // is locked to Private and the service refuses anything else. privacyStatus = "private", selfDeclaredMadeForKids = false }, contentDetails = new { enableAutoStart = true, enableAutoStop = true } }; _http.DefaultRequestHeaders.Authorization = new("Bearer", _auth.CurrentChannel!.AccessToken); var response = await _http.PostAsJsonAsync( $"{ApiBase}/liveBroadcasts?part=snippet,status,contentDetails", broadcast); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(); var data = JsonSerializer.Deserialize(json); return data.GetProperty("id").GetString(); } public async Task BindStream(string broadcastId, string streamKey) { if (!await EnsureToken()) return null; // Create stream resource var stream = new { snippet = new { title = $"stream-{broadcastId}" }, contentDetails = new { ingestionType = "rtmp", frameRate = "60fps", resolution = "1080p" } }; _http.DefaultRequestHeaders.Authorization = new("Bearer", _auth.CurrentChannel!.AccessToken); var response = await _http.PostAsJsonAsync( $"{ApiBase}/liveStreams?part=snippet,contentDetails", stream); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(); var data = JsonSerializer.Deserialize(json); var streamId = data.GetProperty("id").GetString(); // Bind stream to broadcast var bindResponse = await _http.PutAsJsonAsync( $"{ApiBase}/liveBroadcasts?part=id,contentDetails&id={broadcastId}", new { contentDetails = new { streamId } }); return bindResponse.IsSuccessStatusCode ? streamId : null; } public async Task GetStreamHealth(string broadcastId) { if (!await EnsureToken()) return null; _http.DefaultRequestHeaders.Authorization = new("Bearer", _auth.CurrentChannel!.AccessToken); var response = await _http.GetAsync( $"{ApiBase}/liveBroadcasts?part=contentDetails,status&id={broadcastId}"); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(); var data = JsonSerializer.Deserialize(json); var items = data.GetProperty("items"); if (items.GetArrayLength() == 0) return null; var status = items[0].GetProperty("status").GetProperty("lifeCycleStatus").GetString(); return new StreamHealth { HealthStatus = status switch { "live" => "good", "ready" => "ok", "created" => "ok", _ => "bad" }, HealthMessage = status }; } }