diff --git a/js/docs/src/content/docs/lex-reference/openapi.json b/js/docs/src/content/docs/lex-reference/openapi.json index 927e08b8..ef53b659 100644 --- a/js/docs/src/content/docs/lex-reference/openapi.json +++ b/js/docs/src/content/docs/lex-reference/openapi.json @@ -688,14 +688,13 @@ }, "parameters": [ { - "name": "did", + "name": "streamer", "in": "query", "required": true, - "description": "DID of the streamer whose live stream to play back.", + "description": "The streamer to play back: a DID (did:plc/did:web/did:key) or a Bluesky handle, which is resolved to its DID.", "schema": { "type": "string", - "description": "DID of the streamer whose live stream to play back.", - "format": "did" + "description": "The streamer to play back: a DID (did:plc/did:web/did:key) or a Bluesky handle, which is resolved to its DID." } }, { @@ -765,14 +764,13 @@ }, "parameters": [ { - "name": "did", + "name": "streamer", "in": "query", "required": true, - "description": "DID of the streamer.", + "description": "The streamer: a DID or a Bluesky handle (resolved to its DID).", "schema": { "type": "string", - "description": "DID of the streamer.", - "format": "did" + "description": "The streamer: a DID or a Bluesky handle (resolved to its DID)." } }, { diff --git a/js/docs/src/content/docs/lex-reference/playback/place-stream-playback-getliveplaylist.md b/js/docs/src/content/docs/lex-reference/playback/place-stream-playback-getliveplaylist.md index 9b59d8eb..a8d7faf5 100644 --- a/js/docs/src/content/docs/lex-reference/playback/place-stream-playback-getliveplaylist.md +++ b/js/docs/src/content/docs/lex-reference/playback/place-stream-playback-getliveplaylist.md @@ -17,11 +17,11 @@ Get an HLS CMAF playlist for a live stream. Returns a master playlist when `trac **Parameters:** -| Name | Type | Req'd | Description | Constraints | -| ------- | -------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | -| `did` | `string` | ✅ | DID of the streamer whose live stream to play back. | Format: `did` | -| `track` | `string` | ❌ | Track ID (stringified u32 matching the MUXL container) for a single-track media playlist. Omit for the master playlist. | | -| `sid` | `string` | ❌ | Opaque playback session identifier. Omit on the master playlist request; the server generates one and threads it through the sub-playlist + segment URLs it returns, for view-count correlation. | | +| Name | Type | Req'd | Description | Constraints | +| ---------- | -------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | +| `streamer` | `string` | ✅ | The streamer to play back: a DID (did:plc/did:web/did:key) or a Bluesky handle, which is resolved to its DID. | | +| `track` | `string` | ❌ | Track ID (stringified u32 matching the MUXL container) for a single-track media playlist. Omit for the master playlist. | | +| `sid` | `string` | ❌ | Opaque playback session identifier. Omit on the master playlist request; the server generates one and threads it through the sub-playlist + segment URLs it returns, for view-count correlation. | | **Output:** @@ -49,12 +49,11 @@ _Schema not defined._ "description": "Get an HLS CMAF playlist for a live stream. Returns a master playlist when `track` is omitted, or a single-track media playlist when `track` is supplied. The playlist references each segment + per-track init segment via getLiveSegment. Segments come from an in-memory sliding window fed as the stream is ingested (or replicated to this node), so a playlist is only available while the stream is live here.", "parameters": { "type": "params", - "required": ["did"], + "required": ["streamer"], "properties": { - "did": { + "streamer": { "type": "string", - "format": "did", - "description": "DID of the streamer whose live stream to play back." + "description": "The streamer to play back: a DID (did:plc/did:web/did:key) or a Bluesky handle, which is resolved to its DID." }, "track": { "type": "string", diff --git a/js/docs/src/content/docs/lex-reference/playback/place-stream-playback-getlivesegment.md b/js/docs/src/content/docs/lex-reference/playback/place-stream-playback-getlivesegment.md index 19b12a97..ce0de5aa 100644 --- a/js/docs/src/content/docs/lex-reference/playback/place-stream-playback-getlivesegment.md +++ b/js/docs/src/content/docs/lex-reference/playback/place-stream-playback-getlivesegment.md @@ -17,12 +17,12 @@ Fetch a single live HLS segment, or a track's init segment, from the in-memory l **Parameters:** -| Name | Type | Req'd | Description | Constraints | -| ------- | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `did` | `string` | ✅ | DID of the streamer. | Format: `did` | -| `track` | `string` | ✅ | Track ID (stringified u32 matching the MUXL container). | | -| `seg` | `string` | ✅ | `init` for the track's init segment, or the segment's media-sequence number. A trailing `.m4s` is accepted and ignored. | | -| `sid` | `string` | ❌ | Opaque playback session identifier, propagated from the media playlist that referenced this segment. Logged for view-count correlation; not used for access control. | | +| Name | Type | Req'd | Description | Constraints | +| ---------- | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `streamer` | `string` | ✅ | The streamer: a DID or a Bluesky handle (resolved to its DID). | | +| `track` | `string` | ✅ | Track ID (stringified u32 matching the MUXL container). | | +| `seg` | `string` | ✅ | `init` for the track's init segment, or the segment's media-sequence number. A trailing `.m4s` is accepted and ignored. | | +| `sid` | `string` | ❌ | Opaque playback session identifier, propagated from the media playlist that referenced this segment. Logged for view-count correlation; not used for access control. | | **Output:** @@ -49,12 +49,11 @@ _Schema not defined._ "description": "Fetch a single live HLS segment, or a track's init segment, from the in-memory live window. `seg` is `init` for the EXT-X-MAP init segment, otherwise the segment's media-sequence number; a cosmetic `.m4s` suffix is accepted (and ignored) so ffmpeg-based HLS players will fetch it. HTTP Range is honored. Segments are the verbatim signed canonical .m4s, so provenance travels with playback.", "parameters": { "type": "params", - "required": ["did", "track", "seg"], + "required": ["streamer", "track", "seg"], "properties": { - "did": { + "streamer": { "type": "string", - "format": "did", - "description": "DID of the streamer." + "description": "The streamer: a DID or a Bluesky handle (resolved to its DID)." }, "track": { "type": "string", diff --git a/lexicons/place/stream/playback/getLivePlaylist.json b/lexicons/place/stream/playback/getLivePlaylist.json index 7894072e..594c14bd 100644 --- a/lexicons/place/stream/playback/getLivePlaylist.json +++ b/lexicons/place/stream/playback/getLivePlaylist.json @@ -7,12 +7,11 @@ "description": "Get an HLS CMAF playlist for a live stream. Returns a master playlist when `track` is omitted, or a single-track media playlist when `track` is supplied. The playlist references each segment + per-track init segment via getLiveSegment. Segments come from an in-memory sliding window fed as the stream is ingested (or replicated to this node), so a playlist is only available while the stream is live here.", "parameters": { "type": "params", - "required": ["did"], + "required": ["streamer"], "properties": { - "did": { + "streamer": { "type": "string", - "format": "did", - "description": "DID of the streamer whose live stream to play back." + "description": "The streamer to play back: a DID (did:plc/did:web/did:key) or a Bluesky handle, which is resolved to its DID." }, "track": { "type": "string", diff --git a/lexicons/place/stream/playback/getLiveSegment.json b/lexicons/place/stream/playback/getLiveSegment.json index 86303b1f..48519db8 100644 --- a/lexicons/place/stream/playback/getLiveSegment.json +++ b/lexicons/place/stream/playback/getLiveSegment.json @@ -7,12 +7,11 @@ "description": "Fetch a single live HLS segment, or a track's init segment, from the in-memory live window. `seg` is `init` for the EXT-X-MAP init segment, otherwise the segment's media-sequence number; a cosmetic `.m4s` suffix is accepted (and ignored) so ffmpeg-based HLS players will fetch it. HTTP Range is honored. Segments are the verbatim signed canonical .m4s, so provenance travels with playback.", "parameters": { "type": "params", - "required": ["did", "track", "seg"], + "required": ["streamer", "track", "seg"], "properties": { - "did": { + "streamer": { "type": "string", - "format": "did", - "description": "DID of the streamer." + "description": "The streamer: a DID or a Bluesky handle (resolved to its DID)." }, "track": { "type": "string", diff --git a/pkg/spxrpc/place_stream_playback_getlive.go b/pkg/spxrpc/place_stream_playback_getlive.go index fcaaf322..a7190abf 100644 --- a/pkg/spxrpc/place_stream_playback_getlive.go +++ b/pkg/spxrpc/place_stream_playback_getlive.go @@ -10,7 +10,6 @@ import ( "strings" "time" - "github.com/bluesky-social/indigo/atproto/syntax" "github.com/labstack/echo/v4" ) @@ -21,14 +20,35 @@ import ( // getLiveSegment, vnd.apple.mpegurl on getLivePlaylist). NewServer registers // custom echo routes that override them; these exist only to satisfy the build. -func (s *Server) handlePlaceStreamPlaybackGetLivePlaylist(ctx context.Context, did string, sid string, track string) (io.Reader, error) { +func (s *Server) handlePlaceStreamPlaybackGetLivePlaylist(ctx context.Context, sid string, streamer string, track string) (io.Reader, error) { return nil, stubMisrouted("getLivePlaylist") } -func (s *Server) handlePlaceStreamPlaybackGetLiveSegment(ctx context.Context, did string, seg string, sid string, track string) (io.Reader, error) { +func (s *Server) handlePlaceStreamPlaybackGetLiveSegment(ctx context.Context, seg string, sid string, streamer string, track string) (io.Reader, error) { return nil, stubMisrouted("getLiveSegment") } +// resolveStreamer turns a `streamer` query param — an alias, a DID +// (did:plc/did:web/did:key), or a Bluesky handle — into the streamer's DID, +// which is how the live window is keyed. Handles are resolved via the atproto +// sync cache; anything already a DID passes through unchanged. +func (s *Server) resolveStreamer(ctx context.Context, streamer string) (string, error) { + if alias, ok := s.aliases[streamer]; ok { + streamer = alias + } + if streamer == "" { + return "", echo.NewHTTPError(http.StatusBadRequest, "streamer is required") + } + if strings.HasPrefix(streamer, "did:") { + return streamer, nil + } + repo, err := s.ATSync.SyncBlueskyRepoCached(ctx, streamer) + if err != nil { + return "", echo.NewHTTPError(http.StatusBadRequest, "could not resolve streamer handle: "+err.Error()) + } + return repo.DID, nil +} + // --- getLivePlaylist ---------------------------------------------------- // HandleGetLivePlaylist serves a live HLS master playlist (track omitted) or a @@ -37,11 +57,11 @@ func (s *Server) handlePlaceStreamPlaybackGetLiveSegment(ctx context.Context, di // so a playlist exists only while the stream is live here. Open playback, // gated only on an account ban (auth middleware can layer on later). func (s *Server) HandleGetLivePlaylist(c echo.Context) error { - parsedDID, err := syntax.ParseDID(c.QueryParam("did")) + ctx := c.Request().Context() + did, err := s.resolveStreamer(ctx, c.QueryParam("streamer")) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "did is required and must be a valid DID") + return err } - did := parsedDID.String() if banned, err := s.accountBanned(did); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) @@ -61,6 +81,8 @@ func (s *Server) HandleGetLivePlaylist(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } + // Sub-playlist + segment URLs carry the resolved DID, so follow-up requests + // skip handle resolution and stay stable across a session. track := c.QueryParam("track") var body string if track == "" { @@ -94,11 +116,11 @@ func (s *Server) HandleGetLivePlaylist(c echo.Context) error { // The bytes are the verbatim signed segment, so provenance travels with // playback. func (s *Server) HandleGetLiveSegment(c echo.Context) error { - parsedDID, err := syntax.ParseDID(c.QueryParam("did")) + ctx := c.Request().Context() + did, err := s.resolveStreamer(ctx, c.QueryParam("streamer")) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "did is required and must be a valid DID") + return err } - did := parsedDID.String() track := c.QueryParam("track") if track == "" { return echo.NewHTTPError(http.StatusBadRequest, "track is required") @@ -149,10 +171,10 @@ func (s *Server) HandleGetLiveSegment(c echo.Context) error { // --- url builders ------------------------------------------------------- // liveTrackPlaylistURL is the URL to a single-track live media playlist served -// by this same handler. sid is propagated so a player's playlist + segment -// requests share an identifier. +// by this same handler. did is the resolved streamer DID; sid is propagated so +// a player's playlist + segment requests share an identifier. func liveTrackPlaylistURL(did, track, sid string) string { - q := url.Values{"did": {did}, "track": {track}} + q := url.Values{"streamer": {did}, "track": {track}} if sid != "" { q.Set("sid", sid) } @@ -164,7 +186,7 @@ func liveTrackPlaylistURL(did, track, sid string) string { // query token) to satisfy ffmpeg's segment-extension allowlist — the handler // strips it back off. func liveSegmentURL(did, track, seg, sid string) string { - q := url.Values{"did": {did}, "track": {track}} + q := url.Values{"streamer": {did}, "track": {track}} if sid != "" { q.Set("sid", sid) } diff --git a/pkg/spxrpc/place_stream_playback_getlive_test.go b/pkg/spxrpc/place_stream_playback_getlive_test.go index 33e413b0..0065c0b5 100644 --- a/pkg/spxrpc/place_stream_playback_getlive_test.go +++ b/pkg/spxrpc/place_stream_playback_getlive_test.go @@ -18,8 +18,8 @@ func TestLiveSegmentURL(t *testing.T) { require.Contains(t, u, "track=1") require.Contains(t, u, "sid=3kabc") require.Contains(t, u, "seg=42.m4s") - // DID is percent-encoded in the query. - require.Contains(t, u, "did=did%3Aplc%3Aabc123") + // The resolved streamer DID is percent-encoded under the streamer param. + require.Contains(t, u, "streamer=did%3Aplc%3Aabc123") // init segment URL (no sid). ui := liveSegmentURL("did:plc:abc123", "2", "init", "") @@ -34,5 +34,5 @@ func TestLiveTrackPlaylistURL(t *testing.T) { require.True(t, strings.HasPrefix(u, "/xrpc/place.stream.playback.getLivePlaylist?")) require.Contains(t, u, "track=1") require.Contains(t, u, "sid=3ksid") - require.Contains(t, u, "did=did%3Aplc%3Axyz") + require.Contains(t, u, "streamer=did%3Aplc%3Axyz") } diff --git a/pkg/spxrpc/stubs.go b/pkg/spxrpc/stubs.go index b6ab8c69..1bc0504a 100644 --- a/pkg/spxrpc/stubs.go +++ b/pkg/spxrpc/stubs.go @@ -979,13 +979,13 @@ func (s *Server) HandlePlaceStreamMultistreamPutTarget(c echo.Context) error { func (s *Server) HandlePlaceStreamPlaybackGetLivePlaylist(c echo.Context) error { ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamPlaybackGetLivePlaylist") defer span.End() - did := c.QueryParam("did") sid := c.QueryParam("sid") + streamer := c.QueryParam("streamer") track := c.QueryParam("track") var out io.Reader var handleErr error - // func (s *Server) handlePlaceStreamPlaybackGetLivePlaylist(ctx context.Context,did string,sid string,track string) (io.Reader, error) - out, handleErr = s.handlePlaceStreamPlaybackGetLivePlaylist(ctx, did, sid, track) + // func (s *Server) handlePlaceStreamPlaybackGetLivePlaylist(ctx context.Context,sid string,streamer string,track string) (io.Reader, error) + out, handleErr = s.handlePlaceStreamPlaybackGetLivePlaylist(ctx, sid, streamer, track) if handleErr != nil { return handleErr } @@ -995,14 +995,14 @@ func (s *Server) HandlePlaceStreamPlaybackGetLivePlaylist(c echo.Context) error func (s *Server) HandlePlaceStreamPlaybackGetLiveSegment(c echo.Context) error { ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamPlaybackGetLiveSegment") defer span.End() - did := c.QueryParam("did") seg := c.QueryParam("seg") sid := c.QueryParam("sid") + streamer := c.QueryParam("streamer") track := c.QueryParam("track") var out io.Reader var handleErr error - // func (s *Server) handlePlaceStreamPlaybackGetLiveSegment(ctx context.Context,did string,seg string,sid string,track string) (io.Reader, error) - out, handleErr = s.handlePlaceStreamPlaybackGetLiveSegment(ctx, did, seg, sid, track) + // func (s *Server) handlePlaceStreamPlaybackGetLiveSegment(ctx context.Context,seg string,sid string,streamer string,track string) (io.Reader, error) + out, handleErr = s.handlePlaceStreamPlaybackGetLiveSegment(ctx, seg, sid, streamer, track) if handleErr != nil { return handleErr } diff --git a/pkg/streamplace/playbackgetLivePlaylist.go b/pkg/streamplace/playbackgetLivePlaylist.go index 4ce2e143..948dd2a1 100644 --- a/pkg/streamplace/playbackgetLivePlaylist.go +++ b/pkg/streamplace/playbackgetLivePlaylist.go @@ -13,17 +13,17 @@ import ( // PlaybackGetLivePlaylist calls the XRPC method "place.stream.playback.getLivePlaylist". // -// did: DID of the streamer whose live stream to play back. // sid: Opaque playback session identifier. Omit on the master playlist request; the server generates one and threads it through the sub-playlist + segment URLs it returns, for view-count correlation. +// streamer: The streamer to play back: a DID (did:plc/did:web/did:key) or a Bluesky handle, which is resolved to its DID. // track: Track ID (stringified u32 matching the MUXL container) for a single-track media playlist. Omit for the master playlist. -func PlaybackGetLivePlaylist(ctx context.Context, c lexutil.LexClient, did string, sid string, track string) ([]byte, error) { +func PlaybackGetLivePlaylist(ctx context.Context, c lexutil.LexClient, sid string, streamer string, track string) ([]byte, error) { buf := new(bytes.Buffer) params := map[string]interface{}{} - params["did"] = did if sid != "" { params["sid"] = sid } + params["streamer"] = streamer if track != "" { params["track"] = track } diff --git a/pkg/streamplace/playbackgetLiveSegment.go b/pkg/streamplace/playbackgetLiveSegment.go index fa914256..276becc0 100644 --- a/pkg/streamplace/playbackgetLiveSegment.go +++ b/pkg/streamplace/playbackgetLiveSegment.go @@ -13,19 +13,19 @@ import ( // PlaybackGetLiveSegment calls the XRPC method "place.stream.playback.getLiveSegment". // -// did: DID of the streamer. // seg: `init` for the track's init segment, or the segment's media-sequence number. A trailing `.m4s` is accepted and ignored. // sid: Opaque playback session identifier, propagated from the media playlist that referenced this segment. Logged for view-count correlation; not used for access control. +// streamer: The streamer: a DID or a Bluesky handle (resolved to its DID). // track: Track ID (stringified u32 matching the MUXL container). -func PlaybackGetLiveSegment(ctx context.Context, c lexutil.LexClient, did string, seg string, sid string, track string) ([]byte, error) { +func PlaybackGetLiveSegment(ctx context.Context, c lexutil.LexClient, seg string, sid string, streamer string, track string) ([]byte, error) { buf := new(bytes.Buffer) params := map[string]interface{}{} - params["did"] = did params["seg"] = seg if sid != "" { params["sid"] = sid } + params["streamer"] = streamer params["track"] = track if err := c.LexDo(ctx, lexutil.Query, "", "place.stream.playback.getLiveSegment", params, nil, buf); err != nil { return nil, err