diff --git a/js/components/src/components/mobile-player/use-webrtc.tsx b/js/components/src/components/mobile-player/use-webrtc.tsx index 3d66caf39..e81227e9b 100644 --- a/js/components/src/components/mobile-player/use-webrtc.tsx +++ b/js/components/src/components/mobile-player/use-webrtc.tsx @@ -20,6 +20,9 @@ export default function useWebRTC( const setStatus = usePlayerStore((x) => x.setStatus); let agent = usePossiblyUnauthedPDSAgent(); const oauthSession = useStreamplaceStore((state) => state.oauthSession); + const playbackWorkerUrl = useStreamplaceStore( + (state) => state.playbackWorkerUrl, + ); const lastChange = useRef(0); @@ -64,6 +67,7 @@ export default function useWebRTC( undefined, agent, oauthSession, + playbackWorkerUrl, ); }); @@ -102,7 +106,7 @@ export default function useWebRTC( clearInterval(handle); peerConnection.close(); }; - }, [streamer, agent, oauthSession]); + }, [streamer, agent, oauthSession, playbackWorkerUrl]); return [mediaStream, stuck]; } @@ -124,6 +128,7 @@ export async function negotiateConnectionWithClientOffer( bearerToken?: string, agent?: StreamplaceAgent, oauthSession?: SessionManager | null, + playbackWorkerUrl?: string | null, ) { /** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer */ const offer = await peerConnection.createOffer({ @@ -162,6 +167,7 @@ export async function negotiateConnectionWithClientOffer( bearerToken, agent, oauthSession, + playbackWorkerUrl, ); let text = new TextDecoder().decode(response.data); if (response.success) { @@ -249,14 +255,14 @@ async function getPlaybackServerAgent( agent: StreamplaceAgent, oauthSession: SessionManager | null | undefined, streamer: string, + playbackWorkerUrl?: string | null, ): Promise { - const workerUrl = (window as any).PLAYBACK_WORKER_URL as string | undefined; - if (!workerUrl) { + if (!playbackWorkerUrl) { return agent; } try { - const lookupAgent = new StreamplaceAgent(workerUrl); + const lookupAgent = new StreamplaceAgent(playbackWorkerUrl); const res = await lookupAgent.place.stream.playback.getPlaybackServer({ stream: streamer, }); @@ -277,6 +283,7 @@ async function postSDPOffer( bearerToken?: string, agent?: StreamplaceAgent, oauthSession?: SessionManager | null, + playbackWorkerUrl?: string | null, ) { if (!agent) { throw new Error("No agent found"); @@ -285,6 +292,7 @@ async function postSDPOffer( agent, oauthSession, streamer, + playbackWorkerUrl, ); return await playbackAgent.place.stream.playback.whep(data, { qp: { diff --git a/js/components/src/streamplace-provider/index.tsx b/js/components/src/streamplace-provider/index.tsx index df9af2961..1554d057a 100644 --- a/js/components/src/streamplace-provider/index.tsx +++ b/js/components/src/streamplace-provider/index.tsx @@ -5,6 +5,7 @@ import { useDocumentTitle } from "../hooks"; import { useBrandingAutoFetch, useFetchBroadcasterDID, + useFetchEnvConfig, useGetChatProfile, } from "../streamplace-store"; import { makeStreamplaceStore } from "../streamplace-store/streamplace-store"; @@ -46,12 +47,14 @@ export function StreamplaceProvider({ export function BrandingFetcher({ children }: { children: React.ReactNode }) { const fetchBroadcasterDID = useFetchBroadcasterDID(); + const fetchEnvConfig = useFetchEnvConfig(); useBrandingAutoFetch(); useDocumentTitle(); useEffect(() => { fetchBroadcasterDID(); - }, [fetchBroadcasterDID]); + fetchEnvConfig(); + }, [fetchBroadcasterDID, fetchEnvConfig]); return <>{children}; } diff --git a/js/components/src/streamplace-store/branding.tsx b/js/components/src/streamplace-store/branding.tsx index dc882cea1..6cf1ccb3b 100644 --- a/js/components/src/streamplace-store/branding.tsx +++ b/js/components/src/streamplace-store/branding.tsx @@ -105,6 +105,26 @@ export function useFetchBroadcasterDID() { }, [streamplaceAgent, store]); } +// hook to fetch client-facing env config from the server +export function useFetchEnvConfig() { + const streamplaceAgent = usePossiblyUnauthedPDSAgent(); + const store = getStreamplaceStoreFromContext(); + + return useCallback(async () => { + try { + if (!streamplaceAgent) { + throw new Error("Streamplace agent not available"); + } + const result = await streamplaceAgent.place.stream.config.getEnv(); + if (result.data.playbackWorkerUrl) { + store.setState({ playbackWorkerUrl: result.data.playbackWorkerUrl }); + } + } catch (err) { + console.error("Failed to fetch env config:", err); + } + }, [streamplaceAgent, store]); +} + // hook to fetch branding data from the server export function useFetchBranding() { const streamplaceAgent = usePossiblyUnauthedPDSAgent(); diff --git a/js/components/src/streamplace-store/streamplace-store.tsx b/js/components/src/streamplace-store/streamplace-store.tsx index beaa6488c..3a3912a67 100644 --- a/js/components/src/streamplace-store/streamplace-store.tsx +++ b/js/components/src/streamplace-store/streamplace-store.tsx @@ -59,6 +59,9 @@ export interface StreamplaceState { adminDIDs: string[]; setAdminDIDs: (adminDIDs: string[]) => void; + playbackWorkerUrl: string | null; + setPlaybackWorkerUrl: (url: string | null) => void; + // Branding state branding: Record | null; brandingLoading: boolean; @@ -131,6 +134,10 @@ export const makeStreamplaceStore = ({ adminDIDs: [], setAdminDIDs: (adminDIDs: string[]) => set({ adminDIDs }), + playbackWorkerUrl: null, + setPlaybackWorkerUrl: (playbackWorkerUrl: string | null) => + set({ playbackWorkerUrl }), + // Content metadata contentMetadata: null, setContentMetadata: (metadata) => set({ contentMetadata: metadata }), diff --git a/js/docs/src/content/docs/lex-reference/config/place-stream-config-getenv.md b/js/docs/src/content/docs/lex-reference/config/place-stream-config-getenv.md new file mode 100644 index 000000000..32348956f --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/config/place-stream-config-getenv.md @@ -0,0 +1,65 @@ +--- +title: place.stream.config.getEnv +description: Reference for the place.stream.config.getEnv lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `query` + +Get client-facing environment configuration from the server. + +**Parameters:** _(None defined)_ + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ------------------- | -------- | ----- | -------------------------------------------- | ----------- | +| `playbackWorkerUrl` | `string` | ❌ | URL of the Cloudflare playback router worker | | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.config.getEnv", + "defs": { + "main": { + "type": "query", + "description": "Get client-facing environment configuration from the server.", + "parameters": { + "type": "params", + "required": [], + "properties": {} + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [], + "properties": { + "playbackWorkerUrl": { + "type": "string", + "description": "URL of the Cloudflare playback router worker" + } + } + } + }, + "errors": [] + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/openapi.json b/js/docs/src/content/docs/lex-reference/openapi.json index c6143cc97..4f3bb86b6 100644 --- a/js/docs/src/content/docs/lex-reference/openapi.json +++ b/js/docs/src/content/docs/lex-reference/openapi.json @@ -1967,6 +1967,31 @@ ] } }, + "/xrpc/place.stream.config.getEnv": { + "get": { + "summary": "Get client-facing environment configuration from the server.", + "operationId": "place.stream.config.getEnv", + "tags": ["place.stream.config"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "playbackWorkerUrl": { + "type": "string", + "description": "URL of the Cloudflare playback router worker" + } + } + } + } + } + } + } + } + }, "/xrpc/place.stream.broadcast.getBroadcaster": { "get": { "summary": "Get information about a Streamplace broadcaster.", diff --git a/lexicons/place/stream/config/getEnv.json b/lexicons/place/stream/config/getEnv.json new file mode 100644 index 000000000..ac0098064 --- /dev/null +++ b/lexicons/place/stream/config/getEnv.json @@ -0,0 +1,29 @@ +{ + "lexicon": 1, + "id": "place.stream.config.getEnv", + "defs": { + "main": { + "type": "query", + "description": "Get client-facing environment configuration from the server.", + "parameters": { + "type": "params", + "required": [], + "properties": {} + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [], + "properties": { + "playbackWorkerUrl": { + "type": "string", + "description": "URL of the Cloudflare playback router worker" + } + } + } + }, + "errors": [] + } + } +} diff --git a/pkg/linking/linking.go b/pkg/linking/linking.go index dc59383dd..b89dd9f66 100644 --- a/pkg/linking/linking.go +++ b/pkg/linking/linking.go @@ -31,11 +31,10 @@ func NewLinker(ctx context.Context, baseHTML []byte, sdb *statedb.StatefulDB, cl } type PageConfig struct { - Title string - Metas []MetaTag - SentryDSN string - PlaybackWorkerURL string - Branding []string + Title string + Metas []MetaTag + SentryDSN string + Branding []string } // Define all meta tags in a structured way @@ -170,15 +169,11 @@ func (l *Linker) GenerateStreamerCard(ctx context.Context, u *url.URL, lsv *stre Content: fmt.Sprintf("%s%s", titleStr, brandingTitle), }) - pc := &PageConfig{ + return l.GenerateHTML(ctx, &PageConfig{ Title: fmt.Sprintf("%s%s", titleStr, brandingTitle), Metas: metaTags, SentryDSN: sentryDSN, - } - if l.cli != nil { - pc.PlaybackWorkerURL = l.cli.PlaybackWorkerURL - } - return l.GenerateHTML(ctx, pc) + }) } func (l *Linker) GenerateDefaultCard(ctx context.Context, u *url.URL, sentryDSN string) ([]byte, error) { @@ -248,15 +243,11 @@ func (l *Linker) GenerateDefaultCard(ctx context.Context, u *url.URL, sentryDSN Content: brandingTitle, }) - pc := &PageConfig{ + return l.GenerateHTML(ctx, &PageConfig{ Title: brandingTitle, Metas: metaTags, SentryDSN: sentryDSN, - } - if l.cli != nil { - pc.PlaybackWorkerURL = l.cli.PlaybackWorkerURL - } - return l.GenerateHTML(ctx, pc) + }) } func (l *Linker) GenerateHTML(ctx context.Context, pc *PageConfig) ([]byte, error) { @@ -323,15 +314,8 @@ func (l *Linker) GenerateHTML(ctx context.Context, pc *PageConfig) ([]byte, erro }) } - // Add injected config script - var configScript string + // Add Sentry DSN script if configured if pc.SentryDSN != "" { - configScript += `window.SENTRY_DSN = "` + pc.SentryDSN + `";` - } - if pc.PlaybackWorkerURL != "" { - configScript += `window.PLAYBACK_WORKER_URL = "` + pc.PlaybackWorkerURL + `";` - } - if configScript != "" { script := &html.Node{ Type: html.ElementNode, Data: "script", @@ -339,7 +323,7 @@ func (l *Linker) GenerateHTML(ctx context.Context, pc *PageConfig) ([]byte, erro head.AppendChild(script) script.AppendChild(&html.Node{ Type: html.TextNode, - Data: configScript, + Data: `window.SENTRY_DSN = "` + pc.SentryDSN + `";`, }) } diff --git a/pkg/spxrpc/place_stream_config.go b/pkg/spxrpc/place_stream_config.go new file mode 100644 index 000000000..992f9c8f2 --- /dev/null +++ b/pkg/spxrpc/place_stream_config.go @@ -0,0 +1,15 @@ +package spxrpc + +import ( + "context" + + placestream "stream.place/streamplace/pkg/streamplace" +) + +func (s *Server) handlePlaceStreamConfigGetEnv(ctx context.Context) (*placestream.ConfigGetEnv_Output, error) { + out := &placestream.ConfigGetEnv_Output{} + if s.cli.PlaybackWorkerURL != "" { + out.PlaybackWorkerUrl = &s.cli.PlaybackWorkerURL + } + return out, nil +} diff --git a/pkg/spxrpc/stubs.go b/pkg/spxrpc/stubs.go index 2ddcf76a3..bba8588e7 100644 --- a/pkg/spxrpc/stubs.go +++ b/pkg/spxrpc/stubs.go @@ -284,6 +284,7 @@ func (s *Server) RegisterHandlersPlaceStream(e *echo.Echo) error { e.GET("/xrpc/place.stream.branding.getBranding", s.HandlePlaceStreamBrandingGetBranding) e.POST("/xrpc/place.stream.branding.updateBlob", s.HandlePlaceStreamBrandingUpdateBlob) e.GET("/xrpc/place.stream.broadcast.getBroadcaster", s.HandlePlaceStreamBroadcastGetBroadcaster) + e.GET("/xrpc/place.stream.config.getEnv", s.HandlePlaceStreamConfigGetEnv) e.GET("/xrpc/place.stream.graph.getFollowingUser", s.HandlePlaceStreamGraphGetFollowingUser) e.GET("/xrpc/place.stream.ingest.getIngestUrls", s.HandlePlaceStreamIngestGetIngestUrls) e.POST("/xrpc/place.stream.live.denyTeleport", s.HandlePlaceStreamLiveDenyTeleport) @@ -408,6 +409,19 @@ func (s *Server) HandlePlaceStreamBroadcastGetBroadcaster(c echo.Context) error return c.JSON(200, out) } +func (s *Server) HandlePlaceStreamConfigGetEnv(c echo.Context) error { + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamConfigGetEnv") + defer span.End() + var out *placestream.ConfigGetEnv_Output + var handleErr error + // func (s *Server) handlePlaceStreamConfigGetEnv(ctx context.Context) (*placestream.ConfigGetEnv_Output, error) + out, handleErr = s.handlePlaceStreamConfigGetEnv(ctx) + if handleErr != nil { + return handleErr + } + return c.JSON(200, out) +} + func (s *Server) HandlePlaceStreamGraphGetFollowingUser(c echo.Context) error { ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamGraphGetFollowingUser") defer span.End() diff --git a/pkg/streamplace/configgetEnv.go b/pkg/streamplace/configgetEnv.go new file mode 100644 index 000000000..86911af40 --- /dev/null +++ b/pkg/streamplace/configgetEnv.go @@ -0,0 +1,29 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: place.stream.config.getEnv + +package streamplace + +import ( + "context" + + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +// ConfigGetEnv_Output is the output of a place.stream.config.getEnv call. +type ConfigGetEnv_Output struct { + // playbackWorkerUrl: URL of the Cloudflare playback router worker + PlaybackWorkerUrl *string `json:"playbackWorkerUrl,omitempty" cborgen:"playbackWorkerUrl,omitempty"` +} + +// ConfigGetEnv calls the XRPC method "place.stream.config.getEnv". +func ConfigGetEnv(ctx context.Context, c lexutil.LexClient) (*ConfigGetEnv_Output, error) { + var out ConfigGetEnv_Output + + params := map[string]interface{}{} + if err := c.LexDo(ctx, lexutil.Query, "", "place.stream.config.getEnv", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +}