diff --git a/Makefile b/Makefile index fdfdae6f..a4fce7fe 100644 --- a/Makefile +++ b/Makefile @@ -398,6 +398,8 @@ go-lexicons: && echo 'func (t *MediaDefs_SourceClip) UnmarshalCBOR(r io.Reader) error { return nil }' >> pkg/streamplace/cbor_gen.go \ && echo 'func (t *Video_Connection) MarshalCBOR(w io.Writer) error { return nil }' >> pkg/streamplace/cbor_gen.go \ && echo 'func (t *Video_Connection) UnmarshalCBOR(r io.Reader) error { return nil }' >> pkg/streamplace/cbor_gen.go \ + && echo 'func (t *MediaTrack_CommonMetadata) MarshalCBOR(w io.Writer) error { return nil }' >> pkg/streamplace/cbor_gen.go \ + && echo 'func (t *MediaTrack_CommonMetadata) UnmarshalCBOR(r io.Reader) error { return nil }' >> pkg/streamplace/cbor_gen.go \ && sed -i.bak 's/\tlexutil\.RegisterType/\/\/\tlexutil.RegisterType/' $$(find ./pkg/streamplace -type f) \ && go run golang.org/x/tools/cmd/goimports@latest -w $$(find ./pkg/streamplace ./pkg/gamesgamesgamesgames -type f) \ && go run ./pkg/gen/gen_stubs.go \ diff --git a/js/docs/src/content/docs/lex-reference/media/place-stream-media-defs.md b/js/docs/src/content/docs/lex-reference/media/place-stream-media-defs.md index 212c9014..0ccfecb7 100644 --- a/js/docs/src/content/docs/lex-reference/media/place-stream-media-defs.md +++ b/js/docs/src/content/docs/lex-reference/media/place-stream-media-defs.md @@ -51,13 +51,14 @@ A track backed by a MUXL container **Properties:** -| Name | Type | Req'd | Description | Constraints | -| ------------ | -------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `blob` | `string` | ✅ | BLAKE-3 content hash (BDASL CID) of the source video segment. | | -| `trackId` | `string` | ✅ | ID of the track within the MUXL container. 1-indexed for MP4 reasons. | | -| `mediaType` | `string` | ✅ | Type of the track: video, audio, or text. | | -| `language` | `string` | ❌ | Language of the track, if applicable. | | -| `signingKey` | `string` | ❌ | did:key of the ephemeral key that C2PA-signed this track's segments. The private key is discarded once the track is produced, so it can only ever have signed this content. | Format: `did` | +| Name | Type | Req'd | Description | Constraints | +| ------------ | --------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `blob` | `string` | ✅ | BLAKE-3 content hash (BDASL CID) of the source video segment. | | +| `size` | `integer` | ❌ | Size in bytes of the source video segment. | | +| `trackId` | `string` | ✅ | ID of the track within the MUXL container. 1-indexed for MP4 reasons. | | +| `mediaType` | `string` | ✅ | Type of the track: video, audio, or text. | | +| `language` | `string` | ❌ | Language of the track, if applicable. | | +| `signingKey` | `string` | ❌ | did:key of the ephemeral key that C2PA-signed this track's segments. The private key is discarded once the track is produced, so it can only ever have signed this content. | Format: `did` | --- @@ -112,6 +113,10 @@ A track backed by a MUXL container "type": "string", "description": "BLAKE-3 content hash (BDASL CID) of the source video segment." }, + "size": { + "type": "integer", + "description": "Size in bytes of the source video segment." + }, "trackId": { "type": "string", "description": "ID of the track within the MUXL container. 1-indexed for MP4 reasons." diff --git a/js/docs/src/content/docs/lex-reference/media/place-stream-media-track.md b/js/docs/src/content/docs/lex-reference/media/place-stream-media-track.md index 9923de06..db48e267 100644 --- a/js/docs/src/content/docs/lex-reference/media/place-stream-media-track.md +++ b/js/docs/src/content/docs/lex-reference/media/place-stream-media-track.md @@ -13,7 +13,7 @@ description: Reference for the place.stream.media.track lexicon **Type:** `record` -A track for a video stream, either part of the source or a custom additional track. +A track for a video stream, either part of the source or a custom additional track. One of: video, audio, subtitles. **Record Key:** `tid` @@ -24,7 +24,7 @@ A track for a video stream, either part of the source or a custom additional tra | `track` | Union of:
  [`place.stream.media.defs#muxlTrack`](/lex-reference/place-stream-media-defs#muxltrack) | ✅ | | | | `video` | `string` | ❌ | If this is a derived track like a transcode or a transcript, what was the source place.stream.video? | Format: `at-uri` | | `parentTrack` | [`com.atproto.repo.strongRef`](https://github.com/bluesky-social/atproto/tree/main/lexicons/com/atproto/repo/strongref.json#undefined) | ❌ | If this is a derived track like a transcode or a transcript, what was the parent track? | | -| `metadata` | [`place.stream.media.track#commonMetadata`](/lex-reference/place-stream-media-track#commonmetadata) | ❌ | | | +| `metadata` | Union of:
  [`place.stream.media.track#commonMetadata`](/lex-reference/place-stream-media-track#commonmetadata) | ❌ | | | --- @@ -41,6 +41,7 @@ Metadata common to all media types. Contains subobjects for other media types. | Name | Type | Req'd | Description | Constraints | | ---------- | ------------------------------------------------------------------------- | ----- | ------------------------------------------------------------------- | ----------- | | `language` | `string` | ❌ | IETF BCP 47 language tag corresponding to the content of this track | | +| `duration` | `integer` | ❌ | duration of this track in milliseconds | | | `video` | [`place.stream.segment#video`](/lex-reference/place-stream-segment#video) | ❌ | | | | `audio` | [`place.stream.segment#audio`](/lex-reference/place-stream-segment#audio) | ❌ | | | @@ -55,7 +56,7 @@ Metadata common to all media types. Contains subobjects for other media types. "defs": { "main": { "type": "record", - "description": "A track for a video stream, either part of the source or a custom additional track.", + "description": "A track for a video stream, either part of the source or a custom additional track. One of: video, audio, subtitles.", "key": "tid", "record": { "required": ["track"], @@ -76,8 +77,8 @@ Metadata common to all media types. Contains subobjects for other media types. "description": "If this is a derived track like a transcode or a transcript, what was the parent track?" }, "metadata": { - "type": "ref", - "ref": "place.stream.media.track#commonMetadata" + "type": "union", + "refs": ["place.stream.media.track#commonMetadata"] } } } @@ -91,6 +92,10 @@ Metadata common to all media types. Contains subobjects for other media types. "type": "string", "description": "IETF BCP 47 language tag corresponding to the content of this track" }, + "duration": { + "type": "integer", + "description": "duration of this track in milliseconds" + }, "video": { "type": "ref", "ref": "place.stream.segment#video" diff --git a/js/docs/src/content/docs/lex-reference/place-stream-video.md b/js/docs/src/content/docs/lex-reference/place-stream-video.md index daff58c9..03683afb 100644 --- a/js/docs/src/content/docs/lex-reference/place-stream-video.md +++ b/js/docs/src/content/docs/lex-reference/place-stream-video.md @@ -22,13 +22,14 @@ Some audiovisual content. | Name | Type | Req'd | Description | Constraints | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `title` | `string` | ✅ | Title of the video referenced by this record | Max Length: 1400
Max Graphemes: 140 | -| `source` | Union of:
  [`place.stream.media.defs#sourceTracks`](/lex-reference/place-stream-media-defs#sourcetracks)
  [`place.stream.media.defs#sourceClip`](/lex-reference/place-stream-media-defs#sourceclip) | ❌ | What is the source of this video? | | +| `source` | Union of:
  [`place.stream.media.defs#sourceTracks`](/lex-reference/place-stream-media-defs#sourcetracks)
  [`place.stream.media.defs#sourceClip`](/lex-reference/place-stream-media-defs#sourceclip) | ✅ | What is the source of this video? | | | `description` | `string` | ❌ | Description of this video | Max Length: 50000
Max Graphemes: 5000 | -| `duration` | `integer` | ❌ | Duration of the video in milliseconds | | -| `descriptionFacets` | Array of [`place.stream.richtext.facet`](/lex-reference/place-stream-richtext-facet) | ❌ | Annotations of text (mentions, URLs, etc) | | +| `duration` | `integer` | ✅ | Duration of the video in milliseconds | | +| `descriptionFacets` | Array of [`place.stream.richtext.videoFacet`](/lex-reference/place-stream-richtext-videofacet) | ❌ | Annotations of text (mentions, URLs, etc) | | | `thumb` | `blob` | ❌ | Thumbnail image for the video. | Accept: `image/*`
Max Size: 1000000 bytes | | `connections` | Array of Union of:
  [`#connection`](#connection) | ❌ | Free-form list of atproto records related in some way to this video | | -| `contentPolicy` | [`place.stream.metadata.configuration`](/lex-reference/place-stream-metadata-configuration) | ❌ | copyright, distribution, and content warning data | | +| `contentWarnings` | [`place.stream.metadata.contentWarnings`](/lex-reference/place-stream-metadata-contentwarnings) | ❌ | content warning data for this VOD | | +| `contentRights` | [`place.stream.metadata.contentRights`](/lex-reference/place-stream-metadata-contentrights) | ❌ | copyright and licensing information for this VOD | | | `activity` | Union of:
  [`place.stream.defs#activityGame`](/lex-reference/place-stream-defs#activitygame)
  [`place.stream.defs#activityLabel`](/lex-reference/place-stream-defs#activitylabel) | ❌ | The game or activity in the video. | | | `tags` | Array of `string` | ❌ | Freeform tags for this stream. Each tag must be alphanumeric (a-z, A-Z, 0-9) plus colon. Tags with colons indicate a specific tag group (e.g. 'lang:en' indicates the stream's primary language). | Max Items: 10 | @@ -48,20 +49,6 @@ Some audiovisual content. --- - - -### `videoSourceContent` - -**Type:** `object` - -**Properties:** - -| Name | Type | Req'd | Description | Constraints | -| ----- | -------------------------------------------------------------------------------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------- | ----------- | -| `ref` | [`com.atproto.repo.strongRef`](https://github.com/bluesky-social/atproto/tree/main/lexicons/com/atproto/repo/strongref.json#undefined) | ❌ | place.stream.media.source record providing the content for this video record | | - ---- - ## Lexicon Source ```json @@ -74,7 +61,7 @@ Some audiovisual content. "description": "Some audiovisual content.", "key": "tid", "record": { - "required": ["title"], + "required": ["title", "source", "duration"], "type": "object", "properties": { "title": { @@ -106,7 +93,7 @@ Some audiovisual content. "description": "Annotations of text (mentions, URLs, etc)", "items": { "type": "ref", - "ref": "place.stream.richtext.facet" + "ref": "place.stream.richtext.videoFacet" } }, "thumb": { @@ -123,10 +110,15 @@ Some audiovisual content. "refs": ["#connection"] } }, - "contentPolicy": { - "description": "copyright, distribution, and content warning data", + "contentWarnings": { + "description": "content warning data for this VOD", "type": "ref", - "ref": "place.stream.metadata.configuration" + "ref": "place.stream.metadata.contentWarnings" + }, + "contentRights": { + "description": "copyright and licensing information for this VOD", + "type": "ref", + "ref": "place.stream.metadata.contentRights" }, "activity": { "type": "union", @@ -157,16 +149,6 @@ Some audiovisual content. "ref": "com.atproto.repo.strongRef" } } - }, - "videoSourceContent": { - "type": "object", - "properties": { - "ref": { - "description": "place.stream.media.source record providing the content for this video record", - "type": "ref", - "ref": "com.atproto.repo.strongRef" - } - } } } } diff --git a/js/docs/src/content/docs/lex-reference/richtext/place-stream-richtext-facet.md b/js/docs/src/content/docs/lex-reference/richtext/place-stream-richtext-facet.md index cebfc538..c8e8f3e6 100644 --- a/js/docs/src/content/docs/lex-reference/richtext/place-stream-richtext-facet.md +++ b/js/docs/src/content/docs/lex-reference/richtext/place-stream-richtext-facet.md @@ -13,7 +13,7 @@ description: Reference for the place.stream.richtext.facet lexicon **Type:** `object` -Annotation of a sub-string within rich text. +Annotation of a sub-string within rich text in a livestream chat message. **Properties:** @@ -33,7 +33,7 @@ Annotation of a sub-string within rich text. "defs": { "main": { "type": "object", - "description": "Annotation of a sub-string within rich text.", + "description": "Annotation of a sub-string within rich text in a livestream chat message.", "required": ["index", "features"], "properties": { "index": { diff --git a/js/docs/src/content/docs/lex-reference/richtext/place-stream-richtext-videofacet.md b/js/docs/src/content/docs/lex-reference/richtext/place-stream-richtext-videofacet.md new file mode 100644 index 00000000..ef687c91 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/richtext/place-stream-richtext-videofacet.md @@ -0,0 +1,57 @@ +--- +title: place.stream.richtext.videoFacet +description: Reference for the place.stream.richtext.videoFacet lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `object` + +Annotation of a sub-string within rich text in a VOD description or comment. + +**Properties:** + +| Name | Type | Req'd | Description | Constraints | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ----------- | ----------- | +| `index` | [`app.bsky.richtext.facet#byteSlice`](https://github.com/bluesky-social/atproto/tree/main/lexicons/app/bsky/richtext/facet.json#byteSlice) | ✅ | | | +| `features` | Array of Union of:
  [`app.bsky.richtext.facet#mention`](https://github.com/bluesky-social/atproto/tree/main/lexicons/app/bsky/richtext/facet.json#mention)
  [`app.bsky.richtext.facet#link`](https://github.com/bluesky-social/atproto/tree/main/lexicons/app/bsky/richtext/facet.json#link) | ✅ | | | + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.richtext.videoFacet", + "defs": { + "main": { + "type": "object", + "description": "Annotation of a sub-string within rich text in a VOD description or comment.", + "required": ["index", "features"], + "properties": { + "index": { + "type": "ref", + "ref": "app.bsky.richtext.facet#byteSlice" + }, + "features": { + "type": "array", + "items": { + "type": "union", + "refs": [ + "app.bsky.richtext.facet#mention", + "app.bsky.richtext.facet#link" + ] + } + } + } + } + } +} +``` diff --git a/lexicons/place/stream/media/defs.json b/lexicons/place/stream/media/defs.json index 9ac68466..f7b4a974 100644 --- a/lexicons/place/stream/media/defs.json +++ b/lexicons/place/stream/media/defs.json @@ -46,6 +46,10 @@ "type": "string", "description": "BLAKE-3 content hash (BDASL CID) of the source video segment." }, + "size": { + "type": "integer", + "description": "Size in bytes of the source video segment." + }, "trackId": { "type": "string", "description": "ID of the track within the MUXL container. 1-indexed for MP4 reasons." diff --git a/lexicons/place/stream/media/track.json b/lexicons/place/stream/media/track.json index 0e6d2d18..e40f459d 100644 --- a/lexicons/place/stream/media/track.json +++ b/lexicons/place/stream/media/track.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "record", - "description": "A track for a video stream, either part of the source or a custom additional track.", + "description": "A track for a video stream, either part of the source or a custom additional track. One of: video, audio, subtitles.", "key": "tid", "record": { "required": ["track"], @@ -25,8 +25,8 @@ "description": "If this is a derived track like a transcode or a transcript, what was the parent track?" }, "metadata": { - "type": "ref", - "ref": "place.stream.media.track#commonMetadata" + "type": "union", + "refs": ["place.stream.media.track#commonMetadata"] } } } @@ -40,6 +40,10 @@ "type": "string", "description": "IETF BCP 47 language tag corresponding to the content of this track" }, + "duration": { + "type": "integer", + "description": "duration of this track in milliseconds" + }, "video": { "type": "ref", "ref": "place.stream.segment#video" diff --git a/lexicons/place/stream/richtext/facet.json b/lexicons/place/stream/richtext/facet.json index 9a461e55..09c0af82 100644 --- a/lexicons/place/stream/richtext/facet.json +++ b/lexicons/place/stream/richtext/facet.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "object", - "description": "Annotation of a sub-string within rich text.", + "description": "Annotation of a sub-string within rich text in a livestream chat message.", "required": ["index", "features"], "properties": { "index": { "type": "ref", "ref": "app.bsky.richtext.facet#byteSlice" }, diff --git a/lexicons/place/stream/richtext/videoFacet.json b/lexicons/place/stream/richtext/videoFacet.json new file mode 100644 index 00000000..a0e0ef13 --- /dev/null +++ b/lexicons/place/stream/richtext/videoFacet.json @@ -0,0 +1,24 @@ +{ + "lexicon": 1, + "id": "place.stream.richtext.videoFacet", + "defs": { + "main": { + "type": "object", + "description": "Annotation of a sub-string within rich text in a VOD description or comment.", + "required": ["index", "features"], + "properties": { + "index": { "type": "ref", "ref": "app.bsky.richtext.facet#byteSlice" }, + "features": { + "type": "array", + "items": { + "type": "union", + "refs": [ + "app.bsky.richtext.facet#mention", + "app.bsky.richtext.facet#link" + ] + } + } + } + } + } +} diff --git a/lexicons/place/stream/video.json b/lexicons/place/stream/video.json index 82083500..9d72c809 100644 --- a/lexicons/place/stream/video.json +++ b/lexicons/place/stream/video.json @@ -7,7 +7,7 @@ "description": "Some audiovisual content.", "key": "tid", "record": { - "required": ["title"], + "required": ["title", "source", "duration"], "type": "object", "properties": { "title": { @@ -37,7 +37,10 @@ "descriptionFacets": { "type": "array", "description": "Annotations of text (mentions, URLs, etc)", - "items": { "type": "ref", "ref": "place.stream.richtext.facet" } + "items": { + "type": "ref", + "ref": "place.stream.richtext.videoFacet" + } }, "thumb": { "description": "Thumbnail image for the video.", @@ -53,10 +56,15 @@ "refs": ["#connection"] } }, - "contentPolicy": { - "description": "copyright, distribution, and content warning data", + "contentWarnings": { + "description": "content warning data for this VOD", + "type": "ref", + "ref": "place.stream.metadata.contentWarnings" + }, + "contentRights": { + "description": "copyright and licensing information for this VOD", "type": "ref", - "ref": "place.stream.metadata.configuration" + "ref": "place.stream.metadata.contentRights" }, "activity": { "type": "union", @@ -87,16 +95,6 @@ "ref": "com.atproto.repo.strongRef" } } - }, - "videoSourceContent": { - "type": "object", - "properties": { - "ref": { - "description": "place.stream.media.source record providing the content for this video record", - "type": "ref", - "ref": "com.atproto.repo.strongRef" - } - } } } } diff --git a/pkg/atproto/sync.go b/pkg/atproto/sync.go index 7b9965d2..7ba19bf5 100644 --- a/pkg/atproto/sync.go +++ b/pkg/atproto/sync.go @@ -740,9 +740,7 @@ func (atsync *ATProtoSynchronizer) handleCreateUpdate(ctx context.Context, userD Record: *recCBOR, IndexedAt: now, } - if rec.Duration != nil { - v.DurationMS = rec.Duration - } + v.DurationMS = &rec.Duration if err := atsync.Model.UpsertVideo(ctx, v); err != nil { return fmt.Errorf("failed to upsert video: %w", err) } diff --git a/pkg/gen/gen.go b/pkg/gen/gen.go index 7fe38235..5b9299f0 100644 --- a/pkg/gen/gen.go +++ b/pkg/gen/gen.go @@ -21,6 +21,7 @@ func main() { streamplace.Segment_Framerate{}, streamplace.ChatMessage{}, streamplace.RichtextFacet{}, + streamplace.RichtextVideoFacet{}, streamplace.ChatProfile{}, streamplace.ChatProfile_BadgeSelections{}, streamplace.ChatProfile_StreamerBadgeSelection{}, diff --git a/pkg/streamplace/cbor_gen.go b/pkg/streamplace/cbor_gen.go index 89866aac..89b85e3b 100644 --- a/pkg/streamplace/cbor_gen.go +++ b/pkg/streamplace/cbor_gen.go @@ -2979,6 +2979,183 @@ func (t *RichtextFacet) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *RichtextVideoFacet) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Index (bsky.RichtextFacet_ByteSlice) (struct) + if len("index") > 1000000 { + return xerrors.Errorf("Value in field \"index\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("index"))); err != nil { + return err + } + if _, err := cw.WriteString(string("index")); err != nil { + return err + } + + if err := t.Index.MarshalCBOR(cw); err != nil { + return err + } + + // t.Features ([]*streamplace.RichtextVideoFacet_Features_Elem) (slice) + if len("features") > 1000000 { + return xerrors.Errorf("Value in field \"features\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("features"))); err != nil { + return err + } + if _, err := cw.WriteString(string("features")); err != nil { + return err + } + + if len(t.Features) > 8192 { + return xerrors.Errorf("Slice value in field t.Features was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Features))); err != nil { + return err + } + for _, v := range t.Features { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + return nil +} + +func (t *RichtextVideoFacet) UnmarshalCBOR(r io.Reader) (err error) { + *t = RichtextVideoFacet{} + + 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("RichtextVideoFacet: 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.Index (bsky.RichtextFacet_ByteSlice) (struct) + case "index": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Index = new(bsky.RichtextFacet_ByteSlice) + if err := t.Index.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Index pointer: %w", err) + } + } + + } + // t.Features ([]*streamplace.RichtextVideoFacet_Features_Elem) (slice) + case "features": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Features: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Features = make([]*RichtextVideoFacet_Features_Elem, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Features[i] = new(RichtextVideoFacet_Features_Elem) + if err := t.Features[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Features[i] pointer: %w", err) + } + } + + } + + } + } + + 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 *ChatProfile) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) @@ -8115,7 +8292,7 @@ func (t *Video) MarshalCBOR(w io.Writer) error { } cw := cbg.NewCborWriter(w) - fieldCount := 11 + fieldCount := 12 if t.Activity == nil { fieldCount-- @@ -8125,23 +8302,19 @@ func (t *Video) MarshalCBOR(w io.Writer) error { fieldCount-- } - if t.ContentPolicy == nil { - fieldCount-- - } - - if t.Description == nil { + if t.ContentRights == nil { fieldCount-- } - if t.DescriptionFacets == nil { + if t.ContentWarnings == nil { fieldCount-- } - if t.Duration == nil { + if t.Description == nil { fieldCount-- } - if t.Source == nil { + if t.DescriptionFacets == nil { fieldCount-- } @@ -8255,22 +8428,19 @@ func (t *Video) MarshalCBOR(w io.Writer) error { } // t.Source (streamplace.Video_Source) (struct) - if t.Source != nil { - - if len("source") > 1000000 { - return xerrors.Errorf("Value in field \"source\" was too long") - } + if len("source") > 1000000 { + return xerrors.Errorf("Value in field \"source\" was too long") + } - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil { - return err - } - if _, err := cw.WriteString(string("source")); err != nil { - return err - } + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil { + return err + } + if _, err := cw.WriteString(string("source")); err != nil { + return err + } - if err := t.Source.MarshalCBOR(cw); err != nil { - return err - } + if err := t.Source.MarshalCBOR(cw); err != nil { + return err } // t.Activity (streamplace.Video_Activity) (struct) @@ -8293,35 +8463,25 @@ func (t *Video) MarshalCBOR(w io.Writer) error { } // t.Duration (int64) (int64) - if t.Duration != nil { + if len("duration") > 1000000 { + return xerrors.Errorf("Value in field \"duration\" was too long") + } - if len("duration") > 1000000 { - return xerrors.Errorf("Value in field \"duration\" was too long") - } + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("duration"))); err != nil { + return err + } + if _, err := cw.WriteString(string("duration")); err != nil { + return err + } - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("duration"))); err != nil { + if t.Duration >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Duration)); err != nil { return err } - if _, err := cw.WriteString(string("duration")); err != nil { + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Duration-1)); err != nil { return err } - - if t.Duration == nil { - if _, err := cw.Write(cbg.CborNull); err != nil { - return err - } - } else { - if *t.Duration >= 0 { - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.Duration)); err != nil { - return err - } - } else { - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.Duration-1)); err != nil { - return err - } - } - } - } // t.Connections ([]*streamplace.Video_Connections_Elem) (slice) @@ -8385,26 +8545,45 @@ func (t *Video) MarshalCBOR(w io.Writer) error { } } - // t.ContentPolicy (streamplace.MetadataConfiguration) (struct) - if t.ContentPolicy != nil { + // t.ContentRights (streamplace.MetadataContentRights) (struct) + if t.ContentRights != nil { - if len("contentPolicy") > 1000000 { - return xerrors.Errorf("Value in field \"contentPolicy\" was too long") + if len("contentRights") > 1000000 { + return xerrors.Errorf("Value in field \"contentRights\" was too long") } - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("contentPolicy"))); err != nil { + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("contentRights"))); err != nil { return err } - if _, err := cw.WriteString(string("contentPolicy")); err != nil { + if _, err := cw.WriteString(string("contentRights")); err != nil { return err } - if err := t.ContentPolicy.MarshalCBOR(cw); err != nil { + if err := t.ContentRights.MarshalCBOR(cw); err != nil { return err } } - // t.DescriptionFacets ([]*streamplace.RichtextFacet) (slice) + // t.ContentWarnings (streamplace.MetadataContentWarnings) (struct) + if t.ContentWarnings != nil { + + if len("contentWarnings") > 1000000 { + return xerrors.Errorf("Value in field \"contentWarnings\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("contentWarnings"))); err != nil { + return err + } + if _, err := cw.WriteString(string("contentWarnings")); err != nil { + return err + } + + if err := t.ContentWarnings.MarshalCBOR(cw); err != nil { + return err + } + } + + // t.DescriptionFacets ([]*streamplace.RichtextVideoFacet) (slice) if t.DescriptionFacets != nil { if len("descriptionFacets") > 1000000 { @@ -8601,38 +8780,28 @@ func (t *Video) UnmarshalCBOR(r io.Reader) (err error) { // t.Duration (int64) (int64) case "duration": { - - b, err := cr.ReadByte() + maj, extra, err := cr.ReadHeader() if err != nil { return err } - if b != cbg.CborNull[0] { - if err := cr.UnreadByte(); err != nil { - return err - } - 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") } - 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) + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") } - - t.Duration = (*int64)(&extraI) + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) } + + t.Duration = int64(extraI) } // t.Connections ([]*streamplace.Video_Connections_Elem) (slice) case "connections": @@ -8704,8 +8873,8 @@ func (t *Video) UnmarshalCBOR(r io.Reader) (err error) { t.Description = (*string)(&sval) } } - // t.ContentPolicy (streamplace.MetadataConfiguration) (struct) - case "contentPolicy": + // t.ContentRights (streamplace.MetadataContentRights) (struct) + case "contentRights": { @@ -8717,14 +8886,34 @@ func (t *Video) UnmarshalCBOR(r io.Reader) (err error) { if err := cr.UnreadByte(); err != nil { return err } - t.ContentPolicy = new(MetadataConfiguration) - if err := t.ContentPolicy.UnmarshalCBOR(cr); err != nil { - return xerrors.Errorf("unmarshaling t.ContentPolicy pointer: %w", err) + t.ContentRights = new(MetadataContentRights) + if err := t.ContentRights.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.ContentRights pointer: %w", err) } } } - // t.DescriptionFacets ([]*streamplace.RichtextFacet) (slice) + // t.ContentWarnings (streamplace.MetadataContentWarnings) (struct) + case "contentWarnings": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.ContentWarnings = new(MetadataContentWarnings) + if err := t.ContentWarnings.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.ContentWarnings pointer: %w", err) + } + } + + } + // t.DescriptionFacets ([]*streamplace.RichtextVideoFacet) (slice) case "descriptionFacets": maj, extra, err = cr.ReadHeader() @@ -8741,7 +8930,7 @@ func (t *Video) UnmarshalCBOR(r io.Reader) (err error) { } if extra > 0 { - t.DescriptionFacets = make([]*RichtextFacet, extra) + t.DescriptionFacets = make([]*RichtextVideoFacet, extra) } for i := 0; i < int(extra); i++ { @@ -8763,7 +8952,7 @@ func (t *Video) UnmarshalCBOR(r io.Reader) (err error) { if err := cr.UnreadByte(); err != nil { return err } - t.DescriptionFacets[i] = new(RichtextFacet) + t.DescriptionFacets[i] = new(RichtextVideoFacet) if err := t.DescriptionFacets[i].UnmarshalCBOR(cr); err != nil { return xerrors.Errorf("unmarshaling t.DescriptionFacets[i] pointer: %w", err) } @@ -8876,7 +9065,7 @@ func (t *MediaTrack) MarshalCBOR(w io.Writer) error { } } - // t.Metadata (streamplace.MediaTrack_CommonMetadata) (struct) + // t.Metadata (streamplace.MediaTrack_Metadata) (struct) if t.Metadata != nil { if len("metadata") > 1000000 { @@ -9009,7 +9198,7 @@ func (t *MediaTrack) UnmarshalCBOR(r io.Reader) (err error) { t.Video = (*string)(&sval) } } - // t.Metadata (streamplace.MediaTrack_CommonMetadata) (struct) + // t.Metadata (streamplace.MediaTrack_Metadata) (struct) case "metadata": { @@ -9022,7 +9211,7 @@ func (t *MediaTrack) UnmarshalCBOR(r io.Reader) (err error) { if err := cr.UnreadByte(); err != nil { return err } - t.Metadata = new(MediaTrack_CommonMetadata) + t.Metadata = new(MediaTrack_Metadata) if err := t.Metadata.UnmarshalCBOR(cr); err != nil { return xerrors.Errorf("unmarshaling t.Metadata pointer: %w", err) } @@ -9279,7 +9468,7 @@ func (t *MediaDefs_MuxlTrack) MarshalCBOR(w io.Writer) error { } cw := cbg.NewCborWriter(w) - fieldCount := 6 + fieldCount := 7 if t.Language == nil { fieldCount-- @@ -9289,6 +9478,10 @@ func (t *MediaDefs_MuxlTrack) MarshalCBOR(w io.Writer) error { fieldCount-- } + if t.Size == nil { + fieldCount-- + } + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { return err } @@ -9316,6 +9509,38 @@ func (t *MediaDefs_MuxlTrack) MarshalCBOR(w io.Writer) error { return err } + // t.Size (int64) (int64) + if t.Size != nil { + + 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 == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + 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.LexiconTypeID (string) (string) if len("$type") > 1000000 { return xerrors.Errorf("Value in field \"$type\" was too long") @@ -9499,6 +9724,42 @@ func (t *MediaDefs_MuxlTrack) UnmarshalCBOR(r io.Reader) (err error) { t.Blob = string(sval) } + // t.Size (int64) (int64) + case "size": + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + 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.LexiconTypeID (string) (string) case "$type": @@ -9989,12 +10250,16 @@ func (t *MediaTrack_CommonMetadata) MarshalCBOR(w io.Writer) error { } cw := cbg.NewCborWriter(w) - fieldCount := 3 + fieldCount := 5 if t.Audio == nil { fieldCount-- } + if t.Duration == nil { + fieldCount-- + } + if t.Language == nil { fieldCount-- } @@ -10007,6 +10272,25 @@ func (t *MediaTrack_CommonMetadata) MarshalCBOR(w io.Writer) error { 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.track#commonMetadata"))); err != nil { + return err + } + if _, err := cw.WriteString(string("place.stream.media.track#commonMetadata")); err != nil { + return err + } + // t.Audio (streamplace.Segment_Audio) (struct) if t.Audio != nil { @@ -10045,6 +10329,38 @@ func (t *MediaTrack_CommonMetadata) MarshalCBOR(w io.Writer) error { } } + // t.Duration (int64) (int64) + if t.Duration != nil { + + if len("duration") > 1000000 { + return xerrors.Errorf("Value in field \"duration\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("duration"))); err != nil { + return err + } + if _, err := cw.WriteString(string("duration")); err != nil { + return err + } + + if t.Duration == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if *t.Duration >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.Duration)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.Duration-1)); err != nil { + return err + } + } + } + + } + // t.Language (string) (string) if t.Language != nil { @@ -10120,7 +10436,18 @@ func (t *MediaTrack_CommonMetadata) UnmarshalCBOR(r io.Reader) (err error) { } switch string(nameBuf[:nameLen]) { - // t.Audio (streamplace.Segment_Audio) (struct) + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Audio (streamplace.Segment_Audio) (struct) case "audio": { @@ -10160,6 +10487,42 @@ func (t *MediaTrack_CommonMetadata) UnmarshalCBOR(r io.Reader) (err error) { } } + // t.Duration (int64) (int64) + case "duration": + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + 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.Duration = (*int64)(&extraI) + } + } // t.Language (string) (string) case "language": diff --git a/pkg/streamplace/mediadefs.go b/pkg/streamplace/mediadefs.go index 18688416..7461866f 100644 --- a/pkg/streamplace/mediadefs.go +++ b/pkg/streamplace/mediadefs.go @@ -21,6 +21,8 @@ type MediaDefs_MuxlTrack struct { MediaType string `json:"mediaType" cborgen:"mediaType"` // signingKey: did:key of the ephemeral key that C2PA-signed this track's segments. The private key is discarded once the track is produced, so it can only ever have signed this content. SigningKey *string `json:"signingKey,omitempty" cborgen:"signingKey,omitempty"` + // size: Size in bytes of the source video segment. + Size *int64 `json:"size,omitempty" cborgen:"size,omitempty"` // trackId: ID of the track within the MUXL container. 1-indexed for MP4 reasons. TrackId string `json:"trackId" cborgen:"trackId"` } diff --git a/pkg/streamplace/mediatrack.go b/pkg/streamplace/mediatrack.go index 8198653c..e98d9553 100644 --- a/pkg/streamplace/mediatrack.go +++ b/pkg/streamplace/mediatrack.go @@ -20,8 +20,8 @@ func init() { } type MediaTrack struct { - LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.media.track"` - Metadata *MediaTrack_CommonMetadata `json:"metadata,omitempty" cborgen:"metadata,omitempty"` + LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.media.track"` + Metadata *MediaTrack_Metadata `json:"metadata,omitempty" cborgen:"metadata,omitempty"` // parentTrack: If this is a derived track like a transcode or a transcript, what was the parent track? ParentTrack *comatproto.RepoStrongRef `json:"parentTrack,omitempty" cborgen:"parentTrack,omitempty"` Track *MediaTrack_Track `json:"track" cborgen:"track"` @@ -33,12 +33,69 @@ type MediaTrack struct { // // Metadata common to all media types. Contains subobjects for other media types. type MediaTrack_CommonMetadata struct { - Audio *Segment_Audio `json:"audio,omitempty" cborgen:"audio,omitempty"` + LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.media.track#commonMetadata"` + Audio *Segment_Audio `json:"audio,omitempty" cborgen:"audio,omitempty"` + // duration: duration of this track in milliseconds + Duration *int64 `json:"duration,omitempty" cborgen:"duration,omitempty"` // language: IETF BCP 47 language tag corresponding to the content of this track Language *string `json:"language,omitempty" cborgen:"language,omitempty"` Video *Segment_Video `json:"video,omitempty" cborgen:"video,omitempty"` } +type MediaTrack_Metadata struct { + MediaTrack_CommonMetadata *MediaTrack_CommonMetadata +} + +func (t *MediaTrack_Metadata) MarshalJSON() ([]byte, error) { + if t.MediaTrack_CommonMetadata != nil { + t.MediaTrack_CommonMetadata.LexiconTypeID = "place.stream.media.track#commonMetadata" + return json.Marshal(t.MediaTrack_CommonMetadata) + } + return nil, fmt.Errorf("can not marshal empty union as JSON") +} + +func (t *MediaTrack_Metadata) UnmarshalJSON(b []byte) error { + typ, err := lexutil.TypeExtract(b) + if err != nil { + return err + } + + switch typ { + case "place.stream.media.track#commonMetadata": + t.MediaTrack_CommonMetadata = new(MediaTrack_CommonMetadata) + return json.Unmarshal(b, t.MediaTrack_CommonMetadata) + default: + return nil + } +} + +func (t *MediaTrack_Metadata) MarshalCBOR(w io.Writer) error { + + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + if t.MediaTrack_CommonMetadata != nil { + return t.MediaTrack_CommonMetadata.MarshalCBOR(w) + } + return fmt.Errorf("can not marshal empty union as CBOR") +} + +func (t *MediaTrack_Metadata) UnmarshalCBOR(r io.Reader) error { + typ, b, err := lexutil.CborTypeExtractReader(r) + if err != nil { + return err + } + + switch typ { + case "place.stream.media.track#commonMetadata": + t.MediaTrack_CommonMetadata = new(MediaTrack_CommonMetadata) + return t.MediaTrack_CommonMetadata.UnmarshalCBOR(bytes.NewReader(b)) + default: + return nil + } +} + type MediaTrack_Track struct { MediaDefs_MuxlTrack *MediaDefs_MuxlTrack } diff --git a/pkg/streamplace/richtextfacet.go b/pkg/streamplace/richtextfacet.go index 6061bea4..7953e87a 100644 --- a/pkg/streamplace/richtextfacet.go +++ b/pkg/streamplace/richtextfacet.go @@ -17,7 +17,7 @@ import ( // RichtextFacet is a "main" in the place.stream.richtext.facet schema. // -// Annotation of a sub-string within rich text. +// Annotation of a sub-string within rich text in a livestream chat message. type RichtextFacet struct { Features []*RichtextFacet_Features_Elem `json:"features" cborgen:"features"` Index *appbsky.RichtextFacet_ByteSlice `json:"index" cborgen:"index"` diff --git a/pkg/streamplace/richtextvideoFacet.go b/pkg/streamplace/richtextvideoFacet.go new file mode 100644 index 00000000..5bd89ee3 --- /dev/null +++ b/pkg/streamplace/richtextvideoFacet.go @@ -0,0 +1,92 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: place.stream.richtext.videoFacet + +package streamplace + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + + appbsky "github.com/bluesky-social/indigo/api/bsky" + lexutil "github.com/bluesky-social/indigo/lex/util" + cbg "github.com/whyrusleeping/cbor-gen" +) + +// RichtextVideoFacet is a "main" in the place.stream.richtext.videoFacet schema. +// +// Annotation of a sub-string within rich text in a VOD description or comment. +type RichtextVideoFacet struct { + Features []*RichtextVideoFacet_Features_Elem `json:"features" cborgen:"features"` + Index *appbsky.RichtextFacet_ByteSlice `json:"index" cborgen:"index"` +} + +type RichtextVideoFacet_Features_Elem struct { + RichtextFacet_Mention *appbsky.RichtextFacet_Mention + RichtextFacet_Link *appbsky.RichtextFacet_Link +} + +func (t *RichtextVideoFacet_Features_Elem) MarshalJSON() ([]byte, error) { + if t.RichtextFacet_Mention != nil { + t.RichtextFacet_Mention.LexiconTypeID = "app.bsky.richtext.facet#mention" + return json.Marshal(t.RichtextFacet_Mention) + } + if t.RichtextFacet_Link != nil { + t.RichtextFacet_Link.LexiconTypeID = "app.bsky.richtext.facet#link" + return json.Marshal(t.RichtextFacet_Link) + } + return nil, fmt.Errorf("can not marshal empty union as JSON") +} + +func (t *RichtextVideoFacet_Features_Elem) UnmarshalJSON(b []byte) error { + typ, err := lexutil.TypeExtract(b) + if err != nil { + return err + } + + switch typ { + case "app.bsky.richtext.facet#mention": + t.RichtextFacet_Mention = new(appbsky.RichtextFacet_Mention) + return json.Unmarshal(b, t.RichtextFacet_Mention) + case "app.bsky.richtext.facet#link": + t.RichtextFacet_Link = new(appbsky.RichtextFacet_Link) + return json.Unmarshal(b, t.RichtextFacet_Link) + default: + return nil + } +} + +func (t *RichtextVideoFacet_Features_Elem) MarshalCBOR(w io.Writer) error { + + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + if t.RichtextFacet_Mention != nil { + return t.RichtextFacet_Mention.MarshalCBOR(w) + } + if t.RichtextFacet_Link != nil { + return t.RichtextFacet_Link.MarshalCBOR(w) + } + return fmt.Errorf("can not marshal empty union as CBOR") +} + +func (t *RichtextVideoFacet_Features_Elem) UnmarshalCBOR(r io.Reader) error { + typ, b, err := lexutil.CborTypeExtractReader(r) + if err != nil { + return err + } + + switch typ { + case "app.bsky.richtext.facet#mention": + t.RichtextFacet_Mention = new(appbsky.RichtextFacet_Mention) + return t.RichtextFacet_Mention.UnmarshalCBOR(bytes.NewReader(b)) + case "app.bsky.richtext.facet#link": + t.RichtextFacet_Link = new(appbsky.RichtextFacet_Link) + return t.RichtextFacet_Link.UnmarshalCBOR(bytes.NewReader(b)) + default: + return nil + } +} diff --git a/pkg/streamplace/streamvideo.go b/pkg/streamplace/streamvideo.go index 778828e6..28d0e28e 100644 --- a/pkg/streamplace/streamvideo.go +++ b/pkg/streamplace/streamvideo.go @@ -25,16 +25,18 @@ type Video struct { Activity *Video_Activity `json:"activity,omitempty" cborgen:"activity,omitempty"` // connections: Free-form list of atproto records related in some way to this video Connections []*Video_Connections_Elem `json:"connections,omitempty" cborgen:"connections,omitempty"` - // contentPolicy: copyright, distribution, and content warning data - ContentPolicy *MetadataConfiguration `json:"contentPolicy,omitempty" cborgen:"contentPolicy,omitempty"` + // contentRights: copyright and licensing information for this VOD + ContentRights *MetadataContentRights `json:"contentRights,omitempty" cborgen:"contentRights,omitempty"` + // contentWarnings: content warning data for this VOD + ContentWarnings *MetadataContentWarnings `json:"contentWarnings,omitempty" cborgen:"contentWarnings,omitempty"` // description: Description of this video Description *string `json:"description,omitempty" cborgen:"description,omitempty"` // descriptionFacets: Annotations of text (mentions, URLs, etc) - DescriptionFacets []*RichtextFacet `json:"descriptionFacets,omitempty" cborgen:"descriptionFacets,omitempty"` + DescriptionFacets []*RichtextVideoFacet `json:"descriptionFacets,omitempty" cborgen:"descriptionFacets,omitempty"` // duration: Duration of the video in milliseconds - Duration *int64 `json:"duration,omitempty" cborgen:"duration,omitempty"` + Duration int64 `json:"duration" cborgen:"duration"` // source: What is the source of this video? - Source *Video_Source `json:"source,omitempty" cborgen:"source,omitempty"` + Source *Video_Source `json:"source" cborgen:"source"` // tags: Freeform tags for this stream. Each tag must be alphanumeric (a-z, A-Z, 0-9) plus colon. Tags with colons indicate a specific tag group (e.g. 'lang:en' indicates the stream's primary language). Tags []string `json:"tags,omitempty" cborgen:"tags,omitempty"` // thumb: Thumbnail image for the video. @@ -240,9 +242,3 @@ func (t *Video_Source) UnmarshalCBOR(r io.Reader) error { return nil } } - -// Video_VideoSourceContent is a "videoSourceContent" in the place.stream.video schema. -type Video_VideoSourceContent struct { - // ref: place.stream.media.source record providing the content for this video record - Ref *comatproto.RepoStrongRef `json:"ref,omitempty" cborgen:"ref,omitempty"` -} diff --git a/pkg/vod/publish.go b/pkg/vod/publish.go index d9ecd258..1373a826 100644 --- a/pkg/vod/publish.go +++ b/pkg/vod/publish.go @@ -84,7 +84,7 @@ func publishRecords(ctx context.Context, p publishParams) error { var sourceTracks []*comatproto.RepoStrongRef if p.probe.Video != nil { - ref, err := publishTrack(ctx, client, p.in.RepoDID, p.cid, "1", "video", p.signingKey, p.probe.Video, nil) + ref, err := publishTrack(ctx, client, p.in.RepoDID, p.cid, p.size, p.probe.DurationMS, "1", "video", p.signingKey, p.probe.Video, nil) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "video_track") @@ -93,7 +93,7 @@ func publishRecords(ctx context.Context, p publishParams) error { sourceTracks = append(sourceTracks, ref) } if p.probe.Audio != nil { - ref, err := publishTrack(ctx, client, p.in.RepoDID, p.cid, "2", "audio", p.signingKey, nil, p.probe.Audio) + ref, err := publishTrack(ctx, client, p.in.RepoDID, p.cid, p.size, p.probe.DurationMS, "2", "audio", p.signingKey, nil, p.probe.Audio) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "audio_track") @@ -149,11 +149,14 @@ func publishOrigin(ctx context.Context, cli *config.CLI, cid string, size int64, // publishTrack creates one place.stream.media.track record in the // user's repo. trackID is the 1-indexed track within the MUXL // container ("1" for video, "2" for audio in our standard layout). +// blobSize is the byte size of the shared MUXL container blob; +// durationMS is the source duration in milliseconds (same for every +// track of one upload since they all read from the same container). // signingKey is the did:key whose ephemeral private half C2PA-signed // this upload's segments — the same key signs every track of an upload. // Exactly one of videoMeta / audioMeta should be non-nil; the other // is ignored. -func publishTrack(ctx context.Context, client XRPCClient, did, cid, trackID, mediaType, signingKey string, videoMeta *media.VODVideoTrack, audioMeta *media.VODAudioTrack) (*comatproto.RepoStrongRef, error) { +func publishTrack(ctx context.Context, client XRPCClient, did, cid string, blobSize, durationMS int64, trackID, mediaType, signingKey string, videoMeta *media.VODVideoTrack, audioMeta *media.VODAudioTrack) (*comatproto.RepoStrongRef, error) { ctx, span := vodTracer.Start(ctx, "vod.publishTrack", trace.WithAttributes( attribute.String("cid", cid), attribute.String("track_id", trackID), @@ -161,7 +164,10 @@ func publishTrack(ctx context.Context, client XRPCClient, did, cid, trackID, med )) defer span.End() - meta := &streamplace.MediaTrack_CommonMetadata{} + meta := &streamplace.MediaTrack_CommonMetadata{ + LexiconTypeID: "place.stream.media.track#commonMetadata", + Duration: &durationMS, + } if videoMeta != nil { meta.Video = &streamplace.Segment_Video{ Codec: "h264", @@ -189,12 +195,15 @@ func publishTrack(ctx context.Context, client XRPCClient, did, cid, trackID, med MediaDefs_MuxlTrack: &streamplace.MediaDefs_MuxlTrack{ LexiconTypeID: "place.stream.media.defs#muxlTrack", Blob: cid, + Size: &blobSize, TrackId: trackID, MediaType: mediaType, SigningKey: &signingKey, }, }, - Metadata: meta, + Metadata: &streamplace.MediaTrack_Metadata{ + MediaTrack_CommonMetadata: meta, + }, } rkey := spid.TIDClock.Next().String() @@ -247,6 +256,7 @@ func publishVideo(ctx context.Context, client XRPCClient, in Input, probe media. rec := &streamplace.Video{ LexiconTypeID: constants.PLACE_STREAM_VIDEO, Title: title, + Duration: duration, Source: &streamplace.Video_Source{ MediaDefs_SourceTracks: &streamplace.MediaDefs_SourceTracks{ LexiconTypeID: "place.stream.media.defs#sourceTracks", @@ -254,9 +264,6 @@ func publishVideo(ctx context.Context, client XRPCClient, in Input, probe media. }, }, } - if duration > 0 { - rec.Duration = &duration - } if len(thumbnail) > 0 { var uploadOut comatproto.RepoUploadBlob_Output