diff --git a/go.mod b/go.mod index a064e920..5af39ad7 100644 --- a/go.mod +++ b/go.mod @@ -91,6 +91,7 @@ require ( gorm.io/driver/postgres v1.5.7 gorm.io/driver/sqlite v1.5.7 gorm.io/plugin/prometheus v0.1.0 + lukechampine.com/blake3 v1.4.1 ) require ( @@ -564,7 +565,6 @@ require ( gotest.tools/v3 v3.5.2 // indirect honnef.co/go/tools v0.6.1 // indirect howett.net/plist v1.0.0 // indirect - lukechampine.com/blake3 v1.4.1 // indirect modernc.org/knuth v0.5.5 // indirect modernc.org/token v1.1.0 // indirect mvdan.cc/gofumpt v0.8.0 // indirect diff --git a/js/docs/src/content/docs/lex-reference/media/place-stream-media-origin.md b/js/docs/src/content/docs/lex-reference/media/place-stream-media-origin.md index ca0e7694..67460377 100644 --- a/js/docs/src/content/docs/lex-reference/media/place-stream-media-origin.md +++ b/js/docs/src/content/docs/lex-reference/media/place-stream-media-origin.md @@ -11,17 +11,20 @@ description: Reference for the place.stream.media.origin lexicon ### `main` -**Type:** `object` +**Type:** `record` -A record indicating that a MUXL blob is available for download somewhere. +An attestation that a MUXL blob is available for download at one or more URLs. Published by the Streamplace node that hosts the blob, not by the user who owns the underlying video. The rkey is conventionally the blob's BDASL CID — atproto lexicon doesn't yet have a literal-rkey syntax so we settle for `key: any` and rely on the convention. -**Properties:** +**Record Key:** `any` -| Name | Type | Req'd | Description | Constraints | -| ---------- | --------- | ----- | ------------------------------------------------------------- | ----------- | -| `blob` | `string` | ✅ | BLAKE-3 content hash (BDASL CID) of the source video segment. | | -| `size` | `integer` | ✅ | Size of the file in bytes. | | -| `mimeType` | `string` | ✅ | MIME type of the file (e.g. video/mp4). | | +**Record Properties:** + +| Name | Type | Req'd | Description | Constraints | +| ---------- | ----------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| `blob` | `string` | ✅ | BLAKE-3 content hash (BDASL CID) of the blob. | | +| `size` | `integer` | ✅ | Size of the blob in bytes. | | +| `mimeType` | `string` | ✅ | MIME type of the blob (e.g. video/mp4). | | +| `urls` | Array of `string` | ✅ | Public URLs from which the blob can be fetched. Multiple entries enable transport diversity (HTTP, future iroh/libp2p schemes). | Min Items: 1 | --- @@ -33,21 +36,34 @@ A record indicating that a MUXL blob is available for download somewhere. "id": "place.stream.media.origin", "defs": { "main": { - "type": "object", - "description": "A record indicating that a MUXL blob is available for download somewhere.", - "required": ["blob", "size", "mimeType"], - "properties": { - "blob": { - "type": "string", - "description": "BLAKE-3 content hash (BDASL CID) of the source video segment." - }, - "size": { - "type": "integer", - "description": "Size of the file in bytes." - }, - "mimeType": { - "type": "string", - "description": "MIME type of the file (e.g. video/mp4)." + "type": "record", + "description": "An attestation that a MUXL blob is available for download at one or more URLs. Published by the Streamplace node that hosts the blob, not by the user who owns the underlying video. The rkey is conventionally the blob's BDASL CID — atproto lexicon doesn't yet have a literal-rkey syntax so we settle for `key: any` and rely on the convention.", + "key": "any", + "record": { + "type": "object", + "required": ["blob", "size", "mimeType", "urls"], + "properties": { + "blob": { + "type": "string", + "description": "BLAKE-3 content hash (BDASL CID) of the blob." + }, + "size": { + "type": "integer", + "description": "Size of the blob in bytes." + }, + "mimeType": { + "type": "string", + "description": "MIME type of the blob (e.g. video/mp4)." + }, + "urls": { + "type": "array", + "description": "Public URLs from which the blob can be fetched. Multiple entries enable transport diversity (HTTP, future iroh/libp2p schemes).", + "minLength": 1, + "items": { + "type": "string", + "format": "uri" + } + } } } } diff --git a/js/docs/src/content/docs/lex-reference/place-stream-segment.md b/js/docs/src/content/docs/lex-reference/place-stream-segment.md index 05e70d45..2bc958da 100644 --- a/js/docs/src/content/docs/lex-reference/place-stream-segment.md +++ b/js/docs/src/content/docs/lex-reference/place-stream-segment.md @@ -43,11 +43,11 @@ Media file representing a segment of a livestream **Properties:** -| Name | Type | Req'd | Description | Constraints | -| ---------- | --------- | ----- | ----------- | ------------ | -| `codec` | `string` | ✅ | | Enum: `opus` | -| `rate` | `integer` | ✅ | | | -| `channels` | `integer` | ✅ | | | +| Name | Type | Req'd | Description | Constraints | +| ---------- | --------- | ----- | ----------- | ------------------- | +| `codec` | `string` | ✅ | | Enum: `opus`, `aac` | +| `rate` | `integer` | ✅ | | | +| `channels` | `integer` | ✅ | | | --- @@ -174,7 +174,7 @@ Media file representing a segment of a livestream "properties": { "codec": { "type": "string", - "enum": ["opus"] + "enum": ["opus", "aac"] }, "rate": { "type": "integer" diff --git a/lexicons/place/stream/media/origin.json b/lexicons/place/stream/media/origin.json index a4770fa0..4fbefa91 100644 --- a/lexicons/place/stream/media/origin.json +++ b/lexicons/place/stream/media/origin.json @@ -3,21 +3,34 @@ "id": "place.stream.media.origin", "defs": { "main": { - "type": "object", - "description": "A record indicating that a MUXL blob is available for download somewhere.", - "required": ["blob", "size", "mimeType"], - "properties": { - "blob": { - "type": "string", - "description": "BLAKE-3 content hash (BDASL CID) of the source video segment." - }, - "size": { - "type": "integer", - "description": "Size of the file in bytes." - }, - "mimeType": { - "type": "string", - "description": "MIME type of the file (e.g. video/mp4)." + "type": "record", + "description": "An attestation that a MUXL blob is available for download at one or more URLs. Published by the Streamplace node that hosts the blob, not by the user who owns the underlying video. The rkey is conventionally the blob's BDASL CID — atproto lexicon doesn't yet have a literal-rkey syntax so we settle for `key: any` and rely on the convention.", + "key": "any", + "record": { + "type": "object", + "required": ["blob", "size", "mimeType", "urls"], + "properties": { + "blob": { + "type": "string", + "description": "BLAKE-3 content hash (BDASL CID) of the blob." + }, + "size": { + "type": "integer", + "description": "Size of the blob in bytes." + }, + "mimeType": { + "type": "string", + "description": "MIME type of the blob (e.g. video/mp4)." + }, + "urls": { + "type": "array", + "description": "Public URLs from which the blob can be fetched. Multiple entries enable transport diversity (HTTP, future iroh/libp2p schemes).", + "minLength": 1, + "items": { + "type": "string", + "format": "uri" + } + } } } } diff --git a/lexicons/place/stream/segment.json b/lexicons/place/stream/segment.json index 3c270dc3..f55e8835 100644 --- a/lexicons/place/stream/segment.json +++ b/lexicons/place/stream/segment.json @@ -68,7 +68,7 @@ "type": "object", "required": ["codec", "rate", "channels"], "properties": { - "codec": { "type": "string", "enum": ["opus"] }, + "codec": { "type": "string", "enum": ["opus", "aac"] }, "rate": { "type": "integer" }, "channels": { "type": "integer" } } diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go index eae900f2..135adfab 100644 --- a/pkg/cmd/streamplace.go +++ b/pkg/cmd/streamplace.go @@ -25,6 +25,7 @@ import ( urfavecli "github.com/urfave/cli/v3" "stream.place/streamplace/pkg/aqhttp" "stream.place/streamplace/pkg/atproto" + "stream.place/streamplace/pkg/blob" "stream.place/streamplace/pkg/bus" "stream.place/streamplace/pkg/director" "stream.place/streamplace/pkg/gstinit" @@ -42,7 +43,6 @@ import ( "stream.place/streamplace/pkg/statedb" "stream.place/streamplace/pkg/storage" "stream.place/streamplace/pkg/upload" - "stream.place/streamplace/pkg/blob" "stream.place/streamplace/pkg/vod" "github.com/aws/aws-sdk-go-v2/aws" diff --git a/pkg/gen/gen.go b/pkg/gen/gen.go index 7971c862..07a7649a 100644 --- a/pkg/gen/gen.go +++ b/pkg/gen/gen.go @@ -46,6 +46,7 @@ func main() { streamplace.Defs_ActivityLabel{}, streamplace.Video{}, streamplace.MediaTrack{}, + streamplace.MediaOrigin{}, streamplace.MediaDefs_MuxlTrack{}, streamplace.MediaDefs_SourceTracks{}, streamplace.MediaDefs_SourceClip{}, diff --git a/pkg/streamplace/cbor_gen.go b/pkg/streamplace/cbor_gen.go index 1af7d064..0345d213 100644 --- a/pkg/streamplace/cbor_gen.go +++ b/pkg/streamplace/cbor_gen.go @@ -9060,6 +9060,291 @@ func (t *MediaTrack) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *MediaOrigin) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{165}); err != nil { + return err + } + + // t.Blob (string) (string) + if len("blob") > 1000000 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if len(t.Blob) > 1000000 { + return xerrors.Errorf("Value in field t.Blob was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Blob))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Blob)); err != nil { + return err + } + + // t.Size (int64) (int64) + if len("size") > 1000000 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if t.Size >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Size-1)); err != nil { + return err + } + } + + // t.Urls ([]string) (slice) + if len("urls") > 1000000 { + return xerrors.Errorf("Value in field \"urls\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("urls"))); err != nil { + return err + } + if _, err := cw.WriteString(string("urls")); err != nil { + return err + } + + if len(t.Urls) > 8192 { + return xerrors.Errorf("Slice value in field t.Urls was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Urls))); err != nil { + return err + } + for _, v := range t.Urls { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.media.origin"))); err != nil { + return err + } + if _, err := cw.WriteString(string("place.stream.media.origin")); err != nil { + return err + } + + // t.MimeType (string) (string) + if len("mimeType") > 1000000 { + return xerrors.Errorf("Value in field \"mimeType\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mimeType"))); err != nil { + return err + } + if _, err := cw.WriteString(string("mimeType")); err != nil { + return err + } + + if len(t.MimeType) > 1000000 { + return xerrors.Errorf("Value in field t.MimeType was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.MimeType))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.MimeType)); err != nil { + return err + } + return nil +} + +func (t *MediaOrigin) UnmarshalCBOR(r io.Reader) (err error) { + *t = MediaOrigin{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("MediaOrigin: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 8) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (string) (string) + case "blob": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Blob = string(sval) + } + // t.Size (int64) (int64) + case "size": + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.Size = int64(extraI) + } + // t.Urls ([]string) (slice) + case "urls": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Urls: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Urls = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Urls[i] = string(sval) + } + + } + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.MimeType (string) (string) + case "mimeType": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.MimeType = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} func (t *MediaDefs_MuxlTrack) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) diff --git a/pkg/streamplace/mediaorigin.go b/pkg/streamplace/mediaorigin.go index 67293c69..80843994 100644 --- a/pkg/streamplace/mediaorigin.go +++ b/pkg/streamplace/mediaorigin.go @@ -4,14 +4,22 @@ package streamplace -// MediaOrigin is a "main" in the place.stream.media.origin schema. -// -// A record indicating that a MUXL blob is available for download somewhere. +import ( + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + lexutil.RegisterType("place.stream.media.origin", &MediaOrigin{}) +} + type MediaOrigin struct { - // blob: BLAKE-3 content hash (BDASL CID) of the source video segment. + LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.media.origin"` + // blob: BLAKE-3 content hash (BDASL CID) of the blob. Blob string `json:"blob" cborgen:"blob"` - // mimeType: MIME type of the file (e.g. video/mp4). + // mimeType: MIME type of the blob (e.g. video/mp4). MimeType string `json:"mimeType" cborgen:"mimeType"` - // size: Size of the file in bytes. + // size: Size of the blob in bytes. Size int64 `json:"size" cborgen:"size"` + // urls: Public URLs from which the blob can be fetched. Multiple entries enable transport diversity (HTTP, future iroh/libp2p schemes). + Urls []string `json:"urls" cborgen:"urls"` }