+ {getHealthIcon(data.healthStatus)}
+
+ health: {data.healthStatus}
+
+
+ );
+};
diff --git a/src/index.tsx b/src/index.tsx
index d940ed3..d2c96d2 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,6 +2,7 @@ import { serve } from "bun";
import index from "./index.html";
import { auth } from "./services/auth";
import { Broadcast } from "./services/youtube/broadcast";
+import * as LiveStream from "./services/youtube/livestream";
const server = serve({
development: process.env.NODE_ENV !== "production" && {
@@ -91,6 +92,25 @@ const server = serve({
});
},
},
+ "/livestream/health": {
+ GET: async (req) => {
+ const accessToken = await auth.api.getAccessToken({
+ body: {
+ providerId: "google",
+ },
+ headers: req.headers,
+ });
+
+ if (!accessToken.accessToken)
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
+
+ const healthStatus = await LiveStream.getHealthStatus({
+ accessToken: accessToken.accessToken,
+ });
+
+ return Response.json(healthStatus);
+ },
+ },
},
});
diff --git a/src/services/youtube/livestream.ts b/src/services/youtube/livestream.ts
new file mode 100644
index 0000000..02ee35a
--- /dev/null
+++ b/src/services/youtube/livestream.ts
@@ -0,0 +1,69 @@
+const baseUrl = "https://www.googleapis.com/youtube/v3";
+
+// https://developers.google.com/youtube/v3/live/docs/liveStreams#resource-representation
+export type LiveStream = {
+ kind: "youtube#liveStream";
+ etag: string;
+ id: string;
+ snippet: {
+ publishedAt: string; // ISO datetime
+ channelId: string;
+ title: string;
+ description: string;
+ isDefaultStream: boolean;
+ };
+ cdn: {
+ ingestionType: string;
+ ingestionInfo: {
+ streamName: string;
+ ingestionAddress: string;
+ backupIngestionAddress: string;
+ };
+ resolution: string;
+ frameRate: string;
+ };
+ status: {
+ streamStatus: string;
+ healthStatus: {
+ status: string; // "good", "ok", "bad", "noData", "revoked"
+ lastUpdateTimeSeconds: string;
+ configurationIssues: Array<{
+ type: string;
+ severity: string;
+ reason: string;
+ description: string;
+ }>;
+ };
+ };
+};
+
+export const getHealthStatus = async ({
+ accessToken,
+}: {
+ accessToken: string;
+}) => {
+ // Get live streams for the authenticated user
+ const args = new URLSearchParams({
+ part: "status",
+ mine: "true",
+ maxResults: "1",
+ });
+
+ const response = await fetch(`${baseUrl}/liveStreams?${args}`, {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error("livestream-health-status failed");
+ }
+
+ const data = await response.json();
+ const liveStream = data.items?.[0] as Pick