Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
11 kB · 303 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304package media
import ( "context" "encoding/json" "fmt"
"stream.place/streamplace/pkg/aqtime" "stream.place/streamplace/pkg/config" "stream.place/streamplace/pkg/constants" "stream.place/streamplace/pkg/log" "stream.place/streamplace/pkg/model" "stream.place/streamplace/pkg/placestream")
// ManifestBuilder is responsible for creating C2PA (Content Credentials) manifests// for livestream segments.// The builder creates manifests that include:// - Basic livestream information (title, creator, date)// - Content rights and copyright information// - Content warnings for sensitive material// - Distribution policies// - C2PA action history (created, published)// The manifest is meant to align closely with the IPTC Video Metadata Recommendations.// See https://iptc.org/std/videometadatahub/recommendation/IPTC-VideoMetadataHub-props-Rec_1.6.htmltype ManifestBuilder struct { model model.Model cli *config.CLI}
func NewManifestBuilder(model model.Model, cli *config.CLI) *ManifestBuilder { return &ManifestBuilder{ model: model, cli: cli, }}
func toObj(record any) (obj, error) { jsonBs, err := json.Marshal(record) if err != nil { return nil, fmt.Errorf("failed to marshal record: %w", err) } var o obj err = json.Unmarshal(jsonBs, &o) if err != nil { return nil, fmt.Errorf("failed to unmarshal record: %w", err) } return o, nil}
func (mb *ManifestBuilder) getLivestream(ctx context.Context, streamerName string) (*placestream.Livestream, error) { if mb.model == nil { return nil, fmt.Errorf("model is nil") } livestream, err := mb.model.GetLatestLivestreamForRepo(streamerName) if err != nil { return nil, fmt.Errorf("failed to retrieve livestream: %w", err) } if livestream == nil { return nil, nil } livestreamRecord, err := livestream.ToLivestreamView() if err != nil { return nil, fmt.Errorf("failed to convert livestream to view: %w", err) } ls, ok := livestreamRecord.Record.Val.(*placestream.Livestream) if !ok { return nil, fmt.Errorf("livestream is not a streamplace livestream") } return ls, nil}
func (mb *ManifestBuilder) BuildManifest(ctx context.Context, streamerName string, start int64) ([]byte, error) { log.Debug(ctx, "🔍 BuildManifest ENTRY", "streamer", streamerName, "start", start)
shouldPublish := false
// Add livestream title if available livestreamTitle := "unpublished livestream" // default fallback ls, err := mb.getLivestream(ctx, streamerName) if err != nil { return nil, fmt.Errorf("failed to get livestream: %w", err) } if ls != nil { livestreamTitle = ls.Title shouldPublish = ls.EndedAt == nil } // for testing only if mb.cli.WideOpen { shouldPublish = true }
// Start with base manifest startTime := aqtime.FromMillis(start).String() actions := []obj{ { "action": "c2pa.created", "when": startTime, }, } if shouldPublish { actions = append(actions, obj{ "action": "c2pa.published", "when": startTime, }) } mani := obj{ "title": fmt.Sprintf("Livestream Segment at %s", startTime), "assertions": []obj{ // Required by spec, just basic info { "label": "c2pa.actions", "data": obj{ "actions": actions, }, }, // Content metadata, with extra custom fields added later { "label": "cawg.metadata", "data": obj{ "@context": obj{ "dc": "http://purl.org/dc/elements/1.1/", "Iptc4xmpExt": "http://iptc.org/std/Iptc4xmpExt/2008-02-29/", "photoshop": "http://ns.adobe.com/photoshop/1.0/", "xmpRights": "http://ns.adobe.com/xap/1.0/rights/", }, "dc:creator": streamerName, "dc:title": "livestream", "dc:date": startTime, }, }, }, }
// Add database metadata if available var streamplaceMetadata placestream.MetadataConfiguration if mb.model != nil { metadata, err := mb.model.GetMetadataConfiguration(ctx, streamerName) if err != nil { log.Warn(ctx, "ManifestBuilder: failed to retrieve metadata", "error", err, "did", streamerName) return nil, fmt.Errorf("failed to retrieve metadata: %w", err) } else if metadata != nil { log.Debug(ctx, "ManifestBuilder: found metadata configuration", "did", streamerName, "metadata", metadata) converted, err := metadata.ToStreamplaceMetadataConfiguration() if err != nil { log.Warn(ctx, "ManifestBuilder: failed to convert metadata, using defaults", "error", err, "did", streamerName) } else { log.Debug(ctx, "ManifestBuilder: enhancing manifest with metadata", "did", streamerName, "contentWarnings", converted.ContentWarnings, "contentRights", converted.ContentRights) streamplaceMetadata = converted mani = mb.enhanceManifestWithMetadata(mani, &streamplaceMetadata, start) } } else { log.Debug(ctx, "ManifestBuilder: no metadata configuration found for streamer", "did", streamerName) } }
// Generative AI training is opt-in: unless the streamer has explicitly // allowed it, every minted segment carries an explicit "not allowed". if streamplaceMetadata.DistributionPolicy == nil { streamplaceMetadata.DistributionPolicy = &placestream.MetadataDistributionPolicy{} } if streamplaceMetadata.DistributionPolicy.AllowGenAiTraining == nil { notAllowed := false streamplaceMetadata.DistributionPolicy.AllowGenAiTraining = ¬Allowed } metadataObj, err := toObj(streamplaceMetadata) if err != nil { return nil, fmt.Errorf("failed to marshal metadata: %w", err) } mani["assertions"] = append(mani["assertions"].([]obj), obj{ "label": "place.stream.metadata.configuration", "data": metadataObj, }, trainingMiningAssertion(*streamplaceMetadata.DistributionPolicy.AllowGenAiTraining), )
// Update the manifest title with the retrieved livestream title mani["assertions"].([]obj)[1]["data"].(obj)["dc:title"] = livestreamTitle
if ls != nil { mani["assertions"] = append(mani["assertions"].([]obj), obj{ "label": "place.stream.livestream", "data": ls, }) } else { log.Warn(ctx, "ManifestBuilder: no livestream found for streamer", "did", streamerName) }
// Convert manifest to JSON bytes for use with Rust c2pa library manifestBs, err := json.Marshal(mani) if err != nil { return nil, fmt.Errorf("failed to marshal manifest: %w", err) }
return manifestBs, nil}
// trainingMiningAssertion builds the CAWG training and data mining assertion// (https://cawg.io/training-and-data-mining/1.1/), the standard machine-readable// way to declare whether content may be used to train generative AI models.func trainingMiningAssertion(allowGenAiTraining bool) obj { use := "notAllowed" if allowGenAiTraining { use = "allowed" } return obj{ "label": "cawg.training-mining", "data": obj{ "entries": obj{ "cawg.ai_generative_training": obj{"use": use}, }, }, }}
// getLicenseCodeMap returns a map of internal license codes to their corresponding URLsfunc getLicenseCodeMap() map[string]string { return map[string]string{ constants.LicenseCC0_1_0: constants.LicenseURLCC0_1_0, constants.LicenseCCBy_4_0: constants.LicenseURLCCBy_4_0, constants.LicenseCCBySA_4_0: constants.LicenseURLCCBySA_4_0, constants.LicenseCCByNC_4_0: constants.LicenseURLCCByNC_4_0, constants.LicenseCCByNCSA_4_0: constants.LicenseURLCCByNCSA_4_0, constants.LicenseCCByND_4_0: constants.LicenseURLCCByND_4_0, constants.LicenseCCByNCND_4_0: constants.LicenseURLCCByNCND_4_0, }}
// getWarningCodeMap returns a map of internal warning codes to their corresponding C2PA codesfunc getWarningCodeMap() map[string]string { return map[string]string{ constants.WarningDeath: constants.WarningC2PADeath, constants.WarningDrugUse: constants.WarningC2PADrugUse, constants.WarningFantasyViolence: constants.WarningC2PAFantasyViolence, constants.WarningFlashingLights: constants.WarningC2PAFlashingLights, constants.WarningLanguage: constants.WarningC2PALanguage, constants.WarningNudity: constants.WarningC2PANudity, constants.WarningPII: constants.WarningC2PAPII, constants.WarningSexuality: constants.WarningC2PASexuality, constants.WarningSuffering: constants.WarningC2PASuffering, constants.WarningViolence: constants.WarningC2PAViolence, }}
func (mb *ManifestBuilder) enhanceManifestWithMetadata(mani obj, metadata *placestream.MetadataConfiguration, startTimeMillis int64) obj { if metadata.ContentRights != nil { // TODO: We are currently validating the creator in the ValidateMP4 function to be the streamer DID // if metadata.ContentRights.Creator != nil { // mani["assertions"].([]obj)[1]["data"].(obj)["dc:creator"] = *metadata.ContentRights.Creator // }
// Copyright Notice if metadata.ContentRights.CopyrightNotice != nil { mani["assertions"].([]obj)[1]["data"].(obj)["dc:rights"] = *metadata.ContentRights.CopyrightNotice }
// Copyright Year if metadata.ContentRights.CopyrightYear != nil { mani["assertions"].([]obj)[1]["data"].(obj)["Iptc4xmpExt:CopyrightYear"] = *metadata.ContentRights.CopyrightYear }
// Credit Line if metadata.ContentRights.CreditLine != nil { mani["assertions"].([]obj)[1]["data"].(obj)["photoshop:Credit"] = *metadata.ContentRights.CreditLine }
// Build the license field if metadata.ContentRights.License != nil { // Map internal license codes to known licenses licenseCodeMap := getLicenseCodeMap() if mappedCode, exists := licenseCodeMap[*metadata.ContentRights.License]; exists { // it's a known linked license, so we can use the mapped code mani["assertions"].([]obj)[1]["data"].(obj)["Iptc4xmpExt:LinkedEncRightsExpr"] = mappedCode } else { // This is either an unknown or an unlinked license, so we need to put it in the UsageTerms field // which allows for licensing terms expressed in free text if *metadata.ContentRights.License == constants.LicenseAllRightsReserved { // if all rights reserved, we can put the string "All rights reserved" in the UsageTerms field mani["assertions"].([]obj)[1]["data"].(obj)["xmpRights:UsageTerms"] = "All rights reserved" } else { // it's an unknown license, so we need to put it directly in the UsageTerms field mani["assertions"].([]obj)[1]["data"].(obj)["xmpRights:UsageTerms"] = *metadata.ContentRights.License } } } }
if metadata.ContentWarnings != nil && len(metadata.ContentWarnings.Warnings) > 0 { // Map internal warning codes to C2PA warning codes warningCodeMap := getWarningCodeMap()
for i, warning := range metadata.ContentWarnings.Warnings { if mappedCode, exists := warningCodeMap[warning]; exists { metadata.ContentWarnings.Warnings[i] = mappedCode } // Unknown warnings remain unchanged } mani["assertions"].([]obj)[1]["data"].(obj)["Iptc4xmpExt:ContentWarning"] = metadata.ContentWarnings.Warnings }
return mani}