Live video on the AT Protocol

feat: add branding to header (meta tag) for preloading on web

+95 -9
+1 -1
pkg/api/api.go
··· 272 272 if err != nil { 273 273 return nil, err 274 274 } 275 - linker, err := linking.NewLinker(ctx, bs) 275 + linker, err := linking.NewLinker(ctx, bs, a.StatefulDB, a.CLI) 276 276 if err != nil { 277 277 return nil, err 278 278 }
+88 -2
pkg/linking/linking.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 + "encoding/json" 6 7 "errors" 7 8 "fmt" 9 + "log" 8 10 "net/url" 9 11 10 12 "golang.org/x/net/html" 13 + "stream.place/streamplace/pkg/config" 14 + "stream.place/streamplace/pkg/statedb" 11 15 "stream.place/streamplace/pkg/streamplace" 12 16 ) 13 17 14 18 type Linker struct { 15 19 BaseHTML []byte 20 + sdb *statedb.StatefulDB 21 + cli *config.CLI 16 22 } 17 23 18 - func NewLinker(ctx context.Context, baseHTML []byte) (*Linker, error) { 24 + func NewLinker(ctx context.Context, baseHTML []byte, sdb *statedb.StatefulDB, cli *config.CLI) (*Linker, error) { 19 25 _, err := html.Parse(bytes.NewReader(baseHTML)) 20 26 if err != nil { 21 27 return nil, err 22 28 } 23 29 24 - return &Linker{BaseHTML: baseHTML}, nil 30 + return &Linker{BaseHTML: baseHTML, sdb: sdb, cli: cli}, nil 25 31 } 26 32 27 33 type PageConfig struct { 28 34 Title string 29 35 Metas []MetaTag 30 36 SentryDSN string 37 + Branding []string 31 38 } 32 39 33 40 // Define all meta tags in a structured way ··· 35 42 Type string // "name" or "property" 36 43 Key string 37 44 Content string 45 + } 46 + 47 + var BrandingAssetList = [...]string{ 48 + "siteTitle", 49 + "siteDescription", 50 + "primaryColor", 51 + "accentColor", 52 + "defaultStreamer", 53 + "mainLogo", 54 + "favicon", 55 + "sidebarBg", 56 + "legalLinks", 57 + } 58 + 59 + // fetch branding assets for a given broadcaster DID 60 + func (l *Linker) getBrandingAssets(broadcasterDid string) ([]streamplace.BrandingGetBranding_BrandingAsset, error) { 61 + ret := make([]streamplace.BrandingGetBranding_BrandingAsset, 0) 62 + for _, asset := range BrandingAssetList { 63 + blob, err := l.sdb.GetBrandingBlob(broadcasterDid, asset) 64 + if err != nil { 65 + // this can probably include a 'record not found' error, in which case we skip 66 + log.Printf("error fetching branding asset %s for broadcaster %s: %v", asset, broadcasterDid, err) 67 + continue 68 + } 69 + asset := streamplace.BrandingGetBranding_BrandingAsset{ 70 + Key: blob.Key, 71 + MimeType: blob.MimeType, 72 + } 73 + 74 + if blob.Width != nil { 75 + w := int64(*blob.Width) 76 + asset.Width = &w 77 + } 78 + if blob.Height != nil { 79 + h := int64(*blob.Height) 80 + asset.Height = &h 81 + } 82 + 83 + // process based on mime type 84 + if blob.MimeType == "text/plain" { 85 + str := string(blob.Data) 86 + asset.Data = &str 87 + } else { 88 + url := fmt.Sprintf("/xrpc/place.stream.branding.getBlob?key=%s&broadcaster=%s", blob.Key, broadcasterDid) 89 + asset.Url = &url 90 + } 91 + ret = append(ret, asset) 92 + } 93 + 94 + return ret, nil 38 95 } 39 96 40 97 func (l *Linker) GenerateStreamerCard(ctx context.Context, u *url.URL, lsv *streamplace.Livestream_LivestreamView, sentryDSN string) ([]byte, error) { ··· 199 256 }) 200 257 } 201 258 259 + if l.sdb != nil && l.cli != nil { 260 + 261 + branding, err := l.getBrandingAssets("did:web:" + l.cli.BroadcasterHost) 262 + 263 + if err == nil { 264 + 265 + for i := range branding { 266 + val := branding[i] 267 + // 268 + marshalledJson, err := json.Marshal(val) 269 + if err != nil { 270 + fmt.Printf("error marshalling branding asset %s: %v\n", val.Key, err) 271 + continue 272 + } 273 + head.AppendChild(&html.Node{ 274 + Type: html.ElementNode, 275 + Data: "meta", 276 + Attr: []html.Attribute{ 277 + {Key: "name", Val: "internal-brand:" + val.Key}, 278 + {Key: "content", Val: string(marshalledJson)}, 279 + }, 280 + }) 281 + 282 + } 283 + } else { 284 + // log but we should not block rendering 285 + fmt.Printf("error fetching branding assets: %v\n", err) 286 + } 287 + } 202 288 // Render the HTML to a string 203 289 var buf bytes.Buffer 204 290 if err := html.Render(&buf, root); err != nil {
+2 -2
pkg/linking/linking_test.go
··· 29 29 30 30 func TestNewLinker(t *testing.T) { 31 31 index := IndexHTML(t) 32 - linker, err := NewLinker(context.Background(), index) 32 + linker, err := NewLinker(context.Background(), index, nil, nil) 33 33 require.NoError(t, err) 34 34 require.NotNil(t, linker) 35 35 } 36 36 37 37 func TestGenerateLinkCard(t *testing.T) { 38 38 index := IndexHTML(t) 39 - linker, err := NewLinker(context.Background(), index) 39 + linker, err := NewLinker(context.Background(), index, nil, nil) 40 40 require.NoError(t, err) 41 41 require.NotNil(t, linker) 42 42
+4 -4
pkg/spxrpc/place_stream_branding.go
··· 38 38 return s.cli.BroadcasterHost 39 39 } 40 40 41 - func (s *Server) getBrandingBlob(ctx context.Context, broadcasterID, key string) ([]byte, string, *int, *int, error) { 41 + func (s *Server) GetBrandingBlob(ctx context.Context, broadcasterID, key string) ([]byte, string, *int, *int, error) { 42 42 // cache miss - fetch from db 43 43 blob, err := s.statefulDB.GetBrandingBlob(broadcasterID, key) 44 44 if err == gorm.ErrRecordNotFound { ··· 61 61 // HandlePlaceStreamBrandingGetBlobDirect is the exported version for direct calls 62 62 func (s *Server) HandlePlaceStreamBrandingGetBlobDirect(ctx context.Context, broadcasterDID string, key string) (io.Reader, error) { 63 63 broadcasterID := s.getBroadcasterID(ctx, broadcasterDID) 64 - data, _, _, _, err := s.getBrandingBlob(ctx, broadcasterID, key) 64 + data, _, _, _, err := s.GetBrandingBlob(ctx, broadcasterID, key) 65 65 if err != nil { 66 66 return nil, err 67 67 } ··· 94 94 // build output 95 95 assets := make([]*placestreamtypes.BrandingGetBranding_BrandingAsset, 0, len(allKeys)) 96 96 for key := range allKeys { 97 - data, mimeType, width, height, err := s.getBrandingBlob(ctx, broadcasterID, key) 97 + data, mimeType, width, height, err := s.GetBrandingBlob(ctx, broadcasterID, key) 98 98 if err != nil { 99 99 continue // skip if error 100 100 } ··· 238 238 239 239 broadcasterID := s.cli.BroadcasterHost 240 240 log.Log(ctx, "fetching favicon", "broadcasterID", broadcasterID) 241 - data, mimeType, _, _, err := s.getBrandingBlob(ctx, "did:web:"+broadcasterID, "favicon") 241 + data, mimeType, _, _, err := s.GetBrandingBlob(ctx, "did:web:"+broadcasterID, "favicon") 242 242 243 243 if err != nil || data == nil { 244 244 log.Log(ctx, "using fallback favicon", "err", err, "data_nil", data == nil)