Skip to content

Commit c41d0dd

Browse files
authored
feat: support loading assets from file:// (#205)
1 parent bd8a6ac commit c41d0dd

File tree

10 files changed

+156
-24
lines changed

10 files changed

+156
-24
lines changed

package/android/src/main/java/com/margelo/filament/FilamentProxy.java

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.margelo.filament;
22

3+
import android.content.ContentResolver;
34
import android.content.Context;
45
import android.content.res.Resources;
56
import android.net.Uri;
@@ -22,6 +23,8 @@
2223
import com.facebook.react.uimanager.UIManagerHelper;
2324

2425
import java.io.ByteArrayOutputStream;
26+
import java.io.File;
27+
import java.io.FileInputStream;
2528
import java.io.IOException;
2629
import java.io.InputStream;
2730
import java.nio.ByteBuffer;
@@ -114,22 +117,30 @@ private ByteBuffer streamToDirectByteBuffer(InputStream stream) throws IOExcepti
114117
*/
115118
@DoNotStrip
116119
@Keep
117-
ByteBuffer loadAsset(String url) throws Exception {
118-
Log.i(NAME, "Loading byte data from URL: " + url + "...");
120+
ByteBuffer loadAsset(String uriString) throws Exception {
121+
Log.i(NAME, "Loading byte data from URL: " + uriString + "...");
122+
123+
// It's a file path, read the file system directly
124+
if (uriString.contains("file://")) {
125+
String filePath = uriString.replace("file://", "");
126+
File file = new File(filePath);
127+
if (!file.exists()) {
128+
throw new Exception("File does not exist: " + filePath);
129+
}
130+
try (FileInputStream stream = new FileInputStream(file)) {
131+
return streamToDirectByteBuffer(stream);
132+
} catch (Exception e) {
133+
Log.e(NAME, "Failed to read file: " + filePath, e);
134+
throw e;
135+
}
136+
}
119137

120-
Uri uri = null;
121-
String assetName = null;
122-
if (url.contains("://")) {
138+
// It's a URL/http resource
139+
if (uriString.contains("http://") || uriString.contains("https://")) {
123140
Log.i(NAME, "Parsing URL...");
124-
uri = Uri.parse(url);
141+
Uri uri = Uri.parse(uriString);
125142
Log.i(NAME, "Parsed URL: " + uri.toString());
126-
} else {
127-
assetName = url;
128-
Log.i(NAME, "Assumed assetName: " + assetName);
129-
}
130143

131-
if (uri != null) {
132-
// It's a URL/http resource
133144
Request request = new Request.Builder().url(uri.toString()).build();
134145
try (Response response = client.newCall(request).execute()) {
135146
if (response.isSuccessful() && response.body() != null) {
@@ -139,18 +150,15 @@ ByteBuffer loadAsset(String url) throws Exception {
139150
throw new RuntimeException("Response was not successful!");
140151
}
141152
} catch (Exception ex) {
142-
Log.e(NAME, "Failed to fetch URL " + url + "!", ex);
153+
Log.e(NAME, "Failed to fetch URL " + uriString + "!", ex);
143154
throw ex;
144155
}
145-
} else if (assetName != null) {
146-
// It's bundled into the Android resources/assets
147-
try (InputStream stream = reactContext.getAssets().open(assetName)) {
148-
return streamToDirectByteBuffer(stream);
149-
}
150-
} else {
151-
// It's a bird? it's a plane? not it's an error
152-
throw new Exception("Input is neither a valid URL, nor an asset path - " +
153-
"cannot load asset! (Input: " + url + ")");
156+
}
157+
158+
// It's bundled into the Android resources/assets
159+
Log.i(NAME, "Assumed assetName: " + uriString);
160+
try (InputStream stream = reactContext.getAssets().open(uriString)) {
161+
return streamToDirectByteBuffer(stream);
154162
}
155163
}
156164

package/example/AppExampleFabric/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"react-native-screens": "^3.31.1",
2424
"react-native-video": "^6.1.2",
2525
"react-native-worklets-core": "^2.0.0-beta.4",
26+
"rn-fetch-blob": "^0.12.0",
2627
"shared": "workspace:^"
2728
},
2829
"devDependencies": {

package/example/AppExamplePaper/ios/Podfile.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,6 +1480,8 @@ PODS:
14801480
- React-logger (= 0.74.1)
14811481
- React-perflogger (= 0.74.1)
14821482
- React-utils (= 0.74.1)
1483+
- rn-fetch-blob (0.12.0):
1484+
- React-Core
14831485
- RNGestureHandler (2.16.2):
14841486
- DoubleConversion
14851487
- glog
@@ -1607,6 +1609,7 @@ DEPENDENCIES:
16071609
- React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
16081610
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
16091611
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
1612+
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
16101613
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
16111614
- RNReanimated (from `../node_modules/react-native-reanimated`)
16121615
- RNScreens (from `../node_modules/react-native-screens`)
@@ -1732,6 +1735,8 @@ EXTERNAL SOURCES:
17321735
:path: "../node_modules/react-native/ReactCommon/react/utils"
17331736
ReactCommon:
17341737
:path: "../node_modules/react-native/ReactCommon"
1738+
rn-fetch-blob:
1739+
:path: "../node_modules/rn-fetch-blob"
17351740
RNGestureHandler:
17361741
:path: "../node_modules/react-native-gesture-handler"
17371742
RNReanimated:
@@ -1799,6 +1804,7 @@ SPEC CHECKSUMS:
17991804
React-runtimescheduler: e2152ed146b6a35c07386fc2ac4827b27e6aad12
18001805
React-utils: 3285151c9d1e3a28a9586571fc81d521678c196d
18011806
ReactCommon: f42444e384d82ab89184aed5d6f3142748b54768
1807+
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
18021808
RNGestureHandler: 2282cfbcf86c360d29f44ace393203afd5c6cff7
18031809
RNReanimated: 7ad0f08a845cb60955ee5d461d2156d7b9707118
18041810
RNScreens: b32a9ff15bea7fcdbe5dff6477bc503f792b1208

package/example/AppExamplePaper/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"react-native-screens": "^3.31.1",
2323
"react-native-video": "^6.1.2",
2424
"react-native-worklets-core": "^1.3.3",
25+
"rn-fetch-blob": "^0.12.0",
2526
"shared": "workspace:^"
2627
},
2728
"devDependencies": {

package/example/Shared/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"react-native-safe-area-context": "^4.10.1",
1717
"react-native-screens": "^3.31.1",
1818
"react-native-video": "^6.1.2",
19-
"react-native-worklets-core": "^2.0.0-beta.4"
19+
"react-native-worklets-core": "^2.0.0-beta.4",
20+
"rn-fetch-blob": "^0.12.0"
2021
},
2122
"devDependencies": {
2223
"@babel/core": "^7.20.0",

package/example/Shared/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { AnimationTransitions } from './AnimationTransitions'
88
import { CameraPan } from './CameraPan'
99
import { AnimationTransitionsRecording } from './AnimationTransitionsRecording'
1010
import { ImageExample } from './ImageExample'
11+
import { LoadFromFile } from './LoadFromFile'
1112
// import { PhysicsCoin } from './PhysicsCoin'
1213
// import { FadeOut } from './FadeOut'
1314
// import { CastShadow } from './CastShadow'
@@ -50,6 +51,7 @@ function HomeScreen() {
5051
<NavigationItem name="📸 Camera Pan" route="CameraPan" />
5152
<NavigationItem name="📹 Offscreen recording" route="AnimationTransitionsRecording" />
5253
<NavigationItem name="🏞️ Image" route="ImageExample" />
54+
<NavigationItem name="📦 Load from file" route="LoadFromFile" />
5355
{/* <NavigationItem name="💰 Physics Coin" route="PhysicsCoin" />
5456
<NavigationItem name="😶‍🌫️ Fade Out" route="FadeOut" />
5557
<NavigationItem name="🎨 Change Materials" route="ChangeMaterials" />
@@ -88,6 +90,7 @@ function App() {
8890
<Stack.Screen name="CameraPan" component={CameraPan} />
8991
<Stack.Screen name="AnimationTransitionsRecording" component={AnimationTransitionsRecording} />
9092
<Stack.Screen name="ImageExample" component={ImageExample} />
93+
<Stack.Screen name="LoadFromFile" component={LoadFromFile} />
9194
{/* TODO: Migrate */}
9295
{/* <Stack.Screen name="PhysicsCoin" component={PhysicsCoin} />
9396
<Stack.Screen name="FadeOut" component={FadeOut} />
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as React from 'react'
2+
import { useCallback, useState } from 'react'
3+
import { Button, StyleSheet, View } from 'react-native'
4+
import { FilamentContext, FilamentView, Camera, Model } from 'react-native-filament'
5+
import { DefaultLight } from './components/DefaultLight'
6+
import RNFetchBlob from 'rn-fetch-blob'
7+
8+
function Renderer({ assetPath }: { assetPath: string }) {
9+
console.log('Renderer assetPath', assetPath)
10+
return (
11+
<FilamentView style={styles.filamentView}>
12+
<Camera />
13+
<DefaultLight />
14+
<Model source={{ uri: assetPath }} transformToUnitCube />
15+
</FilamentView>
16+
)
17+
}
18+
19+
export function LoadFromFile() {
20+
const [assetPath, setAssetPath] = useState<string>()
21+
22+
const downloadAsset = useCallback(() => {
23+
const downloadUrl = 'https://raw.githubusercontent.com/google/filament/main/third_party/models/DamagedHelmet/DamagedHelmet.glb'
24+
console.log('Downloading asset from', downloadUrl)
25+
RNFetchBlob.config({
26+
fileCache: true,
27+
appendExt: 'glb',
28+
})
29+
.fetch('GET', downloadUrl)
30+
.then((res) => {
31+
const path = res.path()
32+
console.log('Downloaded asset to', path)
33+
setAssetPath(`file://${path}`)
34+
})
35+
}, [])
36+
37+
if (assetPath == null) {
38+
return (
39+
<View style={styles.container}>
40+
<Button title="Load asset" onPress={downloadAsset} />
41+
</View>
42+
)
43+
}
44+
return (
45+
<FilamentContext>
46+
<Renderer assetPath={assetPath} />
47+
</FilamentContext>
48+
)
49+
}
50+
51+
const styles = StyleSheet.create({
52+
container: {
53+
flex: 1,
54+
},
55+
filamentView: {
56+
flex: 1,
57+
},
58+
btnContainer: {
59+
position: 'absolute',
60+
bottom: 0,
61+
maxHeight: 120,
62+
width: '100%',
63+
},
64+
})

package/ios/src/RNFAppleFilamentProxy.mm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@
4949
auto managedBuffer = std::make_shared<AppleManagedBuffer>(data);
5050
return std::make_shared<FilamentBuffer>(managedBuffer);
5151
}
52+
53+
// Check if we want to load from file path:
54+
if ([filePath hasPrefix:@"file://"]) {
55+
filePath = [filePath substringFromIndex:7];
56+
57+
// Load the data from the file
58+
NSError* errorPtr;
59+
NSData* bufferData = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&errorPtr];
60+
if (!bufferData || errorPtr != nullptr) {
61+
throw std::runtime_error("File not found or could not be read, error: " + std::string(errorPtr.localizedDescription.UTF8String));
62+
}
63+
auto managedBuffer = std::make_shared<AppleManagedBuffer>(bufferData);
64+
return std::make_shared<FilamentBuffer>(managedBuffer);
65+
}
5266

5367
// Split the path at the last dot to separate name and extension
5468
NSArray<NSString*>* pathComponents = [filePath componentsSeparatedByString:@"."];

package/src/react/Model.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type ModelProps = UseModelProps & {
2121
*
2222
*
2323
* If you are passing in a `.glb` model or similar from your app's bundle using `require(..)`, make sure to add `glb` as an asset extension to `metro.config.js`!
24-
* If you are passing in a `{ url: ... }`, make sure the URL points directly to a `.glb` model. This can either be a web URL (`http://..`/`https://..`), a local file (`file://..`), or an native asset path (`path/to/asset.glb`)
24+
* If you are passing in a `{ uri: ... }`, make sure the URL points directly to a `.glb` model. This can either be a web URL (`http://..`/`https://..`), a local file (`file://..`), or an native asset path (`path/to/asset.glb`)
2525
*/
2626
export function Model({ children, source, transformToUnitCube, ...modelProps }: PropsWithChildren<ModelProps>) {
2727
const model = useModel(source, modelProps)

package/yarn.lock

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4679,6 +4679,13 @@ __metadata:
46794679
languageName: node
46804680
linkType: hard
46814681

4682+
"base-64@npm:0.1.0":
4683+
version: 0.1.0
4684+
resolution: "base-64@npm:0.1.0"
4685+
checksum: 5a42938f82372ab5392cbacc85a5a78115cbbd9dbef9f7540fa47d78763a3a8bd7d598475f0d92341f66285afd377509851a9bb5c67bbecb89686e9255d5b3eb
4686+
languageName: node
4687+
linkType: hard
4688+
46824689
"base64-js@npm:^1.3.1, base64-js@npm:^1.5.1":
46834690
version: 1.5.1
46844691
resolution: "base64-js@npm:1.5.1"
@@ -7125,6 +7132,20 @@ __metadata:
71257132
languageName: node
71267133
linkType: hard
71277134

7135+
"glob@npm:7.0.6":
7136+
version: 7.0.6
7137+
resolution: "glob@npm:7.0.6"
7138+
dependencies:
7139+
fs.realpath: ^1.0.0
7140+
inflight: ^1.0.4
7141+
inherits: 2
7142+
minimatch: ^3.0.2
7143+
once: ^1.3.0
7144+
path-is-absolute: ^1.0.0
7145+
checksum: 6ad065f51982f9a76f7052984121c95bca376ea02060b21200ad62b400422b05f0dc331f72da89a73c21a2451cbe9bec16bb17dcf37a516dc51bbbb6efe462a1
7146+
languageName: node
7147+
linkType: hard
7148+
71287149
"glob@npm:^10.2.2, glob@npm:^10.3.10":
71297150
version: 10.4.1
71307151
resolution: "glob@npm:10.4.1"
@@ -10098,6 +10119,7 @@ __metadata:
1009810119
react-native-screens: ^3.31.1
1009910120
react-native-video: ^6.1.2
1010010121
react-native-worklets-core: ^1.3.3
10122+
rn-fetch-blob: ^0.12.0
1010110123
shared: "workspace:^"
1010210124
typescript: 5.2.2
1010310125
languageName: unknown
@@ -10658,6 +10680,7 @@ __metadata:
1065810680
react-native-screens: ^3.31.1
1065910681
react-native-video: ^6.1.2
1066010682
react-native-worklets-core: ^2.0.0-beta.4
10683+
rn-fetch-blob: ^0.12.0
1066110684
shared: "workspace:^"
1066210685
typescript: 5.2.2
1066310686
languageName: unknown
@@ -11282,6 +11305,16 @@ __metadata:
1128211305
languageName: node
1128311306
linkType: hard
1128411307

11308+
"rn-fetch-blob@npm:^0.12.0":
11309+
version: 0.12.0
11310+
resolution: "rn-fetch-blob@npm:0.12.0"
11311+
dependencies:
11312+
base-64: 0.1.0
11313+
glob: 7.0.6
11314+
checksum: 56e5832be583e97d58f955c0c4f6dcb0eb62c97dde331182c6effb726253731cb555d4a5f6c81079ed2fcb957f81633cbe95b37d326d063405f912506de20ff4
11315+
languageName: node
11316+
linkType: hard
11317+
1128511318
"run-applescript@npm:^5.0.0":
1128611319
version: 5.0.0
1128711320
resolution: "run-applescript@npm:5.0.0"
@@ -11549,6 +11582,7 @@ __metadata:
1154911582
react-native-screens: ^3.31.1
1155011583
react-native-video: ^6.1.2
1155111584
react-native-worklets-core: ^2.0.0-beta.4
11585+
rn-fetch-blob: ^0.12.0
1155211586
typescript: 5.2.2
1155311587
languageName: unknown
1155411588
linkType: soft

0 commit comments

Comments
 (0)