···1export * from "./block";
2export * from "./branding";
03export * from "./moderation";
4export * from "./moderator-management";
5export * from "./stream";
···1export * from "./block";
2export * from "./branding";
3+export * from "./ingest";
4export * from "./moderation";
5export * from "./moderator-management";
6export * from "./stream";
+32
js/components/src/streamplace-store/ingest.tsx
···00000000000000000000000000000000
···1+import { PlaceStreamIngestDefs } from "streamplace";
2+import { useDID, useStreamplaceStore } from "./streamplace-store";
3+import { usePDSAgent } from "./xrpc";
4+5+export default function useGetIngests() {
6+ const pdsAgent = usePDSAgent();
7+ const did = useDID();
8+ const setIngests = useStreamplaceStore((state) => state.setIngests);
9+10+ return async () => {
11+ if (!pdsAgent || !did) {
12+ throw new Error("No PDS agent or DID available");
13+ }
14+15+ const result = await pdsAgent.place.stream.ingest.getIngestUrls();
16+ if (!result.success) {
17+ throw new Error("Failed to get ingests");
18+ }
19+20+ const ingests = result.data.ingests
21+ .map((ingest) => {
22+ if (PlaceStreamIngestDefs.isIngest(ingest)) {
23+ return ingest;
24+ }
25+ console.error("Invalid ingest", ingest);
26+ return null;
27+ })
28+ .filter((ingest) => ingest !== null);
29+30+ setIngests(ingests);
31+ };
32+}
···1683 }
1684 }
1685 },
000000000000000000000000000000001686 "/xrpc/place.stream.graph.getFollowingUser": {
1687 "get": {
1688 "summary": "Get whether or not user A is following user B.",
···3599 "type": "string",
3600 "format": "byte",
3601 "description": "MP4 file of a user's signed livestream segment"
00000000000000003602 },
3603 "com.atproto.repo.strongRef": {
3604 "type": "object",
···1683 }
1684 }
1685 },
1686+ "/xrpc/place.stream.ingest.getIngestUrls": {
1687+ "get": {
1688+ "summary": "Get ingest URLs for a Streamplace station.",
1689+ "operationId": "place.stream.ingest.getIngestUrls",
1690+ "tags": ["place.stream.ingest"],
1691+ "responses": {
1692+ "200": {
1693+ "description": "Success",
1694+ "content": {
1695+ "application/json": {
1696+ "schema": {
1697+ "type": "object",
1698+ "properties": {
1699+ "ingests": {
1700+ "type": "array",
1701+ "items": {
1702+ "oneOf": [
1703+ {
1704+ "$ref": "#/components/schemas/place.stream.ingest.defs_ingest"
1705+ }
1706+ ]
1707+ }
1708+ }
1709+ },
1710+ "required": ["ingests"]
1711+ }
1712+ }
1713+ }
1714+ }
1715+ }
1716+ }
1717+ },
1718 "/xrpc/place.stream.graph.getFollowingUser": {
1719 "get": {
1720 "summary": "Get whether or not user A is following user B.",
···3631 "type": "string",
3632 "format": "byte",
3633 "description": "MP4 file of a user's signed livestream segment"
3634+ },
3635+ "place.stream.ingest.defs_ingest": {
3636+ "type": "object",
3637+ "description": "An ingest URL for a Streamplace station.",
3638+ "properties": {
3639+ "type": {
3640+ "type": "string",
3641+ "description": "The type of ingest endpoint, currently 'rtmp' and 'whip' are supported."
3642+ },
3643+ "url": {
3644+ "type": "string",
3645+ "description": "The URL of the ingest endpoint.",
3646+ "format": "uri"
3647+ }
3648+ },
3649+ "required": ["type", "url"]
3650 },
3651 "com.atproto.repo.strongRef": {
3652 "type": "object",
+22
lexicons/place/stream/ingest/defs.json
···0000000000000000000000
···1+{
2+ "lexicon": 1,
3+ "id": "place.stream.ingest.defs",
4+ "defs": {
5+ "ingest": {
6+ "type": "object",
7+ "description": "An ingest URL for a Streamplace station.",
8+ "required": ["type", "url"],
9+ "properties": {
10+ "type": {
11+ "type": "string",
12+ "description": "The type of ingest endpoint, currently 'rtmp' and 'whip' are supported."
13+ },
14+ "url": {
15+ "type": "string",
16+ "format": "uri",
17+ "description": "The URL of the ingest endpoint."
18+ }
19+ }
20+ }
21+ }
22+}
···392 // if we check after exactly rec.IdleTimeoutSeconds we might miss the finalization by a few seconds
393 scheduledAt = scheduledAt.Add((time.Duration(*rec.IdleTimeoutSeconds) * time.Second) + (10 * time.Second)).UTC()
394 taskKey := fmt.Sprintf("finalize-livestream::%s::%s", aturi.String(), scheduledAt.Format(util.ISO8601))
395- log.Warn(ctx, "queueing stream finalization task", "taskKey", taskKey, "scheduledAt", scheduledAt)
396 _, err = atsync.StatefulDB.EnqueueTask(ctx, statedb.TaskFinalizeLivestream, task, statedb.WithTaskKey(taskKey), statedb.WithScheduledAt(scheduledAt))
397 if err != nil {
398 return fmt.Errorf("failed to enqueue remove red circle task: %w", err)
···392 // if we check after exactly rec.IdleTimeoutSeconds we might miss the finalization by a few seconds
393 scheduledAt = scheduledAt.Add((time.Duration(*rec.IdleTimeoutSeconds) * time.Second) + (10 * time.Second)).UTC()
394 taskKey := fmt.Sprintf("finalize-livestream::%s::%s", aturi.String(), scheduledAt.Format(util.ISO8601))
0395 _, err = atsync.StatefulDB.EnqueueTask(ctx, statedb.TaskFinalizeLivestream, task, statedb.WithTaskKey(taskKey), statedb.WithScheduledAt(scheduledAt))
396 if err != nil {
397 return fmt.Errorf("failed to enqueue remove red circle task: %w", err)
···1+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2+3+// Lexicon schema: place.stream.ingest.defs
4+5+package streamplace
6+7+// IngestDefs_Ingest is a "ingest" in the place.stream.ingest.defs schema.
8+//
9+// An ingest URL for a Streamplace station.
10+type IngestDefs_Ingest struct {
11+ LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.ingest.defs#ingest"`
12+ // type: The type of ingest endpoint, currently 'rtmp' and 'whip' are supported.
13+ Type string `json:"type" cborgen:"type"`
14+ // url: The URL of the ingest endpoint.
15+ Url string `json:"url" cborgen:"url"`
16+}