+
diff --git a/src/pages/providers/index.css b/src/pages/providers/index.css
new file mode 100644
index 0000000..2214a22
--- /dev/null
+++ b/src/pages/providers/index.css
@@ -0,0 +1,35 @@
+.stream-providers-page {
+ padding: 40px;
+}
+
+.stream-providers-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 12px;
+}
+
+.stream-providers-grid>div {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.stream-providers-grid>div img {
+ height: 64px;
+}
+
+.owncast-config {
+ display: flex;
+ gap: 16px;
+ padding: 40px;
+}
+
+.owncast-config>div {
+ flex: 1;
+}
+
+.owncast-config>div:nth-child(2) {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
\ No newline at end of file
diff --git a/src/pages/providers/index.tsx b/src/pages/providers/index.tsx
new file mode 100644
index 0000000..0bb1537
--- /dev/null
+++ b/src/pages/providers/index.tsx
@@ -0,0 +1,57 @@
+import "./index.css";
+import { StreamProviders } from "providers";
+
+import Owncast from "owncast.png";
+import Cloudflare from "cloudflare.png";
+import { useNavigate, useParams } from "react-router-dom";
+import { ConfigureOwncast } from "./owncast";
+
+export function StreamProvidersPage() {
+ const navigate = useNavigate();
+ const { id } = useParams();
+
+ function mapName(p: StreamProviders) {
+ switch (p) {
+ case StreamProviders.Owncast: return "Owncast"
+ case StreamProviders.Cloudflare: return "Cloudflare"
+ }
+ return "Unknown"
+ }
+
+ function mapLogo(p: StreamProviders) {
+ switch (p) {
+ case StreamProviders.Owncast: return

+ case StreamProviders.Cloudflare: return

+ }
+ }
+
+ function providerLink(p: StreamProviders) {
+ return
+
{mapName(p)}
+ {mapLogo(p)}
+
+
+ }
+
+ function index() {
+ return
+
Providers
+
Stream providers streamline the process of streaming on Nostr, some event accept lightning payments!
+
+ {[StreamProviders.Owncast, StreamProviders.Cloudflare].map(v => providerLink(v))}
+
+
+ }
+
+ if (!id) {
+ return index();
+ } else {
+ switch (id) {
+ case StreamProviders.Owncast: {
+ return
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/pages/providers/owncast.tsx b/src/pages/providers/owncast.tsx
new file mode 100644
index 0000000..b9a7054
--- /dev/null
+++ b/src/pages/providers/owncast.tsx
@@ -0,0 +1,86 @@
+import AsyncButton from "element/async-button";
+import { StatePill } from "element/state-pill";
+import { StreamState } from "index";
+import { StreamProviderInfo } from "providers";
+import { OwncastProvider } from "providers/owncast";
+import { useState } from "react";
+
+export function ConfigureOwncast() {
+ const [url, setUrl] = useState("");
+ const [token, setToken] = useState("");
+ const [info, setInfo] = useState
();
+
+ async function tryConnect() {
+ try {
+ const api = new OwncastProvider(url, token);
+ const i = await api.info();
+ setInfo(i);
+ }
+ catch (e) {
+ console.debug(e);
+ }
+ }
+
+ function status() {
+ if (!info) return;
+
+ return <>
+ Status
+
+
+
+
+
Name
+
+ {info?.name}
+
+
+ {info?.summary &&
+
Summary
+
+ {info?.summary}
+
+
}
+ {info?.viewers &&
+
Viewers
+
+ {info?.viewers}
+
+
}
+ {info?.version &&
+
Version
+
+ {info?.version}
+
+
}
+
+
+
+ >
+ }
+
+ return
+
+
+
Owncast instance url
+
+ setUrl(e.target.value)} />
+
+
+
+
API token
+
+ setToken(e.target.value)} />
+
+
+
+ Connect
+
+
+
+ {status()}
+
+
+}
\ No newline at end of file
diff --git a/src/pages/stream-page.tsx b/src/pages/stream-page.tsx
index 4db320d..c14f746 100644
--- a/src/pages/stream-page.tsx
+++ b/src/pages/stream-page.tsx
@@ -16,17 +16,18 @@ import { SendZapsDialog } from "element/send-zap";
import type { NostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { NewStreamDialog } from "element/new-stream";
+import { StatePill } from "element/state-pill";
function ProfileInfo({ link }: { link: NostrLink }) {
const thisEvent = useEventFeed(link, true);
const login = useLogin();
const navigate = useNavigate();
- const profile = useUserProfile(System, thisEvent.data?.pubkey);
+ const host = thisEvent.data?.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? thisEvent.data?.pubkey;
+ const profile = useUserProfile(System, host);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const status = findTag(thisEvent.data, "status");
const start = findTag(thisEvent.data, "starts");
- const isLive = status === "live";
const isMine = link.author === login?.pubkey;
async function deleteStream() {
@@ -46,7 +47,7 @@ function ProfileInfo({ link }: { link: NostrLink }) {
{findTag(thisEvent.data, "title")}
{findTag(thisEvent.data, "summary")}
- {status}
+
{status === StreamState.Planned && (
Starts {moment(Number(start) * 1000).fromNow()}
@@ -77,7 +78,7 @@ function ProfileInfo({ link }: { link: NostrLink }) {
)}
-
+
{zapTarget && thisEvent.data && (
-
diff --git a/src/providers/index.ts b/src/providers/index.ts
new file mode 100644
index 0000000..7c8c344
--- /dev/null
+++ b/src/providers/index.ts
@@ -0,0 +1,26 @@
+import { StreamState } from "index"
+
+export interface StreamProvider {
+ /**
+ * Get general info about connected provider to test everything is working
+ */
+ info(): Promise
+
+ /**
+ * Create a config object to save in localStorage
+ */
+ createConfig(): any & { type: StreamProviders }
+}
+
+export enum StreamProviders {
+ Owncast = "owncast",
+ Cloudflare = "cloudflare"
+}
+
+export interface StreamProviderInfo {
+ name: string
+ summary?: string
+ version?: string
+ state: StreamState
+ viewers: number
+}
diff --git a/src/providers/owncast.ts b/src/providers/owncast.ts
new file mode 100644
index 0000000..3ebca09
--- /dev/null
+++ b/src/providers/owncast.ts
@@ -0,0 +1,66 @@
+import { StreamState } from "index";
+import { StreamProvider, StreamProviderInfo, StreamProviders } from "providers";
+
+export class OwncastProvider implements StreamProvider {
+ #url: string
+ #token: string
+
+ constructor(url: string, token: string) {
+ this.#url = url;
+ this.#token = token;
+ }
+
+ createConfig(): any & { type: StreamProviders; } {
+ return {
+ type: StreamProviders.Owncast,
+ url: this.#url,
+ token: this.#token
+ }
+ }
+
+ async info() {
+ const info = await this.#getJson("GET", "/api/config");
+ const status = await this.#getJson("GET", "/api/status");
+ return {
+ name: info.name,
+ summary: info.summary,
+ version: info.version,
+ state: status.online ? StreamState.Live : StreamState.Ended,
+ viewers: status.viewerCount
+ } as StreamProviderInfo
+ }
+
+ async #getJson(method: "GET" | "POST", path: string, body?: unknown): Promise {
+ const rsp = await fetch(`${this.#url}${path}`, {
+ method: method,
+ body: body ? JSON.stringify(body) : undefined,
+ headers: {
+ "content-type": "application/json",
+ "authorization": `Bearer ${this.#token}`
+ },
+ });
+ const json = await rsp.text();
+ if (!rsp.ok) {
+ throw new Error(json);
+ }
+ return JSON.parse(json) as T;
+ }
+
+}
+
+interface ConfigResponse {
+ name?: string,
+ summary?: string,
+ logo?: string,
+ tags?: Array,
+ version?: string
+}
+
+interface StatusResponse {
+ lastConnectTime?: string
+ lastDisconnectTime?: string
+ online: boolean
+ overallMaxViewerCount: number
+ sessionMaxViewerCount: number
+ viewerCount: number
+}
\ No newline at end of file