···32 protocol: PlayerProtocol;
33 setProtocol: (protocol: PlayerProtocol) => void;
3435- /** Source */
36 src: string;
3738 /** Function to set the source URL */
39 setSrc: (src: string) => void;
40-41- /** Flag indicating if ingest (stream input) is currently starting */
42- ingestStarting: boolean;
43-44- /** Function to set the ingestStarting flag */
45- setIngestStarting: (ingestStarting: boolean) => void;
4647 /** Flag indicating if ingest is live */
48 ingestLive: boolean;
···32 protocol: PlayerProtocol;
33 setProtocol: (protocol: PlayerProtocol) => void;
3435+ /** Source (streamer did) */
36 src: string;
3738 /** Function to set the source URL */
39 setSrc: (src: string) => void;
0000004041 /** Flag indicating if ingest is live */
42 ingestLive: boolean;
···24| `title` | `string` | ✅ | The title of the livestream, as it will be announced to followers. | Max Length: 1400<br/>Max Graphemes: 140 |
25| `url` | `string` | ❌ | The URL where this stream can be found. This is primarily a hint for other Streamplace nodes to locate and replicate the stream. | Format: `uri` |
26| `createdAt` | `string` | ✅ | Client-declared timestamp when this livestream started. | Format: `datetime` |
00027| `post` | [`com.atproto.repo.strongRef`](https://github.com/bluesky-social/atproto/tree/main/lexicons/com/atproto/repo/strongref.json#undefined) | ❌ | The post that announced this livestream. | |
28| `agent` | `string` | ❌ | The source of the livestream, if available, in a User Agent format: `<product> / <product-version> <comment>` e.g. Streamplace/0.7.5 iOS | |
29| `canonicalUrl` | `string` | ❌ | The primary URL where this livestream can be viewed, if available. | Format: `uri` |
···156 "type": "string",
157 "format": "datetime",
158 "description": "Client-declared timestamp when this livestream started."
00000000000000159 },
160 "post": {
161 "type": "ref",
···24| `title` | `string` | ✅ | The title of the livestream, as it will be announced to followers. | Max Length: 1400<br/>Max Graphemes: 140 |
25| `url` | `string` | ❌ | The URL where this stream can be found. This is primarily a hint for other Streamplace nodes to locate and replicate the stream. | Format: `uri` |
26| `createdAt` | `string` | ✅ | Client-declared timestamp when this livestream started. | Format: `datetime` |
27+| `lastSeenAt` | `string` | ❌ | Client-declared timestamp when this livestream was last seen by the Streamplace station. | Format: `datetime` |
28+| `endedAt` | `string` | ❌ | Client-declared timestamp when this livestream ended. Ended livestreams are not supposed to start up again. | Format: `datetime` |
29+| `idleTimeoutSeconds` | `integer` | ❌ | Time in seconds after which this livestream should be automatically ended if idle. Zero means no timeout. | |
30| `post` | [`com.atproto.repo.strongRef`](https://github.com/bluesky-social/atproto/tree/main/lexicons/com/atproto/repo/strongref.json#undefined) | ❌ | The post that announced this livestream. | |
31| `agent` | `string` | ❌ | The source of the livestream, if available, in a User Agent format: `<product> / <product-version> <comment>` e.g. Streamplace/0.7.5 iOS | |
32| `canonicalUrl` | `string` | ❌ | The primary URL where this livestream can be viewed, if available. | Format: `uri` |
···159 "type": "string",
160 "format": "datetime",
161 "description": "Client-declared timestamp when this livestream started."
162+ },
163+ "lastSeenAt": {
164+ "type": "string",
165+ "format": "datetime",
166+ "description": "Client-declared timestamp when this livestream was last seen by the Streamplace station."
167+ },
168+ "endedAt": {
169+ "type": "string",
170+ "format": "datetime",
171+ "description": "Client-declared timestamp when this livestream ended. Ended livestreams are not supposed to start up again."
172+ },
173+ "idleTimeoutSeconds": {
174+ "type": "integer",
175+ "description": "Time in seconds after which this livestream should be automatically ended if idle. Zero means no timeout."
176 },
177 "post": {
178 "type": "ref",
···26 "format": "datetime",
27 "description": "Client-declared timestamp when this livestream started."
28 },
29+ "lastSeenAt": {
30+ "type": "string",
31+ "format": "datetime",
32+ "description": "Client-declared timestamp when this livestream was last seen by the Streamplace station."
33+ },
34+ "endedAt": {
35+ "type": "string",
36+ "format": "datetime",
37+ "description": "Client-declared timestamp when this livestream ended. Ended livestreams are not supposed to start up again."
38+ },
39+ "idleTimeoutSeconds": {
40+ "type": "integer",
41+ "description": "Time in seconds after which this livestream should be automatically ended if idle. Zero means no timeout."
42+ },
43 "post": {
44 "type": "ref",
45 "ref": "com.atproto.repo.strongRef",
+36
lexicons/place/stream/playback/whep.json
···000000000000000000000000000000000000
···1+{
2+ "lexicon": 1,
3+ "id": "place.stream.playback.whep",
4+ "defs": {
5+ "main": {
6+ "type": "procedure",
7+ "description": "Play a stream over WebRTC using WHEP.",
8+ "parameters": {
9+ "type": "params",
10+ "required": ["streamer", "rendition"],
11+ "properties": {
12+ "streamer": {
13+ "type": "string",
14+ "description": "The DID of the streamer to play."
15+ },
16+ "rendition": {
17+ "type": "string",
18+ "description": "The rendition of the stream to play."
19+ }
20+ }
21+ },
22+ "input": {
23+ "encoding": "*/*"
24+ },
25+ "output": {
26+ "encoding": "*/*"
27+ },
28+ "errors": [
29+ {
30+ "name": "Unauthorized",
31+ "description": "This user may not play this stream."
32+ }
33+ ]
34+ }
35+ }
36+}
···14)
1516// This function remains in scope for the duration of a single users' playback
17-func (mm *MediaManager) WebRTCPlayback2(ctx context.Context, user string, rendition string, offer *webrtc.SessionDescription) (*webrtc.SessionDescription, error) {
18 uu, err := uuid.NewV7()
19 if err != nil {
20 return nil, err
···93 return
94 case file := <-segChan.C:
95 log.Debug(ctx, "got segment", "file", file.Filepath)
000096 latency += file.PacketizedData.Duration
97 packetQueue <- file.PacketizedData
98 }
···14)
1516// This function remains in scope for the duration of a single users' playback
17+func (mm *MediaManager) WebRTCPlayback2(ctx context.Context, user string, rendition string, offer *webrtc.SessionDescription, viewer string) (*webrtc.SessionDescription, error) {
18 uu, err := uuid.NewV7()
19 if err != nil {
20 return nil, err
···93 return
94 case file := <-segChan.C:
95 log.Debug(ctx, "got segment", "file", file.Filepath)
96+ if !file.Published && viewer != user && !mm.cli.WideOpen {
97+ log.Warn(ctx, "segment is not published and viewer is not the user", "viewer", viewer, "user", user)
98+ continue
99+ }
100 latency += file.PacketizedData.Duration
101 packetQueue <- file.PacketizedData
102 }
+1-1
pkg/media/webrtc_playback2_test.go
···17 Type: webrtc.SDPTypeOffer,
18 SDP: firefoxNoH264SDP,
19 }
20- answer, err := mm.WebRTCPlayback2(context.Background(), "test-user", "test-rendition", offer)
21 require.ErrorContains(t, err, "RTPSender created with no codecs")
22 require.Nil(t, answer)
23}
···17 Type: webrtc.SDPTypeOffer,
18 SDP: firefoxNoH264SDP,
19 }
20+ answer, err := mm.WebRTCPlayback2(context.Background(), "test-user", "test-rendition", offer, "")
21 require.ErrorContains(t, err, "RTPSender created with no codecs")
22 require.Nil(t, answer)
23}
···2021 "golang.org/x/image/draw"
2223- "github.com/bluesky-social/indigo/api/bsky"
24- "github.com/bluesky-social/indigo/xrpc"
25 "github.com/labstack/echo/v4"
26 "github.com/patrickmn/go-cache"
27 "github.com/tdewolff/canvas"
···89 maxDescriptionLength = 120
90 descriptionTruncate = 117
91)
92-93-var ErrUserNotFound = errors.New("user not found")
9495// blendWithBackground creates a pseudo-transparent color by blending the given color with the background
96// alpha should be between 0.0 (fully background) and 1.0 (fully foreground color)
···235 handle = username
236 description = "Live streaming platform for creators and their communities."
237238- profileData, err := s.fetchUserProfile(ctx, username)
239 if err != nil {
240 return nil, fmt.Errorf("failed to fetch profile, because %w", err)
241 } else if profileData != nil {
···513514 return data, nil
515}
516-517-func (s *Server) fetchUserProfile(ctx context.Context, username string) (*bsky.ActorDefs_ProfileViewDetailed, error) {
518- // Use ATSync to resolve username to DID, then fetch full profile from Bluesky
519- var actor string
520-521- // First try to resolve via internal DB
522- repo, err := s.ATSync.Model.GetRepoByHandleOrDID(username)
523- if err != nil {
524- return nil, fmt.Errorf("%w: %w", ErrUserNotFound, err)
525- } else if repo != nil {
526- // Use the DID as it's the most reliable identifier
527- actor = repo.DID
528- } else {
529- return nil, fmt.Errorf("no repo found for username: %s (%w)", username, ErrUserNotFound)
530- }
531-532- // Fetch full profile from Bluesky public API
533- client := &xrpc.Client{
534- Host: "https://public.api.bsky.app",
535- }
536-537- profile, err := bsky.ActorGetProfile(ctx, client, actor)
538- if err != nil {
539- return nil, fmt.Errorf("failed to fetch profile from Bluesky for '%s': %w", actor, err)
540- }
541-542- if profile == nil {
543- return nil, fmt.Errorf("received nil profile from Bluesky API for '%s'", actor)
544- }
545-546- return profile, nil
547-}
···2021 "golang.org/x/image/draw"
220023 "github.com/labstack/echo/v4"
24 "github.com/patrickmn/go-cache"
25 "github.com/tdewolff/canvas"
···87 maxDescriptionLength = 120
88 descriptionTruncate = 117
89)
009091// blendWithBackground creates a pseudo-transparent color by blending the given color with the background
92// alpha should be between 0.0 (fully background) and 1.0 (fully foreground color)
···231 handle = username
232 description = "Live streaming platform for creators and their communities."
233234+ profileData, err := s.ATSync.FetchUserProfile(ctx, username)
235 if err != nil {
236 return nil, fmt.Errorf("failed to fetch profile, because %w", err)
237 } else if profileData != nil {
···509510 return data, nil
511}
00000000000000000000000000000000
···1+package statedb
2+3+import (
4+ "context"
5+ "crypto/rand"
6+ "encoding/json"
7+ "fmt"
8+9+ "github.com/lestrrat-go/jwx/v2/jwk"
10+ "stream.place/streamplace/pkg/log"
11+)
12+13+// EnsureServiceAuthKey ensures a shared symmetric key exists in the config table
14+// for intra-service JWT authentication. All nodes sharing the same database will
15+// use the same key, enabling mutual authentication within a station.
16+func (state *StatefulDB) EnsureServiceAuthKey(ctx context.Context) (jwk.Key, error) {
17+ conf, err := state.GetConfig("service-auth-key")
18+ if err != nil {
19+ return nil, fmt.Errorf("failed to get service auth key: %w", err)
20+ }
21+22+ if conf != nil {
23+ key, err := jwk.ParseKey(conf.Value)
24+ if err != nil {
25+ return nil, fmt.Errorf("failed to parse service auth key: %w", err)
26+ }
27+ return key, nil
28+ }
29+30+ log.Warn(ctx, "no service auth key found, generating new one")
31+32+ secret := make([]byte, 32)
33+ if _, err := rand.Read(secret); err != nil {
34+ return nil, fmt.Errorf("failed to generate random bytes: %w", err)
35+ }
36+37+ key, err := jwk.FromRaw(secret)
38+ if err != nil {
39+ return nil, fmt.Errorf("failed to create symmetric key: %w", err)
40+ }
41+42+ b, err := json.Marshal(key)
43+ if err != nil {
44+ return nil, fmt.Errorf("failed to marshal service auth key: %w", err)
45+ }
46+47+ if err := state.PutConfig("service-auth-key", b); err != nil {
48+ return nil, fmt.Errorf("failed to save service auth key: %w", err)
49+ }
50+51+ return key, nil
52+}
+2-1
pkg/statedb/statedb.go
···37 // pgLockConn is used to hold a connection to the database for locking
38 pgLockConn *gorm.DB
39 pgLockConnMu sync.Mutex
40- op *oatproxy.OATProxy
41}
4243// list tables here so we can migrate them
···53 MultistreamEvent{},
54 BrandingBlob{},
55 ModerationAuditLog{},
056}
5758var NoPostgresDatabaseCode = "3D000"
···37 // pgLockConn is used to hold a connection to the database for locking
38 pgLockConn *gorm.DB
39 pgLockConnMu sync.Mutex
40+ OATProxy *oatproxy.OATProxy
41}
4243// list tables here so we can migrate them
···53 MultistreamEvent{},
54 BrandingBlob{},
55 ModerationAuditLog{},
56+ BroadcastOrigin{},
57}
5859var NoPostgresDatabaseCode = "3D000"
+2-2
pkg/statedb/task.go
···105 err := state.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
106 query := tx.Where("status = ?", TaskStatusPending).
107 Where("try_count < max_tries").
108- Where("(lock_expires IS NULL OR lock_expires < ?)", time.Now()).
109- Where("(scheduled_at IS NULL OR scheduled_at <= ?)", time.Now())
110111 if len(taskTypes) > 0 {
112 query = query.Where("type IN ?", taskTypes)
···105 err := state.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
106 query := tx.Where("status = ?", TaskStatusPending).
107 Where("try_count < max_tries").
108+ Where("(lock_expires IS NULL OR lock_expires < ?)", time.Now().UTC()).
109+ Where("(scheduled_at IS NULL OR scheduled_at <= ?)", time.Now().UTC())
110111 if len(taskTypes) > 0 {
112 query = query.Where("type IN ?", taskTypes)
···250 }
251252 cw := cbg.NewCborWriter(w)
253+ fieldCount := 12
254255 if t.Agent == nil {
256 fieldCount--
257 }
258259 if t.CanonicalUrl == nil {
260+ fieldCount--
261+ }
262+263+ if t.EndedAt == nil {
264+ fieldCount--
265+ }
266+267+ if t.IdleTimeoutSeconds == nil {
268+ fieldCount--
269+ }
270+271+ if t.LastSeenAt == nil {
272 fieldCount--
273 }
274···436 return err
437 }
438439+ // t.EndedAt (string) (string)
440+ if t.EndedAt != nil {
441+442+ if len("endedAt") > 1000000 {
443+ return xerrors.Errorf("Value in field \"endedAt\" was too long")
444+ }
445+446+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("endedAt"))); err != nil {
447+ return err
448+ }
449+ if _, err := cw.WriteString(string("endedAt")); err != nil {
450+ return err
451+ }
452+453+ if t.EndedAt == nil {
454+ if _, err := cw.Write(cbg.CborNull); err != nil {
455+ return err
456+ }
457+ } else {
458+ if len(*t.EndedAt) > 1000000 {
459+ return xerrors.Errorf("Value in field t.EndedAt was too long")
460+ }
461+462+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.EndedAt))); err != nil {
463+ return err
464+ }
465+ if _, err := cw.WriteString(string(*t.EndedAt)); err != nil {
466+ return err
467+ }
468+ }
469+ }
470+471 // t.CreatedAt (string) (string)
472 if len("createdAt") > 1000000 {
473 return xerrors.Errorf("Value in field \"createdAt\" was too long")
···491 return err
492 }
493494+ // t.LastSeenAt (string) (string)
495+ if t.LastSeenAt != nil {
496+497+ if len("lastSeenAt") > 1000000 {
498+ return xerrors.Errorf("Value in field \"lastSeenAt\" was too long")
499+ }
500+501+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("lastSeenAt"))); err != nil {
502+ return err
503+ }
504+ if _, err := cw.WriteString(string("lastSeenAt")); err != nil {
505+ return err
506+ }
507+508+ if t.LastSeenAt == nil {
509+ if _, err := cw.Write(cbg.CborNull); err != nil {
510+ return err
511+ }
512+ } else {
513+ if len(*t.LastSeenAt) > 1000000 {
514+ return xerrors.Errorf("Value in field t.LastSeenAt was too long")
515+ }
516+517+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.LastSeenAt))); err != nil {
518+ return err
519+ }
520+ if _, err := cw.WriteString(string(*t.LastSeenAt)); err != nil {
521+ return err
522+ }
523+ }
524+ }
525+526 // t.CanonicalUrl (string) (string)
527 if t.CanonicalUrl != nil {
528···555 }
556 }
557558+ // t.IdleTimeoutSeconds (int64) (int64)
559+ if t.IdleTimeoutSeconds != nil {
560+561+ if len("idleTimeoutSeconds") > 1000000 {
562+ return xerrors.Errorf("Value in field \"idleTimeoutSeconds\" was too long")
563+ }
564+565+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("idleTimeoutSeconds"))); err != nil {
566+ return err
567+ }
568+ if _, err := cw.WriteString(string("idleTimeoutSeconds")); err != nil {
569+ return err
570+ }
571+572+ if t.IdleTimeoutSeconds == nil {
573+ if _, err := cw.Write(cbg.CborNull); err != nil {
574+ return err
575+ }
576+ } else {
577+ if *t.IdleTimeoutSeconds >= 0 {
578+ if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.IdleTimeoutSeconds)); err != nil {
579+ return err
580+ }
581+ } else {
582+ if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.IdleTimeoutSeconds-1)); err != nil {
583+ return err
584+ }
585+ }
586+ }
587+588+ }
589+590 // t.NotificationSettings (streamplace.Livestream_NotificationSettings) (struct)
591 if t.NotificationSettings != nil {
592···753754 t.Title = string(sval)
755 }
756+ // t.EndedAt (string) (string)
757+ case "endedAt":
758+759+ {
760+ b, err := cr.ReadByte()
761+ if err != nil {
762+ return err
763+ }
764+ if b != cbg.CborNull[0] {
765+ if err := cr.UnreadByte(); err != nil {
766+ return err
767+ }
768+769+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
770+ if err != nil {
771+ return err
772+ }
773+774+ t.EndedAt = (*string)(&sval)
775+ }
776+ }
777 // t.CreatedAt (string) (string)
778 case "createdAt":
779···785786 t.CreatedAt = string(sval)
787 }
788+ // t.LastSeenAt (string) (string)
789+ case "lastSeenAt":
790+791+ {
792+ b, err := cr.ReadByte()
793+ if err != nil {
794+ return err
795+ }
796+ if b != cbg.CborNull[0] {
797+ if err := cr.UnreadByte(); err != nil {
798+ return err
799+ }
800+801+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
802+ if err != nil {
803+ return err
804+ }
805+806+ t.LastSeenAt = (*string)(&sval)
807+ }
808+ }
809 // t.CanonicalUrl (string) (string)
810 case "canonicalUrl":
811···825 }
826827 t.CanonicalUrl = (*string)(&sval)
828+ }
829+ }
830+ // t.IdleTimeoutSeconds (int64) (int64)
831+ case "idleTimeoutSeconds":
832+ {
833+834+ b, err := cr.ReadByte()
835+ if err != nil {
836+ return err
837+ }
838+ if b != cbg.CborNull[0] {
839+ if err := cr.UnreadByte(); err != nil {
840+ return err
841+ }
842+ maj, extra, err := cr.ReadHeader()
843+ if err != nil {
844+ return err
845+ }
846+ var extraI int64
847+ switch maj {
848+ case cbg.MajUnsignedInt:
849+ extraI = int64(extra)
850+ if extraI < 0 {
851+ return fmt.Errorf("int64 positive overflow")
852+ }
853+ case cbg.MajNegativeInt:
854+ extraI = int64(extra)
855+ if extraI < 0 {
856+ return fmt.Errorf("int64 negative overflow")
857+ }
858+ extraI = -1 - extraI
859+ default:
860+ return fmt.Errorf("wrong type for int64 field: %d", maj)
861+ }
862+863+ t.IdleTimeoutSeconds = (*int64)(&extraI)
864 }
865 }
866 // t.NotificationSettings (streamplace.Livestream_NotificationSettings) (struct)
+38
pkg/streamplace/livestartLivestream.go
···00000000000000000000000000000000000000
···1+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2+3+// Lexicon schema: place.stream.live.startLivestream
4+5+package streamplace
6+7+import (
8+ "context"
9+10+ lexutil "github.com/bluesky-social/indigo/lex/util"
11+)
12+13+// LiveStartLivestream_Input is the input argument to a place.stream.live.startLivestream call.
14+type LiveStartLivestream_Input struct {
15+ // createBlueskyPost: Whether to create a Bluesky post announcing the livestream.
16+ CreateBlueskyPost *bool `json:"createBlueskyPost,omitempty" cborgen:"createBlueskyPost,omitempty"`
17+ Livestream *Livestream `json:"livestream" cborgen:"livestream"`
18+ // streamer: The DID of the streamer.
19+ Streamer string `json:"streamer" cborgen:"streamer"`
20+}
21+22+// LiveStartLivestream_Output is the output of a place.stream.live.startLivestream call.
23+type LiveStartLivestream_Output struct {
24+ // cid: The CID of the livestream record.
25+ Cid string `json:"cid" cborgen:"cid"`
26+ // uri: The URI of the livestream record.
27+ Uri string `json:"uri" cborgen:"uri"`
28+}
29+30+// LiveStartLivestream calls the XRPC method "place.stream.live.startLivestream".
31+func LiveStartLivestream(ctx context.Context, c lexutil.LexClient, input *LiveStartLivestream_Input) (*LiveStartLivestream_Output, error) {
32+ var out LiveStartLivestream_Output
33+ if err := c.LexDo(ctx, lexutil.Procedure, "application/json", "place.stream.live.startLivestream", nil, input, &out); err != nil {
34+ return nil, err
35+ }
36+37+ return &out, nil
38+}
+33
pkg/streamplace/livestopLivestream.go
···000000000000000000000000000000000
···1+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2+3+// Lexicon schema: place.stream.live.stopLivestream
4+5+package streamplace
6+7+import (
8+ "context"
9+10+ lexutil "github.com/bluesky-social/indigo/lex/util"
11+)
12+13+// LiveStopLivestream_Input is the input argument to a place.stream.live.stopLivestream call.
14+type LiveStopLivestream_Input struct {
15+}
16+17+// LiveStopLivestream_Output is the output of a place.stream.live.stopLivestream call.
18+type LiveStopLivestream_Output struct {
19+ // cid: The new CID of the stopped livestream record.
20+ Cid string `json:"cid" cborgen:"cid"`
21+ // uri: The URI of the stopped livestream record.
22+ Uri string `json:"uri" cborgen:"uri"`
23+}
24+25+// LiveStopLivestream calls the XRPC method "place.stream.live.stopLivestream".
26+func LiveStopLivestream(ctx context.Context, c lexutil.LexClient, input *LiveStopLivestream_Input) (*LiveStopLivestream_Output, error) {
27+ var out LiveStopLivestream_Output
28+ if err := c.LexDo(ctx, lexutil.Procedure, "application/json", "place.stream.live.stopLivestream", nil, input, &out); err != nil {
29+ return nil, err
30+ }
31+32+ return &out, nil
33+}
+30
pkg/streamplace/playbackwhep.go
···000000000000000000000000000000
···1+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2+3+// Lexicon schema: place.stream.playback.whep
4+5+package streamplace
6+7+import (
8+ "bytes"
9+ "context"
10+ "io"
11+12+ lexutil "github.com/bluesky-social/indigo/lex/util"
13+)
14+15+// PlaybackWhep calls the XRPC method "place.stream.playback.whep".
16+//
17+// rendition: The rendition of the stream to play.
18+// streamer: The DID of the streamer to play.
19+func PlaybackWhep(ctx context.Context, c lexutil.LexClient, input io.Reader, rendition string, streamer string) ([]byte, error) {
20+ buf := new(bytes.Buffer)
21+22+ params := map[string]interface{}{}
23+ params["rendition"] = rendition
24+ params["streamer"] = streamer
25+ if err := c.LexDo(ctx, lexutil.Procedure, "*/*", "place.stream.playback.whep", params, input, buf); err != nil {
26+ return nil, err
27+ }
28+29+ return buf.Bytes(), nil
30+}
+7-1
pkg/streamplace/streamlivestream.go
···24 // canonicalUrl: The primary URL where this livestream can be viewed, if available.
25 CanonicalUrl *string `json:"canonicalUrl,omitempty" cborgen:"canonicalUrl,omitempty"`
26 // createdAt: Client-declared timestamp when this livestream started.
27- CreatedAt string `json:"createdAt" cborgen:"createdAt"`
00000028 NotificationSettings *Livestream_NotificationSettings `json:"notificationSettings,omitempty" cborgen:"notificationSettings,omitempty"`
29 // post: The post that announced this livestream.
30 Post *comatproto.RepoStrongRef `json:"post,omitempty" cborgen:"post,omitempty"`
···24 // canonicalUrl: The primary URL where this livestream can be viewed, if available.
25 CanonicalUrl *string `json:"canonicalUrl,omitempty" cborgen:"canonicalUrl,omitempty"`
26 // createdAt: Client-declared timestamp when this livestream started.
27+ CreatedAt string `json:"createdAt" cborgen:"createdAt"`
28+ // endedAt: Client-declared timestamp when this livestream ended. Ended livestreams are not supposed to start up again.
29+ EndedAt *string `json:"endedAt,omitempty" cborgen:"endedAt,omitempty"`
30+ // idleTimeoutSeconds: Time in seconds after which this livestream should be automatically ended if idle. Zero means no timeout.
31+ IdleTimeoutSeconds *int64 `json:"idleTimeoutSeconds,omitempty" cborgen:"idleTimeoutSeconds,omitempty"`
32+ // lastSeenAt: Client-declared timestamp when this livestream was last seen by the Streamplace station.
33+ LastSeenAt *string `json:"lastSeenAt,omitempty" cborgen:"lastSeenAt,omitempty"`
34 NotificationSettings *Livestream_NotificationSettings `json:"notificationSettings,omitempty" cborgen:"notificationSettings,omitempty"`
35 // post: The post that announced this livestream.
36 Post *comatproto.RepoStrongRef `json:"post,omitempty" cborgen:"post,omitempty"`