Load images, videos, audio, and stickers from your own backend into CE.SDK to integrate a CMS, DAM, or custom asset management system.
CE.SDK offers two ways to bring your server’s content into the engine. Choose the pattern that fits how your assets change.
Understanding Asset Source Types#
Custom asset sources suit database-backed content — user uploads, DAM integrations, CMS media. You implement the AssetSource protocol and provide a findAssets(queryData:) method that handles search and pagination against your API. Register it with engine.asset.addSource(_:).
JSON asset sources suit static content that rarely changes — stickers, icons, brand elements, templates. You host a JSON manifest alongside the assets and register it with engine.asset.addLocalAssetSourceFromJSON(_:). CE.SDK’s bundled libraries use this pattern; the Serve Assets From Your Server guide covers hosting them on your own infrastructure, and Asset Concepts explains how sources and assets fit together.
Creating a Custom Asset Source#
A custom source is a class that conforms to AssetSource: it exposes a unique id and implements findAssets(queryData:). The queryData carries the search query, the requested page, and perPage; return an AssetQueryResult with the current page of assets, the total count, the currentPage, and nextPage (use -1 when there are no more results).
func findAssets(queryData: AssetQueryData) async throws -> AssetQueryResult { let term = (queryData.query ?? "").lowercased() let matches = term.isEmpty ? catalog : catalog.filter { asset in (asset.label?.lowercased().contains(term) ?? false) || (asset.tags?.contains { $0.lowercased().contains(term) } ?? false) }
let start = queryData.page * queryData.perPage let page = Array(matches.dropFirst(start).prefix(queryData.perPage)) let hasMore = start + page.count < matches.count
return AssetQueryResult( assets: page, currentPage: queryData.page, nextPage: hasMore ? queryData.page + 1 : -1, total: matches.count, )}Register the source once, then query it through the Asset API:
let source = BackendAssetSource(id: "my-backend")try engine.asset.addSource(source)
let results = try await engine.asset.findAssets( sourceID: source.id, query: .init(query: "", page: 0, perPage: 10),)print("Loaded \(results.assets.count) of \(results.total) assets from the backend source")The example returns a fixed catalog so it runs without a network connection. In a real integration, findAssets(queryData:) builds a request from your backend’s base URL, forwards the query, and maps the decoded response into AssetResult values. Attach an Authorization header for protected endpoints, or sign the asset URLs themselves:
var components = URLComponents( url: host.appendingPathComponent("assets"), resolvingAgainstBaseURL: false,)!components.queryItems = [ URLQueryItem(name: "query", value: queryData.query), URLQueryItem(name: "page", value: String(queryData.page)), URLQueryItem(name: "perPage", value: String(queryData.perPage)),]
var request = URLRequest(url: components.url!)request.setValue("Bearer YOUR_API_TOKEN", forHTTPHeaderField: "Authorization")let (data, _) = try await URLSession.shared.data(for: request)let page = try JSONDecoder().decode(BackendAssetPage.self, from: data)
let assets = page.assets.map { item in AssetResult( id: item.id, label: item.label, meta: [ "uri": item.uri, "thumbUri": item.thumbUri, "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "width": String(item.width), "height": String(item.height), ], context: .init(sourceID: sourceID), )}
return AssetQueryResult( assets: assets, currentPage: page.currentPage, nextPage: page.nextPage ?? -1, total: page.total,)For a complete worked source that talks to a live REST API, see the Unsplash integration.
Serving Different Media Types#
Each media type needs the right blockType and fillType in its meta dictionary. Set thumbUri to a small preview and uri to the full-resolution asset.
Image Assets#
Images use a graphic block with an image fill. Include width and height so CE.SDK preserves the aspect ratio.
let image = AssetResult( id: "photo-1", label: "Mountain Landscape", tags: ["nature", "mountain", "landscape"], meta: [ "uri": "https://cdn.example.com/assets/photo-1.jpg", "thumbUri": "https://cdn.example.com/thumbs/photo-1.jpg", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "width": "1920", "height": "1080", ], context: .init(sourceID: id),)Video Assets#
Videos use a graphic block with a video fill. Include duration for the timeline and thumbUri for the preview.
let video = AssetResult( id: "clip-1", label: "City Timelapse", tags: ["city", "timelapse"], meta: [ "uri": "https://cdn.example.com/assets/clip-1.mp4", "thumbUri": "https://cdn.example.com/thumbs/clip-1.jpg", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.video.rawValue, "duration": "12.5", "width": "1920", "height": "1080", ], context: .init(sourceID: id),)Audio Assets#
Audio uses the //ly.img.ubq/audio block type. The mimeType is required for preview playback; include duration for the timeline.
let audio = AssetResult( id: "track-1", label: "Ambient Loop", tags: ["ambient", "loop"], meta: [ "uri": "https://cdn.example.com/assets/track-1.m4a", "thumbUri": "https://cdn.example.com/thumbs/track-1.jpg", "blockType": DesignBlockType.audio.rawValue, "mimeType": "audio/x-m4a", "duration": "30.0", ], context: .init(sourceID: id),)Sticker Assets#
Stickers share the image structure but add kind: "sticker", which marks them as overlays with limited editing options instead of regular images.
let sticker = AssetResult( id: "sticker-1", label: "Star Badge", tags: ["badge", "star"], meta: [ "uri": "https://cdn.example.com/assets/sticker-1.png", "thumbUri": "https://cdn.example.com/thumbs/sticker-1.png", "blockType": DesignBlockType.graphic.rawValue, "fillType": FillType.image.rawValue, "kind": "sticker", "width": "512", "height": "512", ], context: .init(sourceID: id),)Template Assets#
Templates replace the whole scene rather than adding a block, so they use a local source with a custom applyAsset callback that loads a .scene file. Return nil because applying a template mutates the current scene rather than creating a new block.
try engine.asset.addLocalSource(sourceID: "my-backend-templates", applyAsset: { [weak engine] asset in guard let engine, let uri = asset.meta?["uri"], let url = URL(string: uri) else { return nil } try await engine.scene.applyTemplate(from: url) return nil})
let template = AssetDefinition( id: "promo-card", meta: [ "uri": "https://cdn.example.com/templates/promo-card.scene", "thumbUri": "https://cdn.example.com/thumbs/promo-card.jpg", "blockType": DesignBlockType.scene.rawValue, ], label: ["en": "Promo Card"],)try engine.asset.addAsset(to: "my-backend-templates", asset: template)Loading Assets from JSON#
For static collections that rarely change, register a source from a JSON manifest. Each asset carries an id, a localized label, and a meta dictionary. The {{base_url}} placeholder resolves against the basePath you pass, so the manifest stays portable.
let manifest = """{ "version": "1.0.0", "id": "my-backend-stickers", "assets": [ { "id": "star-badge", "label": { "en": "Star Badge" }, "tags": { "en": ["badge", "star"] }, "meta": { "uri": "{{base_url}}/stickers/star-badge.png", "thumbUri": "{{base_url}}/stickers/star-badge.png", "blockType": "//ly.img.ubq/graphic", "fillType": "//ly.img.ubq/fill/image", "kind": "sticker", "width": "512", "height": "512" } } ]}"""let staticSourceID = try engine.asset.addLocalAssetSourceFromJSON( manifest, basePath: "https://cdn.example.com/assets",)print("Registered static source: \(staticSourceID)")addLocalAssetSourceFromJSON(_:basePath:matcher:) takes the manifest inline; the addLocalAssetSourceFromJSON(_:matcher:) overload loads a hosted content.json from a URL instead. See Serve Assets From Your Server for the manifest format and hosting details.
Server Architecture Considerations#
A few backend choices keep asset loading fast and reliable:
- Authentication — Pass tokens through your
findAssetsrequest headers, or hand out signed URLs that embed a temporary access token. - Thumbnails — Generate ~512px-wide thumbnails server-side and return them in
meta.thumbUri; keep the full-resolution asset inmeta.uri. - CDN and caching — Deliver assets through a CDN, with long cache TTLs for immutable thumbnails and shorter TTLs for content that changes.
Troubleshooting#
- Assets not appearing — Confirm
findAssets(queryData:)returns a validAssetQueryResultwithassets,total,currentPage, andnextPage, and that every asset has anidand ameta.uri. - Media not loading — Verify the
uriis reachable from the device. Apple platforms block plaintext HTTP by default, so serve assets over HTTPS or configure an App Transport Security exception. - Search returns nothing — Make sure
findAssets(queryData:)readsqueryData.queryand forwards the term to your backend. - Pagination stalls — Return
nextPage: -1once results are exhausted, and report the complete result count intotalrather than the current page size.
API Reference#
Methods#
| Method | Description |
|---|---|
engine.asset.addSource(_:) |
Register a custom AssetSource whose findAssets(queryData:) fetches from your backend. |
engine.asset.findAssets(sourceID:query:) |
Query a registered source for a page of results. |
engine.asset.addLocalSource(sourceID:applyAsset:) |
Register a source with a custom apply callback, used for templates. |
engine.asset.addAsset(to:asset:) |
Add an AssetDefinition to a local source. |
engine.asset.addLocalAssetSourceFromJSON(_:basePath:matcher:) |
Register a source from an inline JSON manifest string. |
engine.asset.addLocalAssetSourceFromJSON(_:matcher:) |
Register a source from a hosted content.json URL. |
Metadata Properties#
| Property | Description |
|---|---|
uri |
Full-resolution asset URL applied to the canvas. |
thumbUri |
Thumbnail URL shown in the asset library. |
blockType |
//ly.img.ubq/graphic for images, videos, and stickers; //ly.img.ubq/audio for audio. |
fillType |
//ly.img.ubq/fill/image or //ly.img.ubq/fill/video. |
kind |
Set to sticker to treat an image asset as a sticker overlay. |
mimeType |
Required for audio assets, for example audio/x-m4a. |
duration |
Length in seconds for video and audio. |
width / height |
Original dimensions used for aspect-ratio handling. |
Next Steps#
- User Upload — On iOS, handle file uploads with progress tracking
- Asset Concepts — Asset sources and metadata architecture
- Thumbnails — Configure thumbnail display and preview URIs
- Customize Asset Library — On iOS, present your registered sources in the asset library
- Serve Assets From Your Server — Host CE.SDK’s default and sample content on your own infrastructure