Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,24 @@ class ReferencedAssetLoader {

scope.launch {
try {
val bytes = dataSource.createLoader().load(dataSource)
val bytes = when (dataSource) {
is DataSource.Http -> {
// Check cache first for URL assets
val cachedData = URLAssetCache.getCachedData(dataSource.url)
if (cachedData != null) {
cachedData
} else {
// Download and cache
val downloadedData = dataSource.createLoader().load(dataSource)
URLAssetCache.saveToCache(dataSource.url, downloadedData)
downloadedData
}
}
else -> {
// For non-URL assets, use the loader directly
dataSource.createLoader().load(dataSource)
}
}
withContext(Dispatchers.Main) {
processAssetBytes(bytes, asset)
deferred.complete(Unit)
Expand Down Expand Up @@ -82,19 +99,23 @@ class ReferencedAssetLoader {
return object : FileAssetLoader() {
override fun loadContents(asset: FileAsset, inBandBytes: ByteArray): Boolean {
var key = asset.uniqueFilename.substringBeforeLast(".")
cache[key] = asset
cache[asset.name] = asset
var assetData = assetsData[key]

if (assetData == null) {
key = asset.name
assetData = assetsData[asset.name]
}

if (assetData == null && asset.cdnUrl != null) {
assetData = ResolvedReferencedAsset(sourceUrl = asset.cdnUrl, sourceAsset = null, image = null, sourceAssetId = null, path = null)
}

if (assetData == null) {
return false
}

cache[key] = asset

loadAsset(assetData, asset)

return true
Expand Down
65 changes: 65 additions & 0 deletions android/src/main/java/com/margelo/nitro/rive/URLAssetCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.margelo.nitro.rive

import android.content.Context
import android.util.Log
import com.margelo.nitro.NitroModules
import java.io.File
import java.security.MessageDigest

object URLAssetCache {
private const val CACHE_DIR_NAME = "rive_url_assets"
private const val TAG = "URLAssetCache"

private fun getCacheDir(): File? {
val context = NitroModules.applicationContext ?: return null
val cacheDir = File(context.cacheDir, CACHE_DIR_NAME)
if (!cacheDir.exists()) {
cacheDir.mkdirs()
}
return cacheDir
}

private fun urlToCacheKey(url: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hashBytes = digest.digest(url.toByteArray())
return hashBytes.joinToString("") { "%02x".format(it) }
}

private fun getCacheFile(url: String): File? {
val cacheDir = getCacheDir() ?: return null
val cacheKey = urlToCacheKey(url)
return File(cacheDir, cacheKey)
}

fun getCachedData(url: String): ByteArray? {
return try {
val cacheFile = getCacheFile(url) ?: return null
if (cacheFile.exists() && cacheFile.length() > 0) {
cacheFile.readBytes()
} else {
null
}
} catch (e: Exception) {
Log.e(TAG, "Failed to read from cache: ${e.message}")
null
}
}

fun saveToCache(url: String, data: ByteArray) {
try {
val cacheFile = getCacheFile(url) ?: return
cacheFile.writeBytes(data)
} catch (e: Exception) {
Log.e(TAG, "Failed to save to cache: ${e.message}")
}
}

fun clearCache() {
try {
val cacheDir = getCacheDir() ?: return
cacheDir.listFiles()?.forEach { it.delete() }
} catch (e: Exception) {
Log.e(TAG, "Failed to clear cache: ${e.message}")
}
}
}
43 changes: 36 additions & 7 deletions ios/ReferencedAssetLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,23 @@ final class ReferencedAssetLoader {

Task {
do {
let data = try await dataSource.createLoader().load(from: dataSource)
let data: Data

// Check cache first for URL assets
if case .http(let url) = dataSource {
if let cachedData = URLAssetCache.getCachedData(for: url.absoluteString) {
data = cachedData
} else {
// Download and cache
let downloadedData = try await dataSource.createLoader().load(from: dataSource)
URLAssetCache.saveToCache(downloadedData, for: url.absoluteString)
data = downloadedData
}
} else {
// For non-URL assets, use the loader directly
data = try await dataSource.createLoader().load(from: dataSource)
}

await MainActor.run {
self.processAssetBytes(data, asset: asset, factory: factory, completion: completion)
}
Expand All @@ -120,19 +136,32 @@ final class ReferencedAssetLoader {
)
-> LoadAsset?
{
guard let referencedAssets = referencedAssets, let referencedAssets = referencedAssets.data
guard let referencedAssets = referencedAssets, let assetsData = referencedAssets.data
else {
return nil
}
return { (asset: RiveFileAsset, _: Data, factory: RiveFactory) -> Bool in
let assetByUniqueName = referencedAssets[asset.uniqueName()]
guard let assetData = assetByUniqueName ?? referencedAssets[asset.name()] else {
return false
}

cache.value[asset.uniqueName()] = asset
cache.value[asset.name()] = asset
factoryOut.value = factory

// Look up asset data by unique name or name
var key = (asset.uniqueName() as NSString).deletingPathExtension
var assetData = assetsData[key]

if assetData == nil {
key = asset.name()
assetData = assetsData[key]
}

if assetData == nil && !asset.cdnUuid().isEmpty {
assetData = ResolvedReferencedAsset(sourceUrl: "\(asset.cdnBaseUrl())/\(asset.cdnUuid())", sourceAsset: nil, sourceAssetId: nil, path: nil, image: nil)
}

guard let assetData = assetData else {
return false
}

self.loadAssetInternal(
source: assetData, asset: asset, factory: factory,
completion: {
Expand Down
71 changes: 71 additions & 0 deletions ios/URLAssetCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation
import CryptoKit

enum URLAssetCache {
private static let cacheDirName = "rive_url_assets"

private static func getCacheDirectory() -> URL? {
guard let cacheDir = FileManager.default.urls(
for: .cachesDirectory,
in: .userDomainMask
).first else {
return nil
}
let riveCacheDir = cacheDir.appendingPathComponent(cacheDirName)

// Create directory if it doesn't exist
try? FileManager.default.createDirectory(
at: riveCacheDir,
withIntermediateDirectories: true,
attributes: nil
)

return riveCacheDir
}

private static func urlToCacheKey(_ url: String) -> String {
let data = Data(url.utf8)
let hash = SHA256.hash(data: data)
return hash.compactMap { String(format: "%02x", $0) }.joined()
}

private static func getCacheFileURL(for url: String) -> URL? {
guard let cacheDir = getCacheDirectory() else { return nil }
let cacheKey = urlToCacheKey(url)
return cacheDir.appendingPathComponent(cacheKey)
}

static func getCachedData(for url: String) -> Data? {
guard let cacheFileURL = getCacheFileURL(for: url) else { return nil }

do {
let data = try Data(contentsOf: cacheFileURL)
return data.isEmpty ? nil : data
} catch {
return nil
}
}

static func saveToCache(_ data: Data, for url: String) {
guard let cacheFileURL = getCacheFileURL(for: url) else { return }

do {
try data.write(to: cacheFileURL)
} catch {
// Silently fail - caching is best effort
}
}

static func clearCache() {
guard let cacheDir = getCacheDirectory() else { return }

do {
let files = try FileManager.default.contentsOfDirectory(at: cacheDir, includingPropertiesForKeys: nil)
for file in files {
try? FileManager.default.removeItem(at: file)
}
} catch {
// Silently fail
}
}
}
Loading