Initial scaffold: ytLive C# WPF project

- MVVM architecture (Models, ViewModels, Views, Services, Helpers)
- Models: Scene, Source, StreamConfig, StreamHealth, YouTubeChannel, ChatMessage
- Services: YouTubeAuthService (OAuth2), YouTubeStreamService (broadcasts/stream health),
  YouTubeChatService (live chat polling)
- ViewModels: MainViewModel with scene management, stream controls, chat
- MainWindow with dark theme: scene/source panel, preview area, chat panel, status bar
- Version roadmap: v0.1 (scenes, RTMP, OAuth2, chat, health) -> 1.0
This commit is contained in:
2026-08-04 09:09:52 -07:00
commit 9103ff1fb7
16 changed files with 943 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using ytLive.Models;
namespace ytLive.Services;
/// <summary>
/// Manages YouTube live stream lifecycle — create broadcasts,
/// bind stream keys, monitor health.
/// </summary>
public class YouTubeStreamService
{
private readonly YouTubeAuthService _auth;
private readonly HttpClient _http = new();
private const string ApiBase = "https://www.googleapis.com/youtube/v3";
public YouTubeStreamService(YouTubeAuthService auth)
{
_auth = auth;
}
private async Task<bool> EnsureToken()
{
if (_auth.CurrentChannel == null) return false;
if (_auth.CurrentChannel.TokenExpiry <= DateTime.UtcNow.AddMinutes(5))
return await _auth.RefreshToken();
return true;
}
public async Task<string?> 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
{
privacyStatus = "public",
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<JsonElement>(json);
return data.GetProperty("id").GetString();
}
public async Task<string?> 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<JsonElement>(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<StreamHealth?> 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<JsonElement>(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
};
}
}