···1+import {
2+ getPDSServiceEndpoint,
3+ resolveDIDDocument,
4+} from "@streamplace/components";
5import { AppStore } from "store";
6import { StateCreator } from "zustand";
7import { BlueskySlice } from "./blueskySlice";
···205 });
206207 try {
208+ let targetPDS: string | null = null;
209 try {
210+ const didDoc = await resolveDIDDocument(targetDid);
211+ targetPDS = getPDSServiceEndpoint(didDoc);
212+ console.log(
213+ `[getContentMetadata] Resolved PDS for ${targetDid}:`,
214+ targetPDS,
215+ );
00000000216 } catch (pdsResolveError) {
217 console.log(
218 `[getContentMetadata] Failed to resolve PDS for ${targetDid}:`,
+15
js/app/utils/clear-query-params.ts
···000000000000000
···1+import { Platform } from "react-native";
2+3+export default function clearQueryParams(par = ["iss", "state", "code"]) {
4+ if (Platform.OS !== "web") {
5+ return;
6+ }
7+ const u = new URL(document.location.href);
8+ const params = new URLSearchParams(u.search);
9+ if (u.search === "") {
10+ return;
11+ }
12+ par.forEach((p) => params.delete(p));
13+ u.search = params.toString();
14+ window.history.replaceState(null, "", u.toString());
15+}
+3-6
js/atproto-oauth-client-react-native/README.md
···87forwarded the port with `adb reverse`. For testing on iOS hardware, you'll
88instead need to set up TLS.
8990-[react-native-quick-crypto]:
91- https://github.com/margelo/react-native-quick-crypto
92[expo-sqlite]: https://docs.expo.dev/versions/latest/sdk/sqlite/
93-[README]:
94- https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser
95-[example]:
96- https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example
···87forwarded the port with `adb reverse`. For testing on iOS hardware, you'll
88instead need to set up TLS.
8990+[react-native-quick-crypto]: https://github.com/margelo/react-native-quick-crypto
091[expo-sqlite]: https://docs.expo.dev/versions/latest/sdk/sqlite/
92+[README]: https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser
93+[example]: https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example
00
···51 [streamer] Looks like <1>@{ $handle } is offline</1>, but they recommend checking out:
52 *[default] Looks like <1>@{ $handle } is offline</1>, but we recommend checking out:
53}
54-user-offline-no-recommendations =
55 Looks like <1>@{ $handle } is offline</1> right now.
56 Check back later.
57streaming-title = streaming { $title }
···60 [1] 1 viewer
61 *[other] { $count } viewers
62}
000000000000
···51 [streamer] Looks like <1>@{ $handle } is offline</1>, but they recommend checking out:
52 *[default] Looks like <1>@{ $handle } is offline</1>, but we recommend checking out:
53}
54+user-offline-no-recommendations =
55 Looks like <1>@{ $handle } is offline</1> right now.
56 Check back later.
57streaming-title = streaming { $title }
···60 [1] 1 viewer
61 *[other] { $count } viewers
62}
63+64+## PDS Host Selector
65+pds-selector-title = New to the Atmosphere?
66+pds-selector-description = You'll need to select a PDS (Personal Data Server) to access apps on the Atmosphere, such as Bluesky, Tangled, and Spark.
67+pds-selector-custom-label = Another PDS
68+pds-selector-custom-description = Enter your own PDS host URL
69+pds-selector-custom-url-label = Custom PDS URL
70+pds-selector-custom-url-placeholder = https://pds.example.com
71+pds-selector-learn-more = Learn more about self-hosting
72+pds-selector-info = Each host has their own policies and reliability standards. Your ATProto data lives on the host you choose and you can migrate later. Note: Streamplace has its own moderation rules - you can be banned from Streamplace regardless of which host you choose.
73+pds-selector-read-policies = Read { $label }'s <tosLink>Terms of Service</tosLink> and <privacyLink>Privacy Policy</privacyLink> before continuing.
74+pds-selector-handle-policy-checkbox = I have read and agree to the <policyLink>handle policy</policyLink>
+2-1
js/components/locales/en-US/settings.ftl
···121active = Active
122123## Multistreaming
124-multistreaming = Multistreaming
125multistream-targets = Multistream Targets
126multistream-description = Automatically push your Streamplace livestreams to other streaming services like Twitch or YouTube.
127create-multistream-target = Create Multistream Target
···168no-languages-found = No languages found
169170## Branding Administration
0171branding-admin = Branding Administration
172branding-admin-description = Customize your Streamplace instance. Note that settings may take a few hours to propagate.
173branding-login-required = Please log in to manage branding
···121active = Active
122123## Multistreaming
124+multistream = Multistreaming
125multistream-targets = Multistream Targets
126multistream-description = Automatically push your Streamplace livestreams to other streaming services like Twitch or YouTube.
127create-multistream-target = Create Multistream Target
···168no-languages-found = No languages found
169170## Branding Administration
171+branding = Branding
172branding-admin = Branding Administration
173branding-admin-description = Customize your Streamplace instance. Note that settings may take a few hours to propagate.
174branding-login-required = Please log in to manage branding
···5export * from "./primitives/text";
67// Export styled components
08export * from "./button";
9export * from "./checkbox";
10export * from "./dialog";
···5export * from "./primitives/text";
67// Export styled components
8+export * from "./admonition";
9export * from "./button";
10export * from "./checkbox";
11export * from "./dialog";
···5import * as React from "react";
6import { Platform, TextInput, type TextInputProps } from "react-native";
7import { bg, borders, flex, p, text } from "../../lib/theme/atoms";
089const Textarea = React.forwardRef<TextInput, TextInputProps>(
10 ({ style, multiline = true, numberOfLines = 4, ...props }, ref) => {
011 // Detect if we're inside a bottom sheet
12 let isInBottomSheet = false;
13 try {
···38 { borderRadius: 10 },
39 style,
40 ]}
0041 multiline={multiline}
42 numberOfLines={numberOfLines}
43 textAlignVertical="top"
044 {...props}
45 />
46 );
···5import * as React from "react";
6import { Platform, TextInput, type TextInputProps } from "react-native";
7import { bg, borders, flex, p, text } from "../../lib/theme/atoms";
8+import { useTheme } from "../../ui";
910const Textarea = React.forwardRef<TextInput, TextInputProps>(
11 ({ style, multiline = true, numberOfLines = 4, ...props }, ref) => {
12+ let th = useTheme();
13 // Detect if we're inside a bottom sheet
14 let isInBottomSheet = false;
15 try {
···40 { borderRadius: 10 },
41 style,
42 ]}
43+ autoComplete={props.autoComplete || "off"}
44+ textContentType={props.textContentType || "none"}
45 multiline={multiline}
46 numberOfLines={numberOfLines}
47 textAlignVertical="top"
48+ placeholderTextColor={th.theme.colors.textMuted}
49 {...props}
50 />
51 );
+1
js/components/src/hooks/index.ts
···1// barrel file :)
02export * from "./useAvatars";
3export * from "./useCameraToggle";
4export * from "./useDocumentTitle";
···1// barrel file :)
2+export * from "./useAQState";
3export * from "./useAvatars";
4export * from "./useCameraToggle";
5export * from "./useDocumentTitle";
···34export * from "./components/chat/chat";
35export * from "./components/chat/chat-box";
36export * from "./components/chat/system-message";
037export { default as VideoRetry } from "./components/mobile-player/video-retry";
38export * from "./lib/system-messages";
3940export * from "./components/stream-notification";
41export * from "./lib/stream-notifications";
42043export * from "./utils/format-handle";
4445export { DanmuOverlay } from "./components/danmu/danmu-overlay";
···34export * from "./components/chat/chat";
35export * from "./components/chat/chat-box";
36export * from "./components/chat/system-message";
37+export * from "./components/chat/update-stream-title-dialog";
38export { default as VideoRetry } from "./components/mobile-player/video-retry";
39export * from "./lib/system-messages";
4041export * from "./components/stream-notification";
42export * from "./lib/stream-notifications";
4344+export * from "./utils/did";
45export * from "./utils/format-handle";
4647export { DanmuOverlay } from "./components/danmu/danmu-overlay";
···63 ingestAutoStart?: boolean;
64 setIngestAutoStart?: (autoStart: boolean) => void;
6500066 /** Timestamp (number) when ingest started, or null if not started */
67 ingestStarted: number | null;
68
···63 ingestAutoStart?: boolean;
64 setIngestAutoStart?: (autoStart: boolean) => void;
6566+ /** stop ingest process, again with a slight delay to allow UI to update */
67+ stopIngest: () => void;
68+69 /** Timestamp (number) when ingest started, or null if not started */
70 ingestStarted: number | null;
71
···25 });
26};
270000000000000000000000028// hook to fetch broadcaster DID (unauthenticated)
29export function useFetchBroadcasterDID() {
30 const streamplaceAgent = usePossiblyUnauthedPDSAgent();
31 const store = getStreamplaceStoreFromContext();
000000000000000000000000000000003233 return useCallback(async () => {
34 try {
···140141// hook to get a specific branding asset by key
142export function useBrandingAsset(key: string): BrandingAsset | undefined {
143- return useStreamplaceStore((state) => state.branding?.[key]);
0000144}
145146// convenience hook for main logo
···25 });
26};
2728+const PropsInHeader = [
29+ "siteTitle",
30+ "siteDescription",
31+ "primaryColor",
32+ "accentColor",
33+ "defaultStreamer",
34+ "mainLogo",
35+ "favicon",
36+ "sidebarBg",
37+ "legalLinks",
38+];
39+40+function getMetaContent(key: string): BrandingAsset | null {
41+ if (typeof window === "undefined" || !window.document) return null;
42+ const meta = document.querySelector(`meta[name="internal-brand:${key}`);
43+ if (meta && meta.getAttribute("content")) {
44+ let content = meta.getAttribute("content");
45+ if (content) return JSON.parse(content) as BrandingAsset;
46+ }
47+48+ return null;
49+}
50+51// hook to fetch broadcaster DID (unauthenticated)
52export function useFetchBroadcasterDID() {
53 const streamplaceAgent = usePossiblyUnauthedPDSAgent();
54 const store = getStreamplaceStoreFromContext();
55+56+ // prefetch from meta records, if on web
57+ useEffect(() => {
58+ if (typeof window !== "undefined" && window.document) {
59+ try {
60+ const metaRecords = PropsInHeader.reduce(
61+ (acc, key) => {
62+ const meta = document.querySelector(
63+ `meta[name="internal-brand:${key}`,
64+ );
65+ // hrmmmmmmmmmmmm
66+ if (meta && meta.getAttribute("content")) {
67+ let content = meta.getAttribute("content");
68+ if (content) acc[key] = JSON.parse(content) as BrandingAsset;
69+ }
70+ return acc;
71+ },
72+ {} as Record<string, BrandingAsset>,
73+ );
74+75+ console.log("Found meta records for broadcaster DID:", metaRecords);
76+ // filter out all non-text values, can get on second fetch?
77+ for (const key of Object.keys(metaRecords)) {
78+ if (metaRecords[key].mimeType != "text/plain") {
79+ delete metaRecords[key];
80+ }
81+ }
82+ } catch (e) {
83+ console.warn("Failed to parse broadcaster DID from meta tags", e);
84+ }
85+ }
86+ }, []);
8788 return useCallback(async () => {
89 try {
···195196// hook to get a specific branding asset by key
197export function useBrandingAsset(key: string): BrandingAsset | undefined {
198+ return (
199+ useStreamplaceStore((state) => state.branding?.[key]) ||
200+ getMetaContent(key) ||
201+ undefined
202+ );
203}
204205// convenience hook for main logo
···1+---
2+import { Card, CardGrid } from "@astrojs/starlight/components";
3+4+interface Props {
5+ searchPlaceholder?: string;
6+}
7+---
8+9+<div class="helpdesk">
10+11+ <h2>How can we help?</h2>
12+ <p>Search the knowledge base, or check out topics below.</p>
13+14+ <CardGrid>
15+ <Card title="Getting Started" icon="rocket">
16+ <p>New to Streamplace? Start here to set up your first stream.</p>
17+ <ul>
18+ <li><a href="/docs/guides/start-streaming/quick-start">Quick start guide</a></li>
19+ <li><a href="/docs/guides/start-streaming/obs">Stream with OBS</a></li>
20+ </ul>
21+ </Card>
22+23+ <Card title="Developers & Self-Hosters" icon="laptop">
24+ <p>Building with Streamplace or running your own node?</p>
25+ <ul>
26+ <li><a href="/docs/developers">Developer documentation</a></li>
27+ </ul>
28+ </Card>
29+ </CardGrid>
30+</div>
31+32+<style>
33+ .helpdesk {
34+ margin: 0 auto;
35+ }
36+37+ .helpdesk-search {
38+ margin-bottom: 2rem;
39+ }
40+41+ .search-input {
42+ width: 100%;
43+ padding: 1rem 1.5rem;
44+ font-size: 1.125rem;
45+ border: 2px solid var(--sl-color-gray-5);
46+ border-radius: 0.5rem;
47+ background: var(--sl-color-bg);
48+ color: var(--sl-color-text);
49+ transition: border-color 0.2s;
50+ }
51+52+ .search-input:focus {
53+ outline: none;
54+ border-color: var(--sl-color-accent);
55+ }
56+57+ .helpdesk h2 {
58+ margin-bottom: 1.5rem;
59+ }
60+</style>
+1-2
js/docs/src/content/docs/components/custom_ui.md
···1---
2title: Creating your own player UI
3-description:
4- How to set up your player UI with components from @streamplace/components.
5---
67# Building a Custom Player UI
···1---
2title: Creating your own player UI
3+description: How to set up your player UI with components from @streamplace/components.
04---
56# Building a Custom Player UI
+40
js/docs/src/content/docs/developers.mdx
···0000000000000000000000000000000000000000
···1+---
2+title: Developers & Self-Hosters
3+description: Build with Streamplace or run your own infrastructure.
4+template: doc
5+---
6+7+import { Card, CardGrid } from "@astrojs/starlight/components";
8+9+## Learn how to deploy, or contribute to Streamplace.
10+11+<br />
12+13+<CardGrid stagger>
14+ <Card title="Building an Application" icon="laptop">
15+ Integrate live video into your project. - [API
16+ reference](/docs/lex-reference/place-stream-defs) - [Our component
17+ library](/docs/components/custom_ui/)
18+ </Card>
19+20+{" "}
21+22+<Card title="Self-Hosting" icon="seti:config">
23+ Run your own Streamplace infrastructure. - [Installation
24+ guide](/docs/guides/installing/installing-streamplace)
25+</Card>
26+27+{" "}
28+29+<Card title="Contributing" icon="github">
30+ Help improve Streamplace. - [Development
31+ setup](/docs/guides/streamplace-dev-setup) - [Video
32+ signing](/docs/video-metadata/intro/)
33+</Card>
34+35+ <Card title="Support & Community" icon="information">
36+ Get help and connect with other developers. - [GitHub
37+ issues](https://github.com/streamplace/streamplace/issues) - [Discord
38+ community](https://discord.stream.place)
39+ </Card>
40+</CardGrid>
+3-1
js/docs/src/content/docs/features/danmu.md
···3description: Add flying bullet-style chat comments to the player, or your stream
4---
56-:::note This feature is experimental and may change in future releases. :::
0078[Danmu (or Danmaku)](https://en.wikipedia.org/wiki/Danmaku_subtitling) (ๅผนๅน,
9"bullet curtain") is a comment style where messages fly across the video
···3description: Add flying bullet-style chat comments to the player, or your stream
4---
56+:::note
7+This feature is experimental and may change in future releases.
8+:::
910[Danmu (or Danmaku)](https://en.wikipedia.org/wiki/Danmaku_subtitling) (ๅผนๅน,
11"bullet curtain") is a comment style where messages fly across the video
+27
js/docs/src/content/docs/features/embed.md
···000000000000000000000000000
···1+---
2+title: Embedding your livestream
3+description: How to embed your livestream on your website, blog, etc.
4+---
5+6+Streamplace provides an easy way to embed your livestream on any website or
7+blog.
8+9+You can access the embedded livestream page by putting `/embed` in the URL of
10+your livestream. For example, if your livestream URL is
11+`https://stream.place/iame.li`, the embed URL will be
12+`https://stream.place/embed/iame.li`.
13+14+You can use the following HTML snippet to embed your livestream:
15+16+```html
17+<iframe
18+ src="https://stream.place/embed/your-handle"
19+ width="560"
20+ height="315"
21+ frameborder="0"
22+ allowfullscreen
23+></iframe>
24+```
25+26+Alternatively, you can use the share sheet located on your livestream page.
27+Click the "Share" button, and you'll find the embed code ready to copy.
···1+---
2+title: Multistreaming
3+description: Forward your Streamplace stream to other providers.
4+---
5+6+:::note
7+This guide isn't about setting up Streamplace as an OBS destination. See [OBS Multistreaming to Streamplace](/docs/guides/start-streaming/obs-multistreaming/) for information on that.
8+:::
9+10+Multistreaming lets you forward your Streamplace stream to multiple platforms at the same time. Instead of streaming only to Streamplace, you can forward your stream to any platform that accepts RTMP input.
11+12+## Setting up multistream targets
13+14+1. Go to **Settings** > **Streaming** > **Multistream Targets**
15+2. Click **Create Multistream Target**
16+3. Enter the RTMP or RTMPS URL from your destination platform
17+4. Optionally give it a name to identify it later
18+5. Click **Create**
19+20+### Finding your multistream URL
21+22+Different platforms will provide their own RTMP URLs. Some common examples:
23+24+- **YouTube Live**: Format `rtmp://a.rtmp.youtube.com/live2/your-stream-key`
25+ - Find your stream key at https://studio.youtube.com/channel/UC/livestreaming (click the copy icon in the top right corner of the 'connect your encoder to go live' box)
26+- **Twitch**: Format `rtmp://usw20.contribute.live-video.net/app/your-stream-key`
27+ - You can get a valid RTMPS url at https://help.twitch.tv/s/twitch-ingest-recommendation
28+ - Find your stream key at https://dashboard.twitch.tv/settings/stream (your 'primary stream key')
29+30+:::note
31+Your stream key should automatically be hidden once you confirm. Make sure you've entered it correctly!
32+:::
33+34+## Managing targets during a stream
35+36+When you're live, you can see all your multistream targets on the Live Dashboard with their current status:
37+38+- **Green (Active)**: Successfully streaming to this target
39+- **Yellow (Pending)**: Connecting to this target
40+- **Red (Error)**: Connection failed; check your URL and credentials
41+- **Gray (Inactive)**: This target is disabled
42+43+You can toggle any target on or off with the switch next to its name. Changes take effect immediately.
44+45+## Limits
46+47+- **Maximum targets**: 100 total per account
48+- **Maximum active targets**: 5 simultaneous streams
49+50+### Credits
51+52+A portion of this documentation was taken from [ndroo.tv](https://bsky.app/profile/ndroo.tv)'s [guide on Streamplace](https://ndroo.tv/streamplace.html#2-configuring-your-account).
···1+---
2+title: Discord Webhooks
3+description: Configure Discord webhooks for livestream announcements and chat
4+sidebar:
5+ order: 30
6+---
7+8+Streamplace supports Discord webhooks for receiving livestream
9+notifications and chat messages. You can create, manage, and configure webhooks
10+to customize how events are delivered to your Discord channels.
11+12+## Webhook Events
13+14+You can configure webhooks to listen for specific events. For right now, the
15+following events are supported:
16+17+- `Chat`: Triggered when a chat message is sent.
18+- `Livestream`: Triggered when a livestream starts.
19+20+## Creating a Webhook
21+22+To create a webhook, go to the "Settings" page of the Streamplace web app, then
23+navigate to the "Webhooks" section. Click on "Create Webhook". The following
24+fields are required:
25+26+- Name: Webhook URL. For example,
27+ `https://discord.com/api/webhooks/{webhook.id}/{webhook.token}`
28+- Events: Select the events you want to subscribe to (e.g., `Chat Messages`,
29+ `Livestream Started`). `Livestream Started` is pre-checked by default.
30+31+We'd recommend also filling out these optional fields:
32+33+- Name: A name for the webhook (e.g., "Discord Livestream Notifications") that
34+ you can remember.
35+- Description: A description of what this webhook is for (e.g., "Sends
36+ livestream start notifications to Discord channel").
37+- Prefix: A prefix to add to each message sent by this webhook (e.g.,
38+ "[Streamplace] "). Will apply to both Chat and Livestream events!
39+- Suffix: A suffix to add to each message sent by this webhook (e.g., "is now
40+ live!"). Will apply to both Chat and Livestream events!
41+- Text replacements: A list of text replacements to apply to chat messages sent
42+ by this webhook. Each replacement consists of a "from" string and a "to"
43+ string. For example, you could replace all instances of "foo" with "bar".
44+45+After filling out the form, click "Create" to save your webhook. You should see
46+it listed in the "Webhooks" section.
47+48+## Updating a Webhook
49+50+To update a webhook, go to the "Settings" page of the Streamplace web app, then
51+navigate to the "Webhooks" section. Find the webhook you want to update and
52+click on the "pen" icon next to it. This will open the webhook edit form, where
53+you can modify the fields as needed. After making your changes, click "Update"
54+to save your changes.
55+56+## Deleting a Webhook
57+58+To delete a webhook, go to the "Settings" page of the Streamplace web app, then
59+navigate to the "Webhooks" section. Find the webhook you want to delete and
60+click on the "trash" icon next to it. A confirmation dialog will appear; click
61+"Delete" to confirm. The webhook will be removed from the list.
62+63+## Recommendations
64+65+We'd recommend:
66+67+- Creating separate Discord channels for livestream notifications and chat
68+ messages to keep them organized.
69+ - If you want to have one webhook for both chat and livestream events, you can
70+ create multiple webhooks with the same URL but different event subscriptions
71+ and prefixes/suffixes/replacements.
72+- Testing your webhook by starting a livestream or sending a chat message to
73+ ensure that notifications are being sent correctly.
74+75+## API Documentation
76+77+See these endpoint pages:
78+79+- [Create Webhook](/docs/api/operations/placestreamservercreatewebhook)
80+- [Get Webhook](/docs/api/operations/placestreamservergetwebhook)
81+- [List Webhooks](/docs/api/operations/placestreamserverlistwebhooks)
82+- [Update Webhook](/docs/api/operations/placestreamserverupdatewebhook)
83+- [Delete Webhook](/docs/api/operations/placestreamserverdeletewebhook)
···1----
2-title: Discord Webhooks
3-description: Configure Discord webhooks for livestream announcements and chat
4-sidebar:
5- order: 30
6----
7-8-Streamplace supports Discord webhook integration for receiving livestream
9-notifications and chat messages. You can create, manage, and configure webhooks
10-to customize how events are delivered to your Discord channels.
11-12-## Webhook Events
13-14-You can configure webhooks to listen for specific events. For right now, the
15-following events are supported:
16-17-- `Chat`: Triggered when a chat message is sent.
18-- `Livestream`: Triggered when a livestream starts.
19-20-## Creating a Webhook
21-22-To create a webhook, go to the "Settings" page of the Streamplace web app, then
23-navigate to the "Webhooks" section. Click on "Create Webhook". The following
24-fields are required:
25-26-- Name: Webhook URL. For example,
27- `https://discord.com/api/webhooks/{webhook.id}/{webhook.token}`
28-- Events: Select the events you want to subscribe to (e.g., `Chat Messages`,
29- `Livestream Started`). `Livestream Started` is pre-checked by default.
30-31-We'd recommend also filling out these optional fields:
32-33-- Name: A name for the webhook (e.g., "Discord Livestream Notifications") that
34- you can remember.
35-- Description: A description of what this webhook is for (e.g., "Sends
36- livestream start notifications to Discord channel").
37-- Prefix: A prefix to add to each message sent by this webhook (e.g.,
38- "[Streamplace] "). Will apply to both Chat and Livestream events!
39-- Suffix: A suffix to add to each message sent by this webhook (e.g., "is now
40- live!"). Will apply to both Chat and Livestream events!
41-- Text replacements: A list of text replacements to apply to chat messages sent
42- by this webhook. Each replacement consists of a "from" string and a "to"
43- string. For example, you could replace all instances of "foo" with "bar".
44-45-After filling out the form, click "Create" to save your webhook. You should see
46-it listed in the "Webhooks" section.
47-48-## Updating a Webhook
49-50-To update a webhook, go to the "Settings" page of the Streamplace web app, then
51-navigate to the "Webhooks" section. Find the webhook you want to update and
52-click on the "pen" icon next to it. This will open the webhook edit form, where
53-you can modify the fields as needed. After making your changes, click "Update"
54-to save your changes.
55-56-## Deleting a Webhook
57-58-To delete a webhook, go to the "Settings" page of the Streamplace web app, then
59-navigate to the "Webhooks" section. Find the webhook you want to delete and
60-click on the "trash" icon next to it. A confirmation dialog will appear; click
61-"Delete" to confirm. The webhook will be removed from the list.
62-63-## Recommendations
64-65-We'd recommend:
66-67-- Creating separate Discord channels for livestream notifications and chat
68- messages to keep them organized.
69- - If you want to have one webhook for both chat and livestream events, you can
70- create multiple webhooks with the same URL but different event subscriptions
71- and prefixes/suffixes/replacements.
72-- Testing your webhook by starting a livestream or sending a chat message to
73- ensure that notifications are being sent correctly.
74-75-## API Documentation
76-77-See these endpoint pages:
78-79-- [Create Webhook](/docs/api/operations/placestreamservercreatewebhook)
80-- [Get Webhook](/docs/api/operations/placestreamservergetwebhook)
81-- [List Webhooks](/docs/api/operations/placestreamserverlistwebhooks)
82-- [Update Webhook](/docs/api/operations/placestreamserverupdatewebhook)
83-- [Delete Webhook](/docs/api/operations/placestreamserverdeletewebhook)
···1----
2-title: Embedding your livestream
3-description: How to embed your livestream on your website, blog, etc.
4----
5-6-Streamplace provides an easy way to embed your livestream on any website or
7-blog.
8-9-You can access the embedded livestream page by putting `/embed` in the URL of
10-your livestream. For example, if your livestream URL is
11-`https://stream.place/iame.li`, the embed URL will be
12-`https://stream.place/embed/iame.li`.
13-14-You can use the following HTML snippet to embed your livestream:
15-16-```html
17-<iframe
18- src="https://stream.place/embed/your-handle"
19- width="560"
20- height="315"
21- frameborder="0"
22- allowfullscreen
23-></iframe>
24-```
25-26-Alternatively, you can use the share sheet located on your livestream page.
27-Click the "Share" button, and you'll find the embed code ready to copy.
···1---
2-title: OBS Multistreaming with Streamplace
3description:
4 Configure OBS for multistreaming to Streamplace and other platforms using the
5 obs-multi-rtmp plugin.
6sidebar:
7 order: 20
8---
000000910This guide explains how to configure Open Broadcaster Software (OBS) for
11simultaneous streaming to Streamplace and other platforms using the
···1---
2+title: OBS Multistreaming to Streamplace
3description:
4 Configure OBS for multistreaming to Streamplace and other platforms using the
5 obs-multi-rtmp plugin.
6sidebar:
7 order: 20
8---
9+10+:::note
11+This guide is not about the multistreaming feature. Check
12+[the multistreaming guide](/docs/features/multistreaming) out for more
13+information.
14+:::
1516This guide explains how to configure Open Broadcaster Software (OBS) for
17simultaneous streaming to Streamplace and other platforms using the
···58- Audio Encoder:
59 - For `RTMP`, choose an appropriate AAC encoder.
60 - For `WHIP`, use `ffmpeg_opus`.
0061- Video Encoder: _(Select appropriate encoder, e.g. libx264/nvenc_h264)_
6263#### 2e. Suggested Video Encoder Settings
64065- Rate Control: `CBR`
66-- Keyframe Interval: `1s`
00067- x264 Options: `bframes=0`
68- - If available, there also may be a 'no bframes' checkbox which should be
69 checked
70000071### 3. Announce your stream
72731. Once you're live, go back to the live dashboard.
···85862. [**Aitum Multistream Plugin**](https://aitum.tv/products/multi)
8700088## Best Practices
8990- Test your stream settings before going live
···96## Additional Resources
9798- [OBS Official Documentation](https://obsproject.com/docs/)
000000
···58- Audio Encoder:
59 - For `RTMP`, choose an appropriate AAC encoder.
60 - For `WHIP`, use `ffmpeg_opus`.
61+ - If you are using a server that supports the SRT protocol (e.g.
62+ multistreaming via NGINX) please check below for an example config.
63- Video Encoder: _(Select appropriate encoder, e.g. libx264/nvenc_h264)_
6465#### 2e. Suggested Video Encoder Settings
6667+- Video Encoder: x264/h264 (**must** be an x/h.264 encoder)
68- Rate Control: `CBR`
69+- Keyframe Interval: `1s` (or anything less than once every ~7s)
70+ - This is _one keyframe per second_
71+ - In some situations (e.g. 'keyframe interval (**frames**)'), this should be
72+ set to your FPS.
73- x264 Options: `bframes=0`
74+ - If available, there also may be a 'bframes' checkbox which should **NOT** be
75 checked
7677+:::caution
78+These last two options are very important! Your viewers' experience may be choppy or otherwise subpar if you don't have them correct.
79+:::
80+81### 3. Announce your stream
82831. Once you're live, go back to the live dashboard.
···95962. [**Aitum Multistream Plugin**](https://aitum.tv/products/multi)
9798+Alternatively, you can
99+[multistream through Streamplace itself.](/docs/features/multistreaming)
100+101## Best Practices
102103- Test your stream settings before going live
···109## Additional Resources
110111- [OBS Official Documentation](https://obsproject.com/docs/)
112+113+### Example Settings
114+115+
116+117+> Multistreaming via a server that supports the SRT protocol
···1+---
2+title: Quick Start
3+description: Get up and streaming on Streamplace quickly.
4+sidebar:
5+ order: 1
6+---
7+8+This guide gets you from zero to streaming. If you get stuck, check out the full [OBS setup guide](/docs/guides/start-streaming/obs).
9+10+:::tip
11+You will want to check out our [community guidelines](https://blog.stream.place/3mcqwibo4ks2w) first for guidance on what you can and cannot do on Streamplace.
12+:::
13+14+## So, what is Streamplace?
15+16+Streamplace is a video streaming service built on top of the AT Protocol (Authenticated Transfer Protocol), the same protocol Bluesky is built on.
17+18+## Step 1: Create your account
19+20+1. Go to [stream.place](https://stream.place)
21+2. Click "Sign in" in the top right.
22+3. Use your Atmosphere credentials to log in (ex. your Bluesky handle)
23+ - You'll need to use your actual password here - we're using OAuth so you enter your password on your PDS. We do not receive your password at all.
24+4. You're done! Your stream profile is live at `stream.place/your-handle`
25+26+## Step 2: Get your stream key
27+28+1. Click **Live Dashboard** (or go to [stream.place/dashboard](https://stream.place/dashboard))
29+2. Click **Stream from OBS**
30+3. Click **Generate Stream Key**
31+4. Your key is copied to clipboard automatically
32+33+Keep this key private. It's like a password, but for your stream.
34+35+## Step 3: Configure OBS
36+37+Open OBS and go to **Settings โ Stream**:
38+39+- **Service**: `Custom...`
40+- **Server**: `rtmps://stream.place:1935/live`
41+- **Stream Key**: Paste what you copied in Step 2
42+43+Then go to **Settings โ Output โ Streaming**:
44+45+- **Video Encoder**: `libx264` (or `NVIDIA NVENC H.264` if you have an NVIDIA GPU)
46+- **Rate Control**: `CBR`
47+- **Bitrate**: `6000` Kbps (adjust down if you drop frames)
48+- **Keyframe Interval**: `1`
49+- **x264 Options**: `bframes=0`. If there's a 'bframes' option, you'll want to have that at '0' or unchecked.
50+51+:::caution
52+These last two options are very important! Your viewers' experience may be choppy or otherwise subpar if you don't have them correct.
53+:::
54+55+## Step 4: Go live
56+57+1. In OBS, click **Start Streaming**
58+2. Go back to the Live Dashboard at stream.place
59+3. Fill in your stream title and optionally pick a thumbnail8
60+4. If needed, turn on content warnings. ("Metadata" tab in Stream Settings)
61+5. Click **Announce Livestream**
62+6. Your stream is now live and visible to the world!
63+64+## Next steps
65+66+- **Customize your chat**: Change your name color in Settings > Account
67+- **Stream to other platforms too**: Set your Twitch/YouTube URLs in Settings > Multistream Targets to push your stream there automatically. See the [Multistreaming guide](/docs/features/multistreaming) for more information
68+- **Improve stream quality**: See the [OBS guide](/docs/guides/start-streaming/obs) for encoder settings and troubleshooting
69+- **Join the Discord!**: If you need any help, or just want to chat, check out our discord at https://discord.stream.place.
70+71+### Credits
72+73+A portion of this documentation was taken from [ndroo.tv](https://bsky.app/profile/ndroo.tv)'s excellent [guide on Streamplace](https://ndroo.tv/streamplace.html#2-configuring-your-account).
···2title: Welcome to Streamplace!
3description: Begin your development journey with the Streamplace documentation.
4template: doc
5-hero:
6- tagline: Solve live video for your project with Streamplace.
7- image:
8- file: ../../assets/cube.png
9- alt: Streamplace logo. A pink 3d box viewed from a top corner.
10- actions:
11- - text: Get Started
12- link: /docs/guides/start-streaming/obs
13- icon: right-arrow
14- - text: Visit Streamplace
15- link: /
16- icon: external
17- variant: minimal
18---
1920-import { Card, CardGrid } from "@astrojs/starlight/components";
21-22-## Next Steps
2324-<CardGrid>
25- <Card title="Read the Docs" icon="open-book">
26- Learn how to start streaming with
27- [Streamplace](/docs/guides/start-streaming/obs).
28- </Card>
29- <Card title="Install Streamplace" icon="download">
30- [Run your own Streamplace
31- node](/docs/guides/installing/installing-streamplace).
32- </Card>
33- <Card title="API Reference" icon="document">
34- Explore the [Lexicon API reference](/docs/lex-reference/place-stream-defs).
35- </Card>
36- <Card title="Developer Setup" icon="setting">
37- Set up your [development environment](/docs/guides/streamplace-dev-setup).
38- </Card>
39-</CardGrid>
···2title: Welcome to Streamplace!
3description: Begin your development journey with the Streamplace documentation.
4template: doc
00000000000005---
67+import HelpDesk from "../../components/HelpDesk.astro";
0089+<HelpDesk />
000000000000000
···28- **Description:** Raw blob data with appropriate content-type
29- **Schema:**
3031-_Schema not defined._ **Possible Errors:**
03233- `BrandingNotFound`: The requested branding asset does not exist
34
···28- **Description:** Raw blob data with appropriate content-type
29- **Schema:**
3031+_Schema not defined._
32+**Possible Errors:**
3334- `BrandingNotFound`: The requested branding asset does not exist
35
···1314**Type:** `record`
1516-Record indicating a livestream is published and available for replication at a
17-given address. By convention, the record key is streamer::server
1819**Record Key:** `any`
20
···1314**Type:** `record`
1516+Record indicating a livestream is published and available for replication at a given address. By convention, the record key is streamer::server
01718**Record Key:** `any`
19
···1314**Type:** `record`
1516-Record created by a Streamplace broadcaster to indicate that they will be
17-replicating a livestream. NYI
1819**Record Key:** `tid`
20
···1314**Type:** `record`
1516+Record created by a Streamplace broadcaster to indicate that they will be replicating a livestream. NYI
01718**Record Key:** `tid`
19
···1314**Type:** `query`
1516-Find actor suggestions for a prefix search term. Expected use is for
17-auto-completion during text field entry.
1819**Parameters:**
20
···1314**Type:** `query`
1516+Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry.
01718**Parameters:**
19
···1314**Type:** `record`
1516-Default metadata record for livestream including content warnings, rights, and
17-distribution policy
1819**Record Key:** `literal:self`
20
···1314**Type:** `record`
1516+Default metadata record for livestream including content warnings, rights, and distribution policy
01718**Record Key:** `literal:self`
19
···3334**Type:** `token`
3536-All rights reserved to the creator โ others cannot use, modify, or share without
37-explicit authorization.
3839---
40···4445**Type:** `token`
4647-Public domain dedication. You waive all copyright and related rights where
48-possible. Others may copy, modify, distribute, or perform your work for any
49-purpose without attribution.
5051---
52···5657**Type:** `token`
5859-Attribution required. Others may copy, distribute, remix, and build upon your
60-work, even commercially, if they credit you.
6162---
63···6768**Type:** `token`
6970-Attribution + share-alike. Others may adapt and build upon your work, even
71-commercially, if they credit you and license their new creations under identical
72-terms.
7374---
75···7980**Type:** `token`
8182-Attribution + non-commercial. Others may adapt and build upon your work for
83-non-commercial purposes only, and must credit you.
8485---
86···9091**Type:** `token`
9293-Attribution + non-commercial + share-alike. Others may adapt and build upon your
94-work for non-commercial purposes only, must credit you, and must license their
95-new creations under identical terms.
9697---
98···102103**Type:** `token`
104105-Attribution + no derivatives. Others may reuse your work, even commercially, but
106-it must remain unchanged and you must be credited.
107108---
109···113114**Type:** `token`
115116-Attribution + non-commercial + no derivatives. Others may download and share
117-your work with credit, but cannot change it or use it commercially.
118119---
120
···3334**Type:** `token`
3536+All rights reserved to the creator โ others cannot use, modify, or share without explicit authorization.
03738---
39···4344**Type:** `token`
4546+Public domain dedication. You waive all copyright and related rights where possible. Others may copy, modify, distribute, or perform your work for any purpose without attribution.
004748---
49···5354**Type:** `token`
5556+Attribution required. Others may copy, distribute, remix, and build upon your work, even commercially, if they credit you.
05758---
59···6364**Type:** `token`
6566+Attribution + share-alike. Others may adapt and build upon your work, even commercially, if they credit you and license their new creations under identical terms.
006768---
69···7374**Type:** `token`
7576+Attribution + non-commercial. Others may adapt and build upon your work for non-commercial purposes only, and must credit you.
07778---
79···8384**Type:** `token`
8586+Attribution + non-commercial + share-alike. Others may adapt and build upon your work for non-commercial purposes only, must credit you, and must license their new creations under identical terms.
008788---
89···9394**Type:** `token`
9596+Attribution + no derivatives. Others may reuse your work, even commercially, but it must remain unchanged and you must be credited.
09798---
99···103104**Type:** `token`
105106+Attribution + non-commercial + no derivatives. Others may download and share your work with credit, but cannot change it or use it commercially.
0107108---
109
···2930**Type:** `token`
3132-The content could be perceived as offensive due to the discussion or display of
33-death.
3435---
36···4041**Type:** `token`
4243-The content contains a portrayal of the use or abuse of mind altering
44-substances.
4546---
47···5152**Type:** `token`
5354-The content contains violent actions of a fantasy nature, involving human or
55-non-human characters in situations easily distinguishable from real life.
5657---
58···6263**Type:** `token`
6465-The content contains flashing lights that could be harmful to viewers with
66-seizure disorders such as photosensitive epilepsy.
6768---
69···9394**Type:** `token`
9596-The content contains information that can be used to identify a particular
97-individual, such as a name, phone number, email address, physical address, or IP
98-address.
99100---
101···105106**Type:** `token`
107108-The content could be perceived as offensive due to the discussion or display of
109-sexuality.
110111---
112···116117**Type:** `token`
118119-The content could be perceived as distressing due to the discussion or display
120-of suffering or triggering topics, including suicide, eating disorders or self
121-harm.
122123---
124···128129**Type:** `token`
130131-The content could be perceived as offensive due to the discussion or display of
132-violence.
133134---
135
···2930**Type:** `token`
3132+The content could be perceived as offensive due to the discussion or display of death.
03334---
35···3940**Type:** `token`
4142+The content contains a portrayal of the use or abuse of mind altering substances.
04344---
45···4950**Type:** `token`
5152+The content contains violent actions of a fantasy nature, involving human or non-human characters in situations easily distinguishable from real life.
05354---
55···5960**Type:** `token`
6162+The content contains flashing lights that could be harmful to viewers with seizure disorders such as photosensitive epilepsy.
06364---
65···8990**Type:** `token`
9192+The content contains information that can be used to identify a particular individual, such as a name, phone number, email address, physical address, or IP address.
009394---
95···99100**Type:** `token`
101102+The content could be perceived as offensive due to the discussion or display of sexuality.
0103104---
105···109110**Type:** `token`
111112+The content could be perceived as distressing due to the discussion or display of suffering or triggering topics, including suicide, eating disorders or self harm.
00113114---
115···119120**Type:** `token`
121122+The content could be perceived as offensive due to the discussion or display of violence.
0123124---
125
···1314**Type:** `procedure`
1516-Create a block (ban) on behalf of a streamer. Requires 'ban' permission. Creates
17-an app.bsky.graph.block record in the streamer's repository.
1819**Parameters:** _(None defined)_
20···46**Possible Errors:**
4748- `Unauthorized`: The request lacks valid authentication credentials.
49-- `Forbidden`: The caller does not have permission to create blocks for this
50- streamer.
51-- `SessionNotFound`: The streamer's OAuth session could not be found or is
52- invalid.
5354---
55
···1314**Type:** `procedure`
1516+Create a block (ban) on behalf of a streamer. Requires 'ban' permission. Creates an app.bsky.graph.block record in the streamer's repository.
01718**Parameters:** _(None defined)_
19···45**Possible Errors:**
4647- `Unauthorized`: The request lacks valid authentication credentials.
48+- `Forbidden`: The caller does not have permission to create blocks for this streamer.
49+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
005051---
52
···1314**Type:** `procedure`
1516-Create a gate (hide message) on behalf of a streamer. Requires 'hide'
17-permission. Creates a place.stream.chat.gate record in the streamer's
18-repository.
1920**Parameters:** _(None defined)_
21···46**Possible Errors:**
4748- `Unauthorized`: The request lacks valid authentication credentials.
49-- `Forbidden`: The caller does not have permission to hide messages for this
50- streamer.
51-- `SessionNotFound`: The streamer's OAuth session could not be found or is
52- invalid.
5354---
55
···1314**Type:** `procedure`
1516+Create a gate (hide message) on behalf of a streamer. Requires 'hide' permission. Creates a place.stream.chat.gate record in the streamer's repository.
001718**Parameters:** _(None defined)_
19···44**Possible Errors:**
4546- `Unauthorized`: The request lacks valid authentication credentials.
47+- `Forbidden`: The caller does not have permission to hide messages for this streamer.
48+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
004950---
51
···1314**Type:** `procedure`
1516-Delete a block (unban) on behalf of a streamer. Requires 'ban' permission.
17-Deletes an app.bsky.graph.block record from the streamer's repository.
1819**Parameters:** _(None defined)_
20···3738**Schema Type:** `object`
3940-_(No properties defined)_ **Possible Errors:**
04142- `Unauthorized`: The request lacks valid authentication credentials.
43-- `Forbidden`: The caller does not have permission to delete blocks for this
44- streamer.
45-- `SessionNotFound`: The streamer's OAuth session could not be found or is
46- invalid.
4748---
49
···1314**Type:** `procedure`
1516+Delete a block (unban) on behalf of a streamer. Requires 'ban' permission. Deletes an app.bsky.graph.block record from the streamer's repository.
01718**Parameters:** _(None defined)_
19···3637**Schema Type:** `object`
3839+_(No properties defined)_
40+**Possible Errors:**
4142- `Unauthorized`: The request lacks valid authentication credentials.
43+- `Forbidden`: The caller does not have permission to delete blocks for this streamer.
44+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
004546---
47
···1314**Type:** `procedure`
1516-Delete a gate (unhide message) on behalf of a streamer. Requires 'hide'
17-permission. Deletes a place.stream.chat.gate record from the streamer's
18-repository.
1920**Parameters:** _(None defined)_
21···3839**Schema Type:** `object`
4041-_(No properties defined)_ **Possible Errors:**
04243- `Unauthorized`: The request lacks valid authentication credentials.
44-- `Forbidden`: The caller does not have permission to unhide messages for this
45- streamer.
46-- `SessionNotFound`: The streamer's OAuth session could not be found or is
47- invalid.
4849---
50
···1314**Type:** `procedure`
1516+Delete a gate (unhide message) on behalf of a streamer. Requires 'hide' permission. Deletes a place.stream.chat.gate record from the streamer's repository.
001718**Parameters:** _(None defined)_
19···3637**Schema Type:** `object`
3839+_(No properties defined)_
40+**Possible Errors:**
4142- `Unauthorized`: The request lacks valid authentication credentials.
43+- `Forbidden`: The caller does not have permission to unhide messages for this streamer.
44+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
004546---
47
···1314**Type:** `procedure`
1516-Update livestream metadata on behalf of a streamer. Requires 'livestream.manage'
17-permission. Updates a place.stream.livestream record in the streamer's
18-repository.
1920**Parameters:** _(None defined)_
21···47**Possible Errors:**
4849- `Unauthorized`: The request lacks valid authentication credentials.
50-- `Forbidden`: The caller does not have permission to update livestream metadata
51- for this streamer.
52-- `SessionNotFound`: The streamer's OAuth session could not be found or is
53- invalid.
54- `RecordNotFound`: The specified livestream record does not exist.
5556---
···1314**Type:** `procedure`
1516+Update livestream metadata on behalf of a streamer. Requires 'livestream.manage' permission. Updates a place.stream.livestream record in the streamer's repository.
001718**Parameters:** _(None defined)_
19···45**Possible Errors:**
4647- `Unauthorized`: The request lacks valid authentication credentials.
48+- `Forbidden`: The caller does not have permission to update livestream metadata for this streamer.
49+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
0050- `RecordNotFound`: The specified livestream record does not exist.
5152---
···1956 ]
1957 }
1958 },
000000000000000000000000000000000000000000000000000000000000000000000000001959 "/xrpc/com.atproto.sync.listRepos": {
1960 "get": {
1961 "summary": "Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.",
···1956 ]
1957 }
1958 },
1959+ "/xrpc/com.atproto.sync.getRepo": {
1960+ "get": {
1961+ "summary": "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.",
1962+ "operationId": "com.atproto.sync.getRepo",
1963+ "tags": ["com.atproto.sync"],
1964+ "responses": {
1965+ "200": {
1966+ "description": "Success",
1967+ "content": {
1968+ "application/vnd.ipld.car": {
1969+ "schema": {}
1970+ }
1971+ }
1972+ },
1973+ "400": {
1974+ "description": "Bad Request",
1975+ "content": {
1976+ "application/json": {
1977+ "schema": {
1978+ "type": "object",
1979+ "required": ["error", "message"],
1980+ "properties": {
1981+ "error": {
1982+ "type": "string",
1983+ "oneOf": [
1984+ {
1985+ "const": "RepoNotFound"
1986+ },
1987+ {
1988+ "const": "RepoTakendown"
1989+ },
1990+ {
1991+ "const": "RepoSuspended"
1992+ },
1993+ {
1994+ "const": "RepoDeactivated"
1995+ }
1996+ ]
1997+ },
1998+ "message": {
1999+ "type": "string"
2000+ }
2001+ }
2002+ }
2003+ }
2004+ }
2005+ }
2006+ },
2007+ "parameters": [
2008+ {
2009+ "name": "did",
2010+ "in": "query",
2011+ "required": true,
2012+ "description": "The DID of the repo.",
2013+ "schema": {
2014+ "type": "string",
2015+ "description": "The DID of the repo.",
2016+ "format": "did"
2017+ }
2018+ },
2019+ {
2020+ "name": "since",
2021+ "in": "query",
2022+ "required": false,
2023+ "description": "The revision ('rev') of the repo to create a diff from.",
2024+ "schema": {
2025+ "type": "string",
2026+ "description": "The revision ('rev') of the repo to create a diff from.",
2027+ "format": "tid"
2028+ }
2029+ }
2030+ ]
2031+ }
2032+ },
2033 "/xrpc/com.atproto.sync.listRepos": {
2034 "get": {
2035 "summary": "Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.",
+9
js/docs/src/content/docs/reference.mdx
···000000000
···1+---
2+title: API Reference
3+description: Our XRPC and OpenAPI Reference documentation
4+template: doc
5+---
6+7+import { Card, CardGrid } from "@astrojs/starlight/components";
8+9+Here contains our XRPC and OpenAPI Reference documentation.
···56 Build *BuildFlags
57 DataDir string
58 DBURL string
059 EthAccountAddr string
60 EthKeystorePath string
61 EthPassword string
···140 SegmentDebugDir string
141 AdminDIDs []string
142 Syndicate []string
0143}
144145// ContentFilters represents the content filtering configuration
···240 fs.BoolVar(&cli.BehindHTTPSProxy, "behind-https-proxy", false, "set to true if this node is behind an https proxy and we should report https URLs even though the node isn't serving HTTPS")
241 cli.StringSliceFlag(fs, &cli.AdminDIDs, "admin-dids", []string{}, "comma-separated list of DIDs that are authorized to modify branding and other admin operations")
242 cli.StringSliceFlag(fs, &cli.Syndicate, "syndicate", []string{}, "list of DIDs that we should rebroadcast ('*' for everybody)")
000243244 fs.Bool("external-signing", true, "DEPRECATED, does nothing.")
245 fs.Bool("insecure", false, "DEPRECATED, does nothing.")
···56 Build *BuildFlags
57 DataDir string
58 DBURL string
59+ LocalDBURL string
60 EthAccountAddr string
61 EthKeystorePath string
62 EthPassword string
···141 SegmentDebugDir string
142 AdminDIDs []string
143 Syndicate []string
144+ PlayerTelemetry bool
145}
146147// ContentFilters represents the content filtering configuration
···242 fs.BoolVar(&cli.BehindHTTPSProxy, "behind-https-proxy", false, "set to true if this node is behind an https proxy and we should report https URLs even though the node isn't serving HTTPS")
243 cli.StringSliceFlag(fs, &cli.AdminDIDs, "admin-dids", []string{}, "comma-separated list of DIDs that are authorized to modify branding and other admin operations")
244 cli.StringSliceFlag(fs, &cli.Syndicate, "syndicate", []string{}, "list of DIDs that we should rebroadcast ('*' for everybody)")
245+ fs.BoolVar(&cli.PlayerTelemetry, "player-telemetry", true, "enable player telemetry")
246+ fs.StringVar(&cli.LocalDBURL, "local-db-url", "sqlite://$SP_DATA_DIR/localdb.sqlite", "URL of the local database to use for storing local data")
247+ cli.dataDirFlags = append(cli.dataDirFlags, &cli.LocalDBURL)
248249 fs.Bool("external-signing", true, "DEPRECATED, does nothing.")
250 fs.Bool("insecure", false, "DEPRECATED, does nothing.")
···1package model
2-3-import (
4- "context"
5- "database/sql/driver"
6- "encoding/json"
7- "errors"
8- "fmt"
9- "time"
10-11- "gorm.io/gorm"
12- "stream.place/streamplace/pkg/aqtime"
13- "stream.place/streamplace/pkg/log"
14- "stream.place/streamplace/pkg/streamplace"
15-)
16-17-type SegmentMediadataVideo struct {
18- Width int `json:"width"`
19- Height int `json:"height"`
20- FPSNum int `json:"fpsNum"`
21- FPSDen int `json:"fpsDen"`
22- BFrames bool `json:"bframes"`
23-}
24-25-type SegmentMediadataAudio struct {
26- Rate int `json:"rate"`
27- Channels int `json:"channels"`
28-}
29-30-type SegmentMediaData struct {
31- Video []*SegmentMediadataVideo `json:"video"`
32- Audio []*SegmentMediadataAudio `json:"audio"`
33- Duration int64 `json:"duration"`
34- Size int `json:"size"`
35-}
36-37-// Scan scan value into Jsonb, implements sql.Scanner interface
38-func (j *SegmentMediaData) Scan(value any) error {
39- bytes, ok := value.([]byte)
40- if !ok {
41- return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
42- }
43-44- result := SegmentMediaData{}
45- err := json.Unmarshal(bytes, &result)
46- *j = SegmentMediaData(result)
47- return err
48-}
49-50-// Value return json value, implement driver.Valuer interface
51-func (j SegmentMediaData) Value() (driver.Value, error) {
52- return json.Marshal(j)
53-}
54-55-// ContentRights represents content rights and attribution information
56-type ContentRights struct {
57- CopyrightNotice *string `json:"copyrightNotice,omitempty"`
58- CopyrightYear *int64 `json:"copyrightYear,omitempty"`
59- Creator *string `json:"creator,omitempty"`
60- CreditLine *string `json:"creditLine,omitempty"`
61- License *string `json:"license,omitempty"`
62-}
63-64-// Scan scan value into ContentRights, implements sql.Scanner interface
65-func (c *ContentRights) Scan(value any) error {
66- if value == nil {
67- *c = ContentRights{}
68- return nil
69- }
70- bytes, ok := value.([]byte)
71- if !ok {
72- return errors.New(fmt.Sprint("Failed to unmarshal ContentRights value:", value))
73- }
74-75- result := ContentRights{}
76- err := json.Unmarshal(bytes, &result)
77- *c = ContentRights(result)
78- return err
79-}
80-81-// Value return json value, implement driver.Valuer interface
82-func (c ContentRights) Value() (driver.Value, error) {
83- return json.Marshal(c)
84-}
85-86-// DistributionPolicy represents distribution policy information
87-type DistributionPolicy struct {
88- DeleteAfterSeconds *int64 `json:"deleteAfterSeconds,omitempty"`
89-}
90-91-// Scan scan value into DistributionPolicy, implements sql.Scanner interface
92-func (d *DistributionPolicy) Scan(value any) error {
93- if value == nil {
94- *d = DistributionPolicy{}
95- return nil
96- }
97- bytes, ok := value.([]byte)
98- if !ok {
99- return errors.New(fmt.Sprint("Failed to unmarshal DistributionPolicy value:", value))
100- }
101-102- result := DistributionPolicy{}
103- err := json.Unmarshal(bytes, &result)
104- *d = DistributionPolicy(result)
105- return err
106-}
107-108-// Value return json value, implement driver.Valuer interface
109-func (d DistributionPolicy) Value() (driver.Value, error) {
110- return json.Marshal(d)
111-}
112-113-// ContentWarningsSlice is a custom type for storing content warnings as JSON in the database
114-type ContentWarningsSlice []string
115-116-// Scan scan value into ContentWarningsSlice, implements sql.Scanner interface
117-func (c *ContentWarningsSlice) Scan(value any) error {
118- if value == nil {
119- *c = ContentWarningsSlice{}
120- return nil
121- }
122- bytes, ok := value.([]byte)
123- if !ok {
124- return errors.New(fmt.Sprint("Failed to unmarshal ContentWarningsSlice value:", value))
125- }
126-127- result := ContentWarningsSlice{}
128- err := json.Unmarshal(bytes, &result)
129- *c = ContentWarningsSlice(result)
130- return err
131-}
132-133-// Value return json value, implement driver.Valuer interface
134-func (c ContentWarningsSlice) Value() (driver.Value, error) {
135- return json.Marshal(c)
136-}
137-138-type Segment struct {
139- ID string `json:"id" gorm:"primaryKey"`
140- SigningKeyDID string `json:"signingKeyDID" gorm:"column:signing_key_did"`
141- SigningKey *SigningKey `json:"signingKey,omitempty" gorm:"foreignKey:DID;references:SigningKeyDID"`
142- StartTime time.Time `json:"startTime" gorm:"index:latest_segments,priority:2;index:start_time"`
143- RepoDID string `json:"repoDID" gorm:"index:latest_segments,priority:1;column:repo_did"`
144- Repo *Repo `json:"repo,omitempty" gorm:"foreignKey:DID;references:RepoDID"`
145- Title string `json:"title"`
146- Size int `json:"size" gorm:"column:size"`
147- MediaData *SegmentMediaData `json:"mediaData,omitempty"`
148- ContentWarnings ContentWarningsSlice `json:"contentWarnings,omitempty"`
149- ContentRights *ContentRights `json:"contentRights,omitempty"`
150- DistributionPolicy *DistributionPolicy `json:"distributionPolicy,omitempty"`
151- DeleteAfter *time.Time `json:"deleteAfter,omitempty" gorm:"column:delete_after;index:delete_after"`
152-}
153-154-func (s *Segment) ToStreamplaceSegment() (*streamplace.Segment, error) {
155- aqt := aqtime.FromTime(s.StartTime)
156- if s.MediaData == nil {
157- return nil, fmt.Errorf("media data is nil")
158- }
159- if len(s.MediaData.Video) == 0 || s.MediaData.Video[0] == nil {
160- return nil, fmt.Errorf("video data is nil")
161- }
162- if len(s.MediaData.Audio) == 0 || s.MediaData.Audio[0] == nil {
163- return nil, fmt.Errorf("audio data is nil")
164- }
165- duration := s.MediaData.Duration
166- sizei64 := int64(s.Size)
167-168- // Convert model metadata to streamplace metadata
169- var contentRights *streamplace.MetadataContentRights
170- if s.ContentRights != nil {
171- contentRights = &streamplace.MetadataContentRights{
172- CopyrightNotice: s.ContentRights.CopyrightNotice,
173- CopyrightYear: s.ContentRights.CopyrightYear,
174- Creator: s.ContentRights.Creator,
175- CreditLine: s.ContentRights.CreditLine,
176- License: s.ContentRights.License,
177- }
178- }
179-180- var contentWarnings *streamplace.MetadataContentWarnings
181- if len(s.ContentWarnings) > 0 {
182- contentWarnings = &streamplace.MetadataContentWarnings{
183- Warnings: []string(s.ContentWarnings),
184- }
185- }
186-187- var distributionPolicy *streamplace.MetadataDistributionPolicy
188- if s.DistributionPolicy != nil && s.DistributionPolicy.DeleteAfterSeconds != nil {
189- distributionPolicy = &streamplace.MetadataDistributionPolicy{
190- DeleteAfter: s.DistributionPolicy.DeleteAfterSeconds,
191- }
192- }
193-194- return &streamplace.Segment{
195- LexiconTypeID: "place.stream.segment",
196- Creator: s.RepoDID,
197- Id: s.ID,
198- SigningKey: s.SigningKeyDID,
199- StartTime: string(aqt),
200- Duration: &duration,
201- Size: &sizei64,
202- ContentRights: contentRights,
203- ContentWarnings: contentWarnings,
204- DistributionPolicy: distributionPolicy,
205- Video: []*streamplace.Segment_Video{
206- {
207- Codec: "h264",
208- Width: int64(s.MediaData.Video[0].Width),
209- Height: int64(s.MediaData.Video[0].Height),
210- Framerate: &streamplace.Segment_Framerate{
211- Num: int64(s.MediaData.Video[0].FPSNum),
212- Den: int64(s.MediaData.Video[0].FPSDen),
213- },
214- Bframes: &s.MediaData.Video[0].BFrames,
215- },
216- },
217- Audio: []*streamplace.Segment_Audio{
218- {
219- Codec: "opus",
220- Rate: int64(s.MediaData.Audio[0].Rate),
221- Channels: int64(s.MediaData.Audio[0].Channels),
222- },
223- },
224- }, nil
225-}
226-227-func (m *DBModel) CreateSegment(seg *Segment) error {
228- err := m.DB.Model(Segment{}).Create(seg).Error
229- if err != nil {
230- return err
231- }
232- return nil
233-}
234-235-// should return the most recent segment for each user, ordered by most recent first
236-// only includes segments from the last 30 seconds
237-func (m *DBModel) MostRecentSegments() ([]Segment, error) {
238- var segments []Segment
239- thirtySecondsAgo := time.Now().Add(-30 * time.Second)
240-241- err := m.DB.Table("segments").
242- Select("segments.*").
243- Where("start_time > ?", thirtySecondsAgo.UTC()).
244- Order("start_time DESC").
245- Find(&segments).Error
246- if err != nil {
247- return nil, err
248- }
249- if segments == nil {
250- return []Segment{}, nil
251- }
252-253- segmentMap := make(map[string]Segment)
254- for _, seg := range segments {
255- prev, ok := segmentMap[seg.RepoDID]
256- if !ok {
257- segmentMap[seg.RepoDID] = seg
258- } else {
259- if seg.StartTime.After(prev.StartTime) {
260- segmentMap[seg.RepoDID] = seg
261- }
262- }
263- }
264-265- filteredSegments := []Segment{}
266- for _, seg := range segmentMap {
267- filteredSegments = append(filteredSegments, seg)
268- }
269-270- return filteredSegments, nil
271-}
272-273-func (m *DBModel) LatestSegmentForUser(user string) (*Segment, error) {
274- var seg Segment
275- err := m.DB.Model(Segment{}).Where("repo_did = ?", user).Order("start_time DESC").First(&seg).Error
276- if err != nil {
277- return nil, err
278- }
279- return &seg, nil
280-}
281-282-func (m *DBModel) FilterLiveRepoDIDs(repoDIDs []string) ([]string, error) {
283- if len(repoDIDs) == 0 {
284- return []string{}, nil
285- }
286-287- thirtySecondsAgo := time.Now().Add(-30 * time.Second)
288-289- var liveDIDs []string
290-291- err := m.DB.Table("segments").
292- Select("DISTINCT repo_did").
293- Where("repo_did IN ? AND start_time > ?", repoDIDs, thirtySecondsAgo.UTC()).
294- Pluck("repo_did", &liveDIDs).Error
295-296- if err != nil {
297- return nil, err
298- }
299-300- return liveDIDs, nil
301-}
302-303-func (m *DBModel) LatestSegmentsForUser(user string, limit int, before *time.Time, after *time.Time) ([]Segment, error) {
304- var segs []Segment
305- if before == nil {
306- later := time.Now().Add(1000 * time.Hour)
307- before = &later
308- }
309- if after == nil {
310- earlier := time.Time{}
311- after = &earlier
312- }
313- err := m.DB.Model(Segment{}).Where("repo_did = ? AND start_time < ? AND start_time > ?", user, before.UTC(), after.UTC()).Order("start_time DESC").Limit(limit).Find(&segs).Error
314- if err != nil {
315- return nil, err
316- }
317- return segs, nil
318-}
319-320-func (m *DBModel) GetSegment(id string) (*Segment, error) {
321- var seg Segment
322-323- err := m.DB.Model(&Segment{}).
324- Preload("Repo").
325- Where("id = ?", id).
326- First(&seg).Error
327-328- if errors.Is(err, gorm.ErrRecordNotFound) {
329- return nil, nil
330- }
331- if err != nil {
332- return nil, err
333- }
334-335- return &seg, nil
336-}
337-338-func (m *DBModel) GetExpiredSegments(ctx context.Context) ([]Segment, error) {
339-340- var expiredSegments []Segment
341- now := time.Now()
342- err := m.DB.
343- Where("delete_after IS NOT NULL AND delete_after < ?", now.UTC()).
344- Find(&expiredSegments).Error
345- if err != nil {
346- return nil, err
347- }
348-349- return expiredSegments, nil
350-}
351-352-func (m *DBModel) DeleteSegment(ctx context.Context, id string) error {
353- return m.DB.Delete(&Segment{}, "id = ?", id).Error
354-}
355-356-func (m *DBModel) StartSegmentCleaner(ctx context.Context) error {
357- err := m.SegmentCleaner(ctx)
358- if err != nil {
359- return err
360- }
361- ticker := time.NewTicker(1 * time.Minute)
362- defer ticker.Stop()
363-364- for {
365- select {
366- case <-ctx.Done():
367- return nil
368- case <-ticker.C:
369- err := m.SegmentCleaner(ctx)
370- if err != nil {
371- log.Error(ctx, "Failed to clean segments", "error", err)
372- }
373- }
374- }
375-}
376-377-func (m *DBModel) SegmentCleaner(ctx context.Context) error {
378- // Calculate the cutoff time (10 minutes ago)
379- cutoffTime := aqtime.FromTime(time.Now().Add(-10 * time.Minute)).Time()
380-381- // Find all unique repo_did values
382- var repoDIDs []string
383- if err := m.DB.Model(&Segment{}).Distinct("repo_did").Pluck("repo_did", &repoDIDs).Error; err != nil {
384- log.Error(ctx, "Failed to get unique repo_dids for segment cleaning", "error", err)
385- return err
386- }
387-388- // For each user, keep their last 10 segments and delete older ones
389- for _, repoDID := range repoDIDs {
390- // Get IDs of the last 10 segments for this user
391- var keepSegmentIDs []string
392- if err := m.DB.Model(&Segment{}).
393- Where("repo_did = ?", repoDID).
394- Order("start_time DESC").
395- Limit(10).
396- Pluck("id", &keepSegmentIDs).Error; err != nil {
397- log.Error(ctx, "Failed to get segment IDs to keep", "repo_did", repoDID, "error", err)
398- return err
399- }
400-401- // Delete old segments except the ones we want to keep
402- result := m.DB.Where("repo_did = ? AND start_time < ? AND id NOT IN ?",
403- repoDID, cutoffTime, keepSegmentIDs).Delete(&Segment{})
404-405- if result.Error != nil {
406- log.Error(ctx, "Failed to clean old segments", "repo_did", repoDID, "error", result.Error)
407- } else if result.RowsAffected > 0 {
408- log.Log(ctx, "Cleaned old segments", "repo_did", repoDID, "count", result.RowsAffected)
409- }
410- }
411- return nil
412-}
···1package model
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000