···3232 protocol: PlayerProtocol;
3333 setProtocol: (protocol: PlayerProtocol) => void;
34343535- /** Source */
3535+ /** Source (streamer did) */
3636 src: string;
37373838 /** Function to set the source URL */
3939 setSrc: (src: string) => void;
4040-4141- /** Flag indicating if ingest (stream input) is currently starting */
4242- ingestStarting: boolean;
4343-4444- /** Function to set the ingestStarting flag */
4545- setIngestStarting: (ingestStarting: boolean) => void;
46404741 /** Flag indicating if ingest is live */
4842 ingestLive: boolean;
···2424| `title` | `string` | ✅ | The title of the livestream, as it will be announced to followers. | Max Length: 1400<br/>Max Graphemes: 140 |
2525| `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` |
2626| `createdAt` | `string` | ✅ | Client-declared timestamp when this livestream started. | Format: `datetime` |
2727+| `lastSeenAt` | `string` | ❌ | Client-declared timestamp when this livestream was last seen by the Streamplace station. | Format: `datetime` |
2828+| `endedAt` | `string` | ❌ | Client-declared timestamp when this livestream ended. Ended livestreams are not supposed to start up again. | Format: `datetime` |
2929+| `idleTimeoutSeconds` | `integer` | ❌ | Time in seconds after which this livestream should be automatically ended if idle. Zero means no timeout. | |
2730| `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. | |
2831| `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 | |
2932| `canonicalUrl` | `string` | ❌ | The primary URL where this livestream can be viewed, if available. | Format: `uri` |
···156159 "type": "string",
157160 "format": "datetime",
158161 "description": "Client-declared timestamp when this livestream started."
162162+ },
163163+ "lastSeenAt": {
164164+ "type": "string",
165165+ "format": "datetime",
166166+ "description": "Client-declared timestamp when this livestream was last seen by the Streamplace station."
167167+ },
168168+ "endedAt": {
169169+ "type": "string",
170170+ "format": "datetime",
171171+ "description": "Client-declared timestamp when this livestream ended. Ended livestreams are not supposed to start up again."
172172+ },
173173+ "idleTimeoutSeconds": {
174174+ "type": "integer",
175175+ "description": "Time in seconds after which this livestream should be automatically ended if idle. Zero means no timeout."
159176 },
160177 "post": {
161178 "type": "ref",
···11+---
22+title: place.stream.playback.whep
33+description: Reference for the place.stream.playback.whep lexicon
44+---
55+66+**Lexicon Version:** 1
77+88+## Definitions
99+1010+<a name="main"></a>
1111+1212+### `main`
1313+1414+**Type:** `procedure`
1515+1616+Play a stream over WebRTC using WHEP.
1717+1818+**Parameters:**
1919+2020+| Name | Type | Req'd | Description | Constraints |
2121+| ----------- | -------- | ----- | ------------------------------------ | ----------- |
2222+| `streamer` | `string` | ✅ | The DID of the streamer to play. | |
2323+| `rendition` | `string` | ✅ | The rendition of the stream to play. | |
2424+2525+**Input:**
2626+2727+- **Encoding:** `*/*`
2828+- **Schema:**
2929+3030+_Schema not defined._
3131+**Output:**
3232+3333+- **Encoding:** `*/*`
3434+- **Schema:**
3535+3636+_Schema not defined._
3737+**Possible Errors:**
3838+3939+- `Unauthorized`: This user may not play this stream.
4040+4141+---
4242+4343+## Lexicon Source
4444+4545+```json
4646+{
4747+ "lexicon": 1,
4848+ "id": "place.stream.playback.whep",
4949+ "defs": {
5050+ "main": {
5151+ "type": "procedure",
5252+ "description": "Play a stream over WebRTC using WHEP.",
5353+ "parameters": {
5454+ "type": "params",
5555+ "required": ["streamer", "rendition"],
5656+ "properties": {
5757+ "streamer": {
5858+ "type": "string",
5959+ "description": "The DID of the streamer to play."
6060+ },
6161+ "rendition": {
6262+ "type": "string",
6363+ "description": "The rendition of the stream to play."
6464+ }
6565+ }
6666+ },
6767+ "input": {
6868+ "encoding": "*/*"
6969+ },
7070+ "output": {
7171+ "encoding": "*/*"
7272+ },
7373+ "errors": [
7474+ {
7575+ "name": "Unauthorized",
7676+ "description": "This user may not play this stream."
7777+ }
7878+ ]
7979+ }
8080+ }
8181+}
8282+```
+51
lexicons/place/stream/live/startLivestream.json
···11+{
22+ "lexicon": 1,
33+ "id": "place.stream.live.startLivestream",
44+ "defs": {
55+ "main": {
66+ "type": "procedure",
77+ "description": "Create a new place.stream.livestream record, automatically populating a thumbnail and creating a Bluesky post and whatnot. You can do this manually by creating a record but this method can work better for mobile livestreaming and such.",
88+ "input": {
99+ "encoding": "application/json",
1010+ "schema": {
1111+ "type": "object",
1212+ "required": ["streamer", "livestream"],
1313+ "properties": {
1414+ "livestream": {
1515+ "type": "ref",
1616+ "ref": "place.stream.livestream"
1717+ },
1818+ "streamer": {
1919+ "type": "string",
2020+ "format": "did",
2121+ "description": "The DID of the streamer."
2222+ },
2323+ "createBlueskyPost": {
2424+ "type": "boolean",
2525+ "description": "Whether to create a Bluesky post announcing the livestream."
2626+ }
2727+ }
2828+ }
2929+ },
3030+ "output": {
3131+ "encoding": "application/json",
3232+ "schema": {
3333+ "type": "object",
3434+ "required": ["uri", "cid"],
3535+ "properties": {
3636+ "uri": {
3737+ "type": "string",
3838+ "format": "uri",
3939+ "description": "The URI of the livestream record."
4040+ },
4141+ "cid": {
4242+ "type": "string",
4343+ "format": "cid",
4444+ "description": "The CID of the livestream record."
4545+ }
4646+ }
4747+ }
4848+ }
4949+ }
5050+ }
5151+}
+37
lexicons/place/stream/live/stopLivestream.json
···11+{
22+ "lexicon": 1,
33+ "id": "place.stream.live.stopLivestream",
44+ "defs": {
55+ "main": {
66+ "type": "procedure",
77+ "description": "Stop your current livestream, updating your current place.stream.livestream record and ceasing the flow of video.",
88+ "input": {
99+ "encoding": "application/json",
1010+ "schema": {
1111+ "type": "object",
1212+ "required": [],
1313+ "properties": {}
1414+ }
1515+ },
1616+ "output": {
1717+ "encoding": "application/json",
1818+ "schema": {
1919+ "type": "object",
2020+ "required": ["uri", "cid"],
2121+ "properties": {
2222+ "uri": {
2323+ "type": "string",
2424+ "format": "uri",
2525+ "description": "The URI of the stopped livestream record."
2626+ },
2727+ "cid": {
2828+ "type": "string",
2929+ "format": "cid",
3030+ "description": "The new CID of the stopped livestream record."
3131+ }
3232+ }
3333+ }
3434+ }
3535+ }
3636+ }
3737+}
+14
lexicons/place/stream/livestream.json
···2626 "format": "datetime",
2727 "description": "Client-declared timestamp when this livestream started."
2828 },
2929+ "lastSeenAt": {
3030+ "type": "string",
3131+ "format": "datetime",
3232+ "description": "Client-declared timestamp when this livestream was last seen by the Streamplace station."
3333+ },
3434+ "endedAt": {
3535+ "type": "string",
3636+ "format": "datetime",
3737+ "description": "Client-declared timestamp when this livestream ended. Ended livestreams are not supposed to start up again."
3838+ },
3939+ "idleTimeoutSeconds": {
4040+ "type": "integer",
4141+ "description": "Time in seconds after which this livestream should be automatically ended if idle. Zero means no timeout."
4242+ },
2943 "post": {
3044 "type": "ref",
3145 "ref": "com.atproto.repo.strongRef",
+36
lexicons/place/stream/playback/whep.json
···11+{
22+ "lexicon": 1,
33+ "id": "place.stream.playback.whep",
44+ "defs": {
55+ "main": {
66+ "type": "procedure",
77+ "description": "Play a stream over WebRTC using WHEP.",
88+ "parameters": {
99+ "type": "params",
1010+ "required": ["streamer", "rendition"],
1111+ "properties": {
1212+ "streamer": {
1313+ "type": "string",
1414+ "description": "The DID of the streamer to play."
1515+ },
1616+ "rendition": {
1717+ "type": "string",
1818+ "description": "The rendition of the stream to play."
1919+ }
2020+ }
2121+ },
2222+ "input": {
2323+ "encoding": "*/*"
2424+ },
2525+ "output": {
2626+ "encoding": "*/*"
2727+ },
2828+ "errors": [
2929+ {
3030+ "name": "Unauthorized",
3131+ "description": "This user may not play this stream."
3232+ }
3333+ ]
3434+ }
3535+ }
3636+}
···20202121 "golang.org/x/image/draw"
22222323- "github.com/bluesky-social/indigo/api/bsky"
2424- "github.com/bluesky-social/indigo/xrpc"
2523 "github.com/labstack/echo/v4"
2624 "github.com/patrickmn/go-cache"
2725 "github.com/tdewolff/canvas"
···8987 maxDescriptionLength = 120
9088 descriptionTruncate = 117
9189)
9292-9393-var ErrUserNotFound = errors.New("user not found")
94909591// blendWithBackground creates a pseudo-transparent color by blending the given color with the background
9692// alpha should be between 0.0 (fully background) and 1.0 (fully foreground color)
···235231 handle = username
236232 description = "Live streaming platform for creators and their communities."
237233238238- profileData, err := s.fetchUserProfile(ctx, username)
234234+ profileData, err := s.ATSync.FetchUserProfile(ctx, username)
239235 if err != nil {
240236 return nil, fmt.Errorf("failed to fetch profile, because %w", err)
241237 } else if profileData != nil {
···513509514510 return data, nil
515511}
516516-517517-func (s *Server) fetchUserProfile(ctx context.Context, username string) (*bsky.ActorDefs_ProfileViewDetailed, error) {
518518- // Use ATSync to resolve username to DID, then fetch full profile from Bluesky
519519- var actor string
520520-521521- // First try to resolve via internal DB
522522- repo, err := s.ATSync.Model.GetRepoByHandleOrDID(username)
523523- if err != nil {
524524- return nil, fmt.Errorf("%w: %w", ErrUserNotFound, err)
525525- } else if repo != nil {
526526- // Use the DID as it's the most reliable identifier
527527- actor = repo.DID
528528- } else {
529529- return nil, fmt.Errorf("no repo found for username: %s (%w)", username, ErrUserNotFound)
530530- }
531531-532532- // Fetch full profile from Bluesky public API
533533- client := &xrpc.Client{
534534- Host: "https://public.api.bsky.app",
535535- }
536536-537537- profile, err := bsky.ActorGetProfile(ctx, client, actor)
538538- if err != nil {
539539- return nil, fmt.Errorf("failed to fetch profile from Bluesky for '%s': %w", actor, err)
540540- }
541541-542542- if profile == nil {
543543- return nil, fmt.Errorf("received nil profile from Bluesky API for '%s'", actor)
544544- }
545545-546546- return profile, nil
547547-}
···11+package statedb
22+33+import (
44+ "context"
55+ "crypto/rand"
66+ "encoding/json"
77+ "fmt"
88+99+ "github.com/lestrrat-go/jwx/v2/jwk"
1010+ "stream.place/streamplace/pkg/log"
1111+)
1212+1313+// EnsureServiceAuthKey ensures a shared symmetric key exists in the config table
1414+// for intra-service JWT authentication. All nodes sharing the same database will
1515+// use the same key, enabling mutual authentication within a station.
1616+func (state *StatefulDB) EnsureServiceAuthKey(ctx context.Context) (jwk.Key, error) {
1717+ conf, err := state.GetConfig("service-auth-key")
1818+ if err != nil {
1919+ return nil, fmt.Errorf("failed to get service auth key: %w", err)
2020+ }
2121+2222+ if conf != nil {
2323+ key, err := jwk.ParseKey(conf.Value)
2424+ if err != nil {
2525+ return nil, fmt.Errorf("failed to parse service auth key: %w", err)
2626+ }
2727+ return key, nil
2828+ }
2929+3030+ log.Warn(ctx, "no service auth key found, generating new one")
3131+3232+ secret := make([]byte, 32)
3333+ if _, err := rand.Read(secret); err != nil {
3434+ return nil, fmt.Errorf("failed to generate random bytes: %w", err)
3535+ }
3636+3737+ key, err := jwk.FromRaw(secret)
3838+ if err != nil {
3939+ return nil, fmt.Errorf("failed to create symmetric key: %w", err)
4040+ }
4141+4242+ b, err := json.Marshal(key)
4343+ if err != nil {
4444+ return nil, fmt.Errorf("failed to marshal service auth key: %w", err)
4545+ }
4646+4747+ if err := state.PutConfig("service-auth-key", b); err != nil {
4848+ return nil, fmt.Errorf("failed to save service auth key: %w", err)
4949+ }
5050+5151+ return key, nil
5252+}
+2-1
pkg/statedb/statedb.go
···3737 // pgLockConn is used to hold a connection to the database for locking
3838 pgLockConn *gorm.DB
3939 pgLockConnMu sync.Mutex
4040- op *oatproxy.OATProxy
4040+ OATProxy *oatproxy.OATProxy
4141}
42424343// list tables here so we can migrate them
···5353 MultistreamEvent{},
5454 BrandingBlob{},
5555 ModerationAuditLog{},
5656+ BroadcastOrigin{},
5657}
57585859var NoPostgresDatabaseCode = "3D000"
+2-2
pkg/statedb/task.go
···105105 err := state.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
106106 query := tx.Where("status = ?", TaskStatusPending).
107107 Where("try_count < max_tries").
108108- Where("(lock_expires IS NULL OR lock_expires < ?)", time.Now()).
109109- Where("(scheduled_at IS NULL OR scheduled_at <= ?)", time.Now())
108108+ Where("(lock_expires IS NULL OR lock_expires < ?)", time.Now().UTC()).
109109+ Where("(scheduled_at IS NULL OR scheduled_at <= ?)", time.Now().UTC())
110110111111 if len(taskTypes) > 0 {
112112 query = query.Where("type IN ?", taskTypes)
+187-1
pkg/streamplace/cbor_gen.go
···250250 }
251251252252 cw := cbg.NewCborWriter(w)
253253- fieldCount := 9
253253+ fieldCount := 12
254254255255 if t.Agent == nil {
256256 fieldCount--
257257 }
258258259259 if t.CanonicalUrl == nil {
260260+ fieldCount--
261261+ }
262262+263263+ if t.EndedAt == nil {
264264+ fieldCount--
265265+ }
266266+267267+ if t.IdleTimeoutSeconds == nil {
268268+ fieldCount--
269269+ }
270270+271271+ if t.LastSeenAt == nil {
260272 fieldCount--
261273 }
262274···424436 return err
425437 }
426438439439+ // t.EndedAt (string) (string)
440440+ if t.EndedAt != nil {
441441+442442+ if len("endedAt") > 1000000 {
443443+ return xerrors.Errorf("Value in field \"endedAt\" was too long")
444444+ }
445445+446446+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("endedAt"))); err != nil {
447447+ return err
448448+ }
449449+ if _, err := cw.WriteString(string("endedAt")); err != nil {
450450+ return err
451451+ }
452452+453453+ if t.EndedAt == nil {
454454+ if _, err := cw.Write(cbg.CborNull); err != nil {
455455+ return err
456456+ }
457457+ } else {
458458+ if len(*t.EndedAt) > 1000000 {
459459+ return xerrors.Errorf("Value in field t.EndedAt was too long")
460460+ }
461461+462462+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.EndedAt))); err != nil {
463463+ return err
464464+ }
465465+ if _, err := cw.WriteString(string(*t.EndedAt)); err != nil {
466466+ return err
467467+ }
468468+ }
469469+ }
470470+427471 // t.CreatedAt (string) (string)
428472 if len("createdAt") > 1000000 {
429473 return xerrors.Errorf("Value in field \"createdAt\" was too long")
···447491 return err
448492 }
449493494494+ // t.LastSeenAt (string) (string)
495495+ if t.LastSeenAt != nil {
496496+497497+ if len("lastSeenAt") > 1000000 {
498498+ return xerrors.Errorf("Value in field \"lastSeenAt\" was too long")
499499+ }
500500+501501+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("lastSeenAt"))); err != nil {
502502+ return err
503503+ }
504504+ if _, err := cw.WriteString(string("lastSeenAt")); err != nil {
505505+ return err
506506+ }
507507+508508+ if t.LastSeenAt == nil {
509509+ if _, err := cw.Write(cbg.CborNull); err != nil {
510510+ return err
511511+ }
512512+ } else {
513513+ if len(*t.LastSeenAt) > 1000000 {
514514+ return xerrors.Errorf("Value in field t.LastSeenAt was too long")
515515+ }
516516+517517+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.LastSeenAt))); err != nil {
518518+ return err
519519+ }
520520+ if _, err := cw.WriteString(string(*t.LastSeenAt)); err != nil {
521521+ return err
522522+ }
523523+ }
524524+ }
525525+450526 // t.CanonicalUrl (string) (string)
451527 if t.CanonicalUrl != nil {
452528···479555 }
480556 }
481557558558+ // t.IdleTimeoutSeconds (int64) (int64)
559559+ if t.IdleTimeoutSeconds != nil {
560560+561561+ if len("idleTimeoutSeconds") > 1000000 {
562562+ return xerrors.Errorf("Value in field \"idleTimeoutSeconds\" was too long")
563563+ }
564564+565565+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("idleTimeoutSeconds"))); err != nil {
566566+ return err
567567+ }
568568+ if _, err := cw.WriteString(string("idleTimeoutSeconds")); err != nil {
569569+ return err
570570+ }
571571+572572+ if t.IdleTimeoutSeconds == nil {
573573+ if _, err := cw.Write(cbg.CborNull); err != nil {
574574+ return err
575575+ }
576576+ } else {
577577+ if *t.IdleTimeoutSeconds >= 0 {
578578+ if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.IdleTimeoutSeconds)); err != nil {
579579+ return err
580580+ }
581581+ } else {
582582+ if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.IdleTimeoutSeconds-1)); err != nil {
583583+ return err
584584+ }
585585+ }
586586+ }
587587+588588+ }
589589+482590 // t.NotificationSettings (streamplace.Livestream_NotificationSettings) (struct)
483591 if t.NotificationSettings != nil {
484592···645753646754 t.Title = string(sval)
647755 }
756756+ // t.EndedAt (string) (string)
757757+ case "endedAt":
758758+759759+ {
760760+ b, err := cr.ReadByte()
761761+ if err != nil {
762762+ return err
763763+ }
764764+ if b != cbg.CborNull[0] {
765765+ if err := cr.UnreadByte(); err != nil {
766766+ return err
767767+ }
768768+769769+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
770770+ if err != nil {
771771+ return err
772772+ }
773773+774774+ t.EndedAt = (*string)(&sval)
775775+ }
776776+ }
648777 // t.CreatedAt (string) (string)
649778 case "createdAt":
650779···656785657786 t.CreatedAt = string(sval)
658787 }
788788+ // t.LastSeenAt (string) (string)
789789+ case "lastSeenAt":
790790+791791+ {
792792+ b, err := cr.ReadByte()
793793+ if err != nil {
794794+ return err
795795+ }
796796+ if b != cbg.CborNull[0] {
797797+ if err := cr.UnreadByte(); err != nil {
798798+ return err
799799+ }
800800+801801+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
802802+ if err != nil {
803803+ return err
804804+ }
805805+806806+ t.LastSeenAt = (*string)(&sval)
807807+ }
808808+ }
659809 // t.CanonicalUrl (string) (string)
660810 case "canonicalUrl":
661811···675825 }
676826677827 t.CanonicalUrl = (*string)(&sval)
828828+ }
829829+ }
830830+ // t.IdleTimeoutSeconds (int64) (int64)
831831+ case "idleTimeoutSeconds":
832832+ {
833833+834834+ b, err := cr.ReadByte()
835835+ if err != nil {
836836+ return err
837837+ }
838838+ if b != cbg.CborNull[0] {
839839+ if err := cr.UnreadByte(); err != nil {
840840+ return err
841841+ }
842842+ maj, extra, err := cr.ReadHeader()
843843+ if err != nil {
844844+ return err
845845+ }
846846+ var extraI int64
847847+ switch maj {
848848+ case cbg.MajUnsignedInt:
849849+ extraI = int64(extra)
850850+ if extraI < 0 {
851851+ return fmt.Errorf("int64 positive overflow")
852852+ }
853853+ case cbg.MajNegativeInt:
854854+ extraI = int64(extra)
855855+ if extraI < 0 {
856856+ return fmt.Errorf("int64 negative overflow")
857857+ }
858858+ extraI = -1 - extraI
859859+ default:
860860+ return fmt.Errorf("wrong type for int64 field: %d", maj)
861861+ }
862862+863863+ t.IdleTimeoutSeconds = (*int64)(&extraI)
678864 }
679865 }
680866 // t.NotificationSettings (streamplace.Livestream_NotificationSettings) (struct)
+38
pkg/streamplace/livestartLivestream.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+// Lexicon schema: place.stream.live.startLivestream
44+55+package streamplace
66+77+import (
88+ "context"
99+1010+ lexutil "github.com/bluesky-social/indigo/lex/util"
1111+)
1212+1313+// LiveStartLivestream_Input is the input argument to a place.stream.live.startLivestream call.
1414+type LiveStartLivestream_Input struct {
1515+ // createBlueskyPost: Whether to create a Bluesky post announcing the livestream.
1616+ CreateBlueskyPost *bool `json:"createBlueskyPost,omitempty" cborgen:"createBlueskyPost,omitempty"`
1717+ Livestream *Livestream `json:"livestream" cborgen:"livestream"`
1818+ // streamer: The DID of the streamer.
1919+ Streamer string `json:"streamer" cborgen:"streamer"`
2020+}
2121+2222+// LiveStartLivestream_Output is the output of a place.stream.live.startLivestream call.
2323+type LiveStartLivestream_Output struct {
2424+ // cid: The CID of the livestream record.
2525+ Cid string `json:"cid" cborgen:"cid"`
2626+ // uri: The URI of the livestream record.
2727+ Uri string `json:"uri" cborgen:"uri"`
2828+}
2929+3030+// LiveStartLivestream calls the XRPC method "place.stream.live.startLivestream".
3131+func LiveStartLivestream(ctx context.Context, c lexutil.LexClient, input *LiveStartLivestream_Input) (*LiveStartLivestream_Output, error) {
3232+ var out LiveStartLivestream_Output
3333+ if err := c.LexDo(ctx, lexutil.Procedure, "application/json", "place.stream.live.startLivestream", nil, input, &out); err != nil {
3434+ return nil, err
3535+ }
3636+3737+ return &out, nil
3838+}
+33
pkg/streamplace/livestopLivestream.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+// Lexicon schema: place.stream.live.stopLivestream
44+55+package streamplace
66+77+import (
88+ "context"
99+1010+ lexutil "github.com/bluesky-social/indigo/lex/util"
1111+)
1212+1313+// LiveStopLivestream_Input is the input argument to a place.stream.live.stopLivestream call.
1414+type LiveStopLivestream_Input struct {
1515+}
1616+1717+// LiveStopLivestream_Output is the output of a place.stream.live.stopLivestream call.
1818+type LiveStopLivestream_Output struct {
1919+ // cid: The new CID of the stopped livestream record.
2020+ Cid string `json:"cid" cborgen:"cid"`
2121+ // uri: The URI of the stopped livestream record.
2222+ Uri string `json:"uri" cborgen:"uri"`
2323+}
2424+2525+// LiveStopLivestream calls the XRPC method "place.stream.live.stopLivestream".
2626+func LiveStopLivestream(ctx context.Context, c lexutil.LexClient, input *LiveStopLivestream_Input) (*LiveStopLivestream_Output, error) {
2727+ var out LiveStopLivestream_Output
2828+ if err := c.LexDo(ctx, lexutil.Procedure, "application/json", "place.stream.live.stopLivestream", nil, input, &out); err != nil {
2929+ return nil, err
3030+ }
3131+3232+ return &out, nil
3333+}
+30
pkg/streamplace/playbackwhep.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+// Lexicon schema: place.stream.playback.whep
44+55+package streamplace
66+77+import (
88+ "bytes"
99+ "context"
1010+ "io"
1111+1212+ lexutil "github.com/bluesky-social/indigo/lex/util"
1313+)
1414+1515+// PlaybackWhep calls the XRPC method "place.stream.playback.whep".
1616+//
1717+// rendition: The rendition of the stream to play.
1818+// streamer: The DID of the streamer to play.
1919+func PlaybackWhep(ctx context.Context, c lexutil.LexClient, input io.Reader, rendition string, streamer string) ([]byte, error) {
2020+ buf := new(bytes.Buffer)
2121+2222+ params := map[string]interface{}{}
2323+ params["rendition"] = rendition
2424+ params["streamer"] = streamer
2525+ if err := c.LexDo(ctx, lexutil.Procedure, "*/*", "place.stream.playback.whep", params, input, buf); err != nil {
2626+ return nil, err
2727+ }
2828+2929+ return buf.Bytes(), nil
3030+}
+7-1
pkg/streamplace/streamlivestream.go
···2424 // canonicalUrl: The primary URL where this livestream can be viewed, if available.
2525 CanonicalUrl *string `json:"canonicalUrl,omitempty" cborgen:"canonicalUrl,omitempty"`
2626 // createdAt: Client-declared timestamp when this livestream started.
2727- CreatedAt string `json:"createdAt" cborgen:"createdAt"`
2727+ CreatedAt string `json:"createdAt" cborgen:"createdAt"`
2828+ // endedAt: Client-declared timestamp when this livestream ended. Ended livestreams are not supposed to start up again.
2929+ EndedAt *string `json:"endedAt,omitempty" cborgen:"endedAt,omitempty"`
3030+ // idleTimeoutSeconds: Time in seconds after which this livestream should be automatically ended if idle. Zero means no timeout.
3131+ IdleTimeoutSeconds *int64 `json:"idleTimeoutSeconds,omitempty" cborgen:"idleTimeoutSeconds,omitempty"`
3232+ // lastSeenAt: Client-declared timestamp when this livestream was last seen by the Streamplace station.
3333+ LastSeenAt *string `json:"lastSeenAt,omitempty" cborgen:"lastSeenAt,omitempty"`
2834 NotificationSettings *Livestream_NotificationSettings `json:"notificationSettings,omitempty" cborgen:"notificationSettings,omitempty"`
2935 // post: The post that announced this livestream.
3036 Post *comatproto.RepoStrongRef `json:"post,omitempty" cborgen:"post,omitempty"`