Skip to content

Commit

Permalink
feat: support loading assets from file:// (#205)
Browse files Browse the repository at this point in the history
  • Loading branch information
hannojg authored Jun 17, 2024
1 parent bd8a6ac commit c41d0dd
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.margelo.filament;

import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.net.Uri;
Expand All @@ -22,6 +23,8 @@
import com.facebook.react.uimanager.UIManagerHelper;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
Expand Down Expand Up @@ -114,22 +117,30 @@ private ByteBuffer streamToDirectByteBuffer(InputStream stream) throws IOExcepti
*/
@DoNotStrip
@Keep
ByteBuffer loadAsset(String url) throws Exception {
Log.i(NAME, "Loading byte data from URL: " + url + "...");
ByteBuffer loadAsset(String uriString) throws Exception {
Log.i(NAME, "Loading byte data from URL: " + uriString + "...");

// It's a file path, read the file system directly
if (uriString.contains("file://")) {
String filePath = uriString.replace("file://", "");
File file = new File(filePath);
if (!file.exists()) {
throw new Exception("File does not exist: " + filePath);
}
try (FileInputStream stream = new FileInputStream(file)) {
return streamToDirectByteBuffer(stream);
} catch (Exception e) {
Log.e(NAME, "Failed to read file: " + filePath, e);
throw e;
}
}

Uri uri = null;
String assetName = null;
if (url.contains("://")) {
// It's a URL/http resource
if (uriString.contains("http://") || uriString.contains("https://")) {
Log.i(NAME, "Parsing URL...");
uri = Uri.parse(url);
Uri uri = Uri.parse(uriString);
Log.i(NAME, "Parsed URL: " + uri.toString());
} else {
assetName = url;
Log.i(NAME, "Assumed assetName: " + assetName);
}

if (uri != null) {
// It's a URL/http resource
Request request = new Request.Builder().url(uri.toString()).build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
Expand All @@ -139,18 +150,15 @@ ByteBuffer loadAsset(String url) throws Exception {
throw new RuntimeException("Response was not successful!");
}
} catch (Exception ex) {
Log.e(NAME, "Failed to fetch URL " + url + "!", ex);
Log.e(NAME, "Failed to fetch URL " + uriString + "!", ex);
throw ex;
}
} else if (assetName != null) {
// It's bundled into the Android resources/assets
try (InputStream stream = reactContext.getAssets().open(assetName)) {
return streamToDirectByteBuffer(stream);
}
} else {
// It's a bird? it's a plane? not it's an error
throw new Exception("Input is neither a valid URL, nor an asset path - " +
"cannot load asset! (Input: " + url + ")");
}

// It's bundled into the Android resources/assets
Log.i(NAME, "Assumed assetName: " + uriString);
try (InputStream stream = reactContext.getAssets().open(uriString)) {
return streamToDirectByteBuffer(stream);
}
}

Expand Down
1 change: 1 addition & 0 deletions package/example/AppExampleFabric/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"react-native-screens": "^3.31.1",
"react-native-video": "^6.1.2",
"react-native-worklets-core": "^2.0.0-beta.4",
"rn-fetch-blob": "^0.12.0",
"shared": "workspace:^"
},
"devDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions package/example/AppExamplePaper/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,8 @@ PODS:
- React-logger (= 0.74.1)
- React-perflogger (= 0.74.1)
- React-utils (= 0.74.1)
- rn-fetch-blob (0.12.0):
- React-Core
- RNGestureHandler (2.16.2):
- DoubleConversion
- glog
Expand Down Expand Up @@ -1607,6 +1609,7 @@ DEPENDENCIES:
- React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
Expand Down Expand Up @@ -1732,6 +1735,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/react/utils"
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
rn-fetch-blob:
:path: "../node_modules/rn-fetch-blob"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNReanimated:
Expand Down Expand Up @@ -1799,6 +1804,7 @@ SPEC CHECKSUMS:
React-runtimescheduler: e2152ed146b6a35c07386fc2ac4827b27e6aad12
React-utils: 3285151c9d1e3a28a9586571fc81d521678c196d
ReactCommon: f42444e384d82ab89184aed5d6f3142748b54768
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
RNGestureHandler: 2282cfbcf86c360d29f44ace393203afd5c6cff7
RNReanimated: 7ad0f08a845cb60955ee5d461d2156d7b9707118
RNScreens: b32a9ff15bea7fcdbe5dff6477bc503f792b1208
Expand Down
1 change: 1 addition & 0 deletions package/example/AppExamplePaper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"react-native-screens": "^3.31.1",
"react-native-video": "^6.1.2",
"react-native-worklets-core": "^1.3.3",
"rn-fetch-blob": "^0.12.0",
"shared": "workspace:^"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion package/example/Shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"react-native-safe-area-context": "^4.10.1",
"react-native-screens": "^3.31.1",
"react-native-video": "^6.1.2",
"react-native-worklets-core": "^2.0.0-beta.4"
"react-native-worklets-core": "^2.0.0-beta.4",
"rn-fetch-blob": "^0.12.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
Expand Down
3 changes: 3 additions & 0 deletions package/example/Shared/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AnimationTransitions } from './AnimationTransitions'
import { CameraPan } from './CameraPan'
import { AnimationTransitionsRecording } from './AnimationTransitionsRecording'
import { ImageExample } from './ImageExample'
import { LoadFromFile } from './LoadFromFile'
// import { PhysicsCoin } from './PhysicsCoin'
// import { FadeOut } from './FadeOut'
// import { CastShadow } from './CastShadow'
Expand Down Expand Up @@ -50,6 +51,7 @@ function HomeScreen() {
<NavigationItem name="📸 Camera Pan" route="CameraPan" />
<NavigationItem name="📹 Offscreen recording" route="AnimationTransitionsRecording" />
<NavigationItem name="🏞️ Image" route="ImageExample" />
<NavigationItem name="📦 Load from file" route="LoadFromFile" />
{/* <NavigationItem name="💰 Physics Coin" route="PhysicsCoin" />
<NavigationItem name="😶‍🌫️ Fade Out" route="FadeOut" />
<NavigationItem name="🎨 Change Materials" route="ChangeMaterials" />
Expand Down Expand Up @@ -88,6 +90,7 @@ function App() {
<Stack.Screen name="CameraPan" component={CameraPan} />
<Stack.Screen name="AnimationTransitionsRecording" component={AnimationTransitionsRecording} />
<Stack.Screen name="ImageExample" component={ImageExample} />
<Stack.Screen name="LoadFromFile" component={LoadFromFile} />
{/* TODO: Migrate */}
{/* <Stack.Screen name="PhysicsCoin" component={PhysicsCoin} />
<Stack.Screen name="FadeOut" component={FadeOut} />
Expand Down
64 changes: 64 additions & 0 deletions package/example/Shared/src/LoadFromFile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from 'react'
import { useCallback, useState } from 'react'
import { Button, StyleSheet, View } from 'react-native'
import { FilamentContext, FilamentView, Camera, Model } from 'react-native-filament'
import { DefaultLight } from './components/DefaultLight'
import RNFetchBlob from 'rn-fetch-blob'

function Renderer({ assetPath }: { assetPath: string }) {
console.log('Renderer assetPath', assetPath)
return (
<FilamentView style={styles.filamentView}>
<Camera />
<DefaultLight />
<Model source={{ uri: assetPath }} transformToUnitCube />
</FilamentView>
)
}

export function LoadFromFile() {
const [assetPath, setAssetPath] = useState<string>()

const downloadAsset = useCallback(() => {
const downloadUrl = 'https://raw.githubusercontent.com/google/filament/main/third_party/models/DamagedHelmet/DamagedHelmet.glb'
console.log('Downloading asset from', downloadUrl)
RNFetchBlob.config({
fileCache: true,
appendExt: 'glb',
})
.fetch('GET', downloadUrl)
.then((res) => {
const path = res.path()
console.log('Downloaded asset to', path)
setAssetPath(`file://${path}`)
})
}, [])

if (assetPath == null) {
return (
<View style={styles.container}>
<Button title="Load asset" onPress={downloadAsset} />
</View>
)
}
return (
<FilamentContext>
<Renderer assetPath={assetPath} />
</FilamentContext>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
filamentView: {
flex: 1,
},
btnContainer: {
position: 'absolute',
bottom: 0,
maxHeight: 120,
width: '100%',
},
})
14 changes: 14 additions & 0 deletions package/ios/src/RNFAppleFilamentProxy.mm
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@
auto managedBuffer = std::make_shared<AppleManagedBuffer>(data);
return std::make_shared<FilamentBuffer>(managedBuffer);
}

// Check if we want to load from file path:
if ([filePath hasPrefix:@"file://"]) {
filePath = [filePath substringFromIndex:7];

// Load the data from the file
NSError* errorPtr;
NSData* bufferData = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&errorPtr];
if (!bufferData || errorPtr != nullptr) {
throw std::runtime_error("File not found or could not be read, error: " + std::string(errorPtr.localizedDescription.UTF8String));
}
auto managedBuffer = std::make_shared<AppleManagedBuffer>(bufferData);
return std::make_shared<FilamentBuffer>(managedBuffer);
}

// Split the path at the last dot to separate name and extension
NSArray<NSString*>* pathComponents = [filePath componentsSeparatedByString:@"."];
Expand Down
2 changes: 1 addition & 1 deletion package/src/react/Model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type ModelProps = UseModelProps & {
*
*
* 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`!
* 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`)
* 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`)
*/
export function Model({ children, source, transformToUnitCube, ...modelProps }: PropsWithChildren<ModelProps>) {
const model = useModel(source, modelProps)
Expand Down
34 changes: 34 additions & 0 deletions package/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4679,6 +4679,13 @@ __metadata:
languageName: node
linkType: hard

"base-64@npm:0.1.0":
version: 0.1.0
resolution: "base-64@npm:0.1.0"
checksum: 5a42938f82372ab5392cbacc85a5a78115cbbd9dbef9f7540fa47d78763a3a8bd7d598475f0d92341f66285afd377509851a9bb5c67bbecb89686e9255d5b3eb
languageName: node
linkType: hard

"base64-js@npm:^1.3.1, base64-js@npm:^1.5.1":
version: 1.5.1
resolution: "base64-js@npm:1.5.1"
Expand Down Expand Up @@ -7125,6 +7132,20 @@ __metadata:
languageName: node
linkType: hard

"glob@npm:7.0.6":
version: 7.0.6
resolution: "glob@npm:7.0.6"
dependencies:
fs.realpath: ^1.0.0
inflight: ^1.0.4
inherits: 2
minimatch: ^3.0.2
once: ^1.3.0
path-is-absolute: ^1.0.0
checksum: 6ad065f51982f9a76f7052984121c95bca376ea02060b21200ad62b400422b05f0dc331f72da89a73c21a2451cbe9bec16bb17dcf37a516dc51bbbb6efe462a1
languageName: node
linkType: hard

"glob@npm:^10.2.2, glob@npm:^10.3.10":
version: 10.4.1
resolution: "glob@npm:10.4.1"
Expand Down Expand Up @@ -10098,6 +10119,7 @@ __metadata:
react-native-screens: ^3.31.1
react-native-video: ^6.1.2
react-native-worklets-core: ^1.3.3
rn-fetch-blob: ^0.12.0
shared: "workspace:^"
typescript: 5.2.2
languageName: unknown
Expand Down Expand Up @@ -10658,6 +10680,7 @@ __metadata:
react-native-screens: ^3.31.1
react-native-video: ^6.1.2
react-native-worklets-core: ^2.0.0-beta.4
rn-fetch-blob: ^0.12.0
shared: "workspace:^"
typescript: 5.2.2
languageName: unknown
Expand Down Expand Up @@ -11282,6 +11305,16 @@ __metadata:
languageName: node
linkType: hard

"rn-fetch-blob@npm:^0.12.0":
version: 0.12.0
resolution: "rn-fetch-blob@npm:0.12.0"
dependencies:
base-64: 0.1.0
glob: 7.0.6
checksum: 56e5832be583e97d58f955c0c4f6dcb0eb62c97dde331182c6effb726253731cb555d4a5f6c81079ed2fcb957f81633cbe95b37d326d063405f912506de20ff4
languageName: node
linkType: hard

"run-applescript@npm:^5.0.0":
version: 5.0.0
resolution: "run-applescript@npm:5.0.0"
Expand Down Expand Up @@ -11549,6 +11582,7 @@ __metadata:
react-native-screens: ^3.31.1
react-native-video: ^6.1.2
react-native-worklets-core: ^2.0.0-beta.4
rn-fetch-blob: ^0.12.0
typescript: 5.2.2
languageName: unknown
linkType: soft
Expand Down

0 comments on commit c41d0dd

Please sign in to comment.