From d202a1ff6f9c3f071cf67d8971885654d7a5f904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Thu, 31 Dec 2015 14:07:02 +0100 Subject: [PATCH] Implement options for captureFrame - format: "base64" || "file" - type: "png" || "jpeg"/"jpg" || "webm" (android only) - quality: from 0 to 1 (iOS support only with type=jpg/jpeg) - filePath: for format=="file" you must provide a file path (you can use react-native-fs to get directory constants) --- Examples/Simple/android/app/app.iml | 1 + Examples/Simple/android/app/build.gradle | 2 + .../main/java/com/simple/MainActivity.java | 2 + Examples/Simple/android/settings.gradle | 5 +- .../ios/Simple.xcodeproj/project.pbxproj | 30 ++++++ Examples/Simple/package.json | 3 +- Examples/Simple/src/index.js | 45 +++++++-- .../projectseptember/RNGL/CaptureConfig.java | 63 ++++++++++++ .../com/projectseptember/RNGL/GLCanvas.java | 97 +++++++++++++++---- .../RNGL/GLCanvasManager.java | 2 +- ios/CaptureConfig.h | 19 ++++ ios/CaptureConfig.m | 38 ++++++++ ios/GLCanvas.h | 3 +- ios/GLCanvas.m | 75 +++++++++++--- ios/GLCanvasManager.m | 5 +- ios/RCTConvert+CaptureConfig.h | 8 ++ ios/RCTConvert+CaptureConfig.m | 22 +++++ ios/RNGL.xcodeproj/project.pbxproj | 12 +++ src/GLCanvas.captureFrame.android.js | 2 +- src/GLCanvas.captureFrame.ios.js | 2 +- src/GLCanvas.js | 92 +++++++++++++++--- 21 files changed, 467 insertions(+), 61 deletions(-) create mode 100644 android/src/main/java/com/projectseptember/RNGL/CaptureConfig.java create mode 100644 ios/CaptureConfig.h create mode 100644 ios/CaptureConfig.m create mode 100644 ios/RCTConvert+CaptureConfig.h create mode 100644 ios/RCTConvert+CaptureConfig.m diff --git a/Examples/Simple/android/app/app.iml b/Examples/Simple/android/app/app.iml index c6bbb95..d10c644 100644 --- a/Examples/Simple/android/app/app.iml +++ b/Examples/Simple/android/app/app.iml @@ -112,6 +112,7 @@ + diff --git a/Examples/Simple/android/app/build.gradle b/Examples/Simple/android/app/build.gradle index c24a17d..18fa585 100644 --- a/Examples/Simple/android/app/build.gradle +++ b/Examples/Simple/android/app/build.gradle @@ -78,4 +78,6 @@ dependencies { compile project(":RNMaterialKit") compile project(":RNGL") + compile project(':react-native-fs') + } diff --git a/Examples/Simple/android/app/src/main/java/com/simple/MainActivity.java b/Examples/Simple/android/app/src/main/java/com/simple/MainActivity.java index 1822c6c..a50883e 100644 --- a/Examples/Simple/android/app/src/main/java/com/simple/MainActivity.java +++ b/Examples/Simple/android/app/src/main/java/com/simple/MainActivity.java @@ -11,6 +11,7 @@ import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.shell.MainReactPackage; import com.github.xinthink.rnmk.ReactMaterialKitPackage; import com.projectseptember.RNGL.RNGLPackage; +import com.rnfs.RNFSPackage; public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler { @@ -28,6 +29,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand .setJSMainModuleName("index.android") .addPackage(new MainReactPackage()) .addPackage(new RNGLPackage()) + .addPackage(new RNFSPackage()) .addPackage(new ReactMaterialKitPackage()) .setUseDeveloperSupport(BuildConfig.DEBUG) .setInitialLifecycleState(LifecycleState.RESUMED) diff --git a/Examples/Simple/android/settings.gradle b/Examples/Simple/android/settings.gradle index eac886a..06488e7 100644 --- a/Examples/Simple/android/settings.gradle +++ b/Examples/Simple/android/settings.gradle @@ -6,4 +6,7 @@ include ':RNMaterialKit' project(':RNMaterialKit').projectDir = file('../node_modules/react-native-material-kit/android') include ':RNGL' -project(':RNGL').projectDir = file('../../../android') \ No newline at end of file +project(':RNGL').projectDir = file('../../../android') + +include ':react-native-fs' +project(':react-native-fs').projectDir = new File(settingsDir, '../node_modules/react-native-fs/android') \ No newline at end of file diff --git a/Examples/Simple/ios/Simple.xcodeproj/project.pbxproj b/Examples/Simple/ios/Simple.xcodeproj/project.pbxproj index d0fc18e..33690a4 100644 --- a/Examples/Simple/ios/Simple.xcodeproj/project.pbxproj +++ b/Examples/Simple/ios/Simple.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; 34607B601C132B22009203B1 /* libRCTMaterialKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 34607B581C132B1C009203B1 /* libRCTMaterialKit.a */; }; 3461EB4B1C132AEC0003E4A2 /* libRNGL.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3461EB4A1C132AD30003E4A2 /* libRNGL.a */; }; + 34C9905A1C3530D6002F49FC /* libRNFS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C990581C3530CE002F49FC /* libRNFS.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; /* End PBXBuildFile section */ @@ -104,6 +105,13 @@ remoteGlobalIDString = 4107012F1ACB723B00C6AA39; remoteInfo = RNGL; }; + 34C990571C3530CE002F49FC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 34C990481C3530CE002F49FC /* RNFS.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = F12AFB9B1ADAF8F800E0535D; + remoteInfo = RNFS; + }; 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; @@ -142,6 +150,7 @@ 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; 34607B4F1C132B1C009203B1 /* RCTMaterialKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTMaterialKit.xcodeproj; path = "../node_modules/react-native-material-kit/iOS/RCTMaterialKit.xcodeproj"; sourceTree = ""; }; 3461EB3B1C132AD30003E4A2 /* RNGL.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNGL.xcodeproj; path = "../node_modules/gl-react-native/ios/RNGL.xcodeproj"; sourceTree = ""; }; + 34C990481C3530CE002F49FC /* RNFS.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNFS.xcodeproj; path = "../node_modules/react-native-fs/RNFS.xcodeproj"; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -158,6 +167,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 34C9905A1C3530D6002F49FC /* libRNFS.a in Frameworks */, 34607B601C132B22009203B1 /* libRCTMaterialKit.a in Frameworks */, 3461EB4B1C132AEC0003E4A2 /* libRNGL.a in Frameworks */, 146834051AC3E58100842450 /* libReact.a in Frameworks */, @@ -287,6 +297,14 @@ name = Products; sourceTree = ""; }; + 34C990491C3530CE002F49FC /* Products */ = { + isa = PBXGroup; + children = ( + 34C990581C3530CE002F49FC /* libRNFS.a */, + ); + name = Products; + sourceTree = ""; + }; 78C398B11ACF4ADC00677621 /* Products */ = { isa = PBXGroup; children = ( @@ -298,6 +316,7 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + 34C990481C3530CE002F49FC /* RNFS.xcodeproj */, 34607B4F1C132B1C009203B1 /* RCTMaterialKit.xcodeproj */, 3461EB3B1C132AD30003E4A2 /* RNGL.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */, @@ -453,6 +472,10 @@ ProductGroup = 146834001AC3E56700842450 /* Products */; ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; }, + { + ProductGroup = 34C990491C3530CE002F49FC /* Products */; + ProjectRef = 34C990481C3530CE002F49FC /* RNFS.xcodeproj */; + }, { ProductGroup = 3461EB3C1C132AD30003E4A2 /* Products */; ProjectRef = 3461EB3B1C132AD30003E4A2 /* RNGL.xcodeproj */; @@ -537,6 +560,13 @@ remoteRef = 3461EB491C132AD30003E4A2 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 34C990581C3530CE002F49FC /* libRNFS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNFS.a; + remoteRef = 34C990571C3530CE002F49FC /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/Simple/package.json b/Examples/Simple/package.json index 91a0abe..5503597 100644 --- a/Examples/Simple/package.json +++ b/Examples/Simple/package.json @@ -6,9 +6,10 @@ "start": "react-native start" }, "dependencies": { - "gl-react-native": "file:../..", "gl-react": "^2.0.8", + "gl-react-native": "file:../..", "react-native": "^0.17.0", + "react-native-fs": "^1.1.0", "react-native-material-kit": "^0.2.4" } } diff --git a/Examples/Simple/src/index.js b/Examples/Simple/src/index.js index d355246..324d743 100644 --- a/Examples/Simple/src/index.js +++ b/Examples/Simple/src/index.js @@ -22,6 +22,8 @@ const { MKButton, } = require("react-native-material-kit"); +const RNFS = require("react-native-fs"); + const HelloGL = require("./HelloGL"); const Saturation = require("./Saturation"); const HueRotate = require("./HueRotate"); @@ -82,7 +84,8 @@ class Simple extends Component { switch1: false, switch2: false, switch3: false, - captured: null + captured: null, + captureConfig: null }; this.onCapture1 = this.onCapture1.bind(this); @@ -97,9 +100,17 @@ class Simple extends Component { } onCapture1 () { - this.refs.helloGL.captureFrame().then(data64 => { - this.setState({ captured: data64 }); - }); + const captureConfig = { + quality: Math.round((Math.random() * 100))/100, + type: Math.random() < 0.5 ? "jpg": "png", + format: Math.random() < 0.5 ? "base64" : "file" + }; + if (captureConfig.format === "file") { + captureConfig.filePath = RNFS.DocumentDirectoryPath+"/hellogl_capture.png"; + } + this.refs.helloGL + .captureFrame(captureConfig) + .then(captured => this.setState({ captured, captureConfig })); } render () { @@ -113,7 +124,8 @@ class Simple extends Component { switch1, switch2, switch3, - captured + captured, + captureConfig } = this.state; return @@ -128,9 +140,28 @@ class Simple extends Component { - {captured && } + {captured && + } - {captured && {captured.slice(0, 100)}} + {captureConfig && + + + format={captureConfig.format} + + + type={captureConfig.type} + + + quality={captureConfig.quality+""} + + + } + {captured && + + {captured.slice(0, 100)} + } diff --git a/android/src/main/java/com/projectseptember/RNGL/CaptureConfig.java b/android/src/main/java/com/projectseptember/RNGL/CaptureConfig.java new file mode 100644 index 0000000..4900e7b --- /dev/null +++ b/android/src/main/java/com/projectseptember/RNGL/CaptureConfig.java @@ -0,0 +1,63 @@ +package com.projectseptember.RNGL; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; + + +public class CaptureConfig { + String format; + String type; + String filePath; + Double quality; + + public CaptureConfig(String format, String type, String filePath, Double quality) { + this.format = format; + this.type = type; + this.filePath = filePath; + this.quality = quality; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CaptureConfig that = (CaptureConfig) o; + + if (format != null ? !format.equals(that.format) : that.format != null) return false; + if (type != null ? !type.equals(that.type) : that.type != null) return false; + if (filePath != null ? !filePath.equals(that.filePath) : that.filePath != null) + return false; + return !(quality != null ? !quality.equals(that.quality) : that.quality != null); + + } + + @Override + public int hashCode() { + int result = format != null ? format.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + result = 31 * result + (filePath != null ? filePath.hashCode() : 0); + result = 31 * result + (quality != null ? quality.hashCode() : 0); + return result; + } + + public static CaptureConfig fromMap (ReadableMap map) { + return new CaptureConfig( + map.getString("format"), + map.getString("type"), + map.getString("filePath"), + map.getDouble("quality") + ); + } + + public WritableMap toMap () { + WritableMap map = Arguments.createMap(); + map.putString("format", format); + map.putString("type", type); + map.putString("filePath", filePath); + map.putDouble("quality", quality); + return map; + } + +} diff --git a/android/src/main/java/com/projectseptember/RNGL/GLCanvas.java b/android/src/main/java/com/projectseptember/RNGL/GLCanvas.java index eb3ca34..6bfa8fc 100644 --- a/android/src/main/java/com/projectseptember/RNGL/GLCanvas.java +++ b/android/src/main/java/com/projectseptember/RNGL/GLCanvas.java @@ -27,6 +27,7 @@ import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.events.RCTEventEmitter; import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; @@ -69,7 +70,8 @@ public class GLCanvas extends GLSurfaceView private Map fbos; private ExecutorSupplier executorSupplier; private final Queue mRunOnDraw = new LinkedList<>(); - private boolean captureFrameRequested = false; + + private List captureConfigs = new ArrayList<>(); private float pixelRatio; private float displayDensity; @@ -169,18 +171,76 @@ public class GLCanvas extends GLSurfaceView if (shouldRenderNow) { this.render(); deferredRendering = false; - if (captureFrameRequested) { - captureFrameRequested = false; - Bitmap capture = createSnapshot(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - capture.compress(Bitmap.CompressFormat.PNG, 100, baos); - String frame = "data:image/png;base64,"+ - Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT); - dispatchOnCaptureFrame(frame); + if (captureConfigs.size() > 0) { + capture(); // FIXME: maybe we should schedule this? } } } + private void capture () { + Bitmap capture = createSnapshot(); + ReactContext reactContext = (ReactContext)getContext(); + RCTEventEmitter eventEmitter = reactContext.getJSModule(RCTEventEmitter.class); + + for (CaptureConfig config : captureConfigs) { + String result = null, error = null; + boolean isPng = config.type.equals("png"); + boolean isJpeg = !isPng && (config.type.equals("jpg")||config.type.equals("jpeg")); + boolean isWebm = !isPng && !isJpeg && config.type.equals("webm"); + boolean isBase64 = config.format.equals("base64"); + boolean isFile = !isBase64 && config.format.equals("file"); + + Bitmap.CompressFormat compressFormat = + isPng ? Bitmap.CompressFormat.PNG : + isJpeg ? Bitmap.CompressFormat.JPEG : + isWebm ? Bitmap.CompressFormat.WEBP : + null; + + int quality = (int)(100 * config.quality); + + if (compressFormat == null) { + error = "Unsupported capture type '"+config.type+"'"; + } + else if (isBase64) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + capture.compress(compressFormat, quality, baos); + String frame = "data:image/png;base64,"+ + Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT); + baos.close(); + result = frame; + } + catch (Exception e) { + e.printStackTrace(); + error = "Could not capture as base64: "+e.getMessage(); + } + } + else if (isFile) { + try { + FileOutputStream fileOutputStream = new FileOutputStream(config.filePath); + capture.compress(compressFormat, quality, fileOutputStream); + fileOutputStream.close(); + result = "file://"+config.filePath; + } + catch (Exception e) { + e.printStackTrace(); + error = "Could not write file: "+e.getMessage(); + } + } + else { + error = "Unsupported capture format '"+config.format+"'"; + } + + WritableMap response = Arguments.createMap(); + response.putMap("config", config.toMap()); + if (error != null) response.putString("error", error); + if (result != null) response.putString("result", result); + eventEmitter.receiveEvent(getId(), "captureFrame", response); + } + + captureConfigs = new ArrayList<>(); + } + private boolean haveRemainingToPreload() { for (Uri uri: imagesToPreload) { if (!preloaded.contains(uri)) { @@ -677,16 +737,6 @@ public class GLCanvas extends GLSurfaceView } } - private void dispatchOnCaptureFrame (String frame) { - WritableMap event = Arguments.createMap(); - event.putString("frame", frame); - ReactContext reactContext = (ReactContext)getContext(); - reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( - getId(), - "captureFrame", - event); - } - private void dispatchOnProgress (double progress, int loaded, int total) { WritableMap event = Arguments.createMap(); event.putDouble("progress", Double.isNaN(progress) ? 0.0 : progress); @@ -708,9 +758,14 @@ public class GLCanvas extends GLSurfaceView event); } - public void requestCaptureFrame() { - captureFrameRequested = true; + public void requestCaptureFrame (CaptureConfig config) { this.requestRender(); + for (CaptureConfig existing : captureConfigs) { + if (existing.equals(config)) { + return; + } + } + captureConfigs.add(config); } private Bitmap createSnapshot () { diff --git a/android/src/main/java/com/projectseptember/RNGL/GLCanvasManager.java b/android/src/main/java/com/projectseptember/RNGL/GLCanvasManager.java index c0468b7..ded4042 100644 --- a/android/src/main/java/com/projectseptember/RNGL/GLCanvasManager.java +++ b/android/src/main/java/com/projectseptember/RNGL/GLCanvasManager.java @@ -94,7 +94,7 @@ public class GLCanvasManager extends SimpleViewManager { Assertions.assertNotNull(args); switch (commandType) { case COMMAND_CAPTURE_FRAME: { - canvas.requestCaptureFrame(); + canvas.requestCaptureFrame(CaptureConfig.fromMap(args.getMap(0))); return; } default: diff --git a/ios/CaptureConfig.h b/ios/CaptureConfig.h new file mode 100644 index 0000000..d9fa9bd --- /dev/null +++ b/ios/CaptureConfig.h @@ -0,0 +1,19 @@ +#import + +@interface CaptureConfig: NSObject + +@property (nonatomic, copy) NSString *format; +@property (nonatomic, copy) NSString *type; +@property (nonatomic, copy) NSString *filePath; +@property (nonatomic, copy) NSNumber *quality; + +-(instancetype)initWithFormat: (NSString *)format + withType: (NSString *)type + withQuality: (NSNumber *)quality + withFilePath: (NSString *)filePath; + +- (bool) isEqualToCaptureConfig: (CaptureConfig *)other; + +- (NSDictionary *) dictionary; + +@end diff --git a/ios/CaptureConfig.m b/ios/CaptureConfig.m new file mode 100644 index 0000000..f7ef07b --- /dev/null +++ b/ios/CaptureConfig.m @@ -0,0 +1,38 @@ +#import "CaptureConfig.h" + +@implementation CaptureConfig + +-(instancetype)initWithFormat: (NSString *)format + withType: (NSString *)type + withQuality: (NSNumber *)quality + withFilePath: (NSString *)filePath +{ + if ((self = [super init])) { + self.format = format; + self.type = type; + self.quality = quality; + self.filePath = filePath; + } + return self; +} + + +- (bool) isEqualToCaptureConfig: (CaptureConfig *)other +{ + return [self.format isEqualToString:other.format] && + [self.type isEqualToString:other.type] && + [self.quality isEqualToNumber:other.quality] && + [self.filePath isEqualToString:other.filePath]; +} + +- (NSDictionary *) dictionary +{ + return @{ + @"format": self.format, + @"type": self.type, + @"quality": self.quality, + @"filePath": self.filePath + }; +} + +@end diff --git a/ios/GLCanvas.h b/ios/GLCanvas.h index 44a9ba7..9084b4b 100644 --- a/ios/GLCanvas.h +++ b/ios/GLCanvas.h @@ -1,5 +1,6 @@ #import #import "GLData.h" +#import "CaptureConfig.h" #import "RCTComponent.h" @interface GLCanvas: GLKView @@ -19,6 +20,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge; -- (void) requestCaptureFrame; +- (void) requestCaptureFrame:(CaptureConfig *)config; @end diff --git a/ios/GLCanvas.m b/ios/GLCanvas.m index d9708a6..4703b40 100644 --- a/ios/GLCanvas.m +++ b/ios/GLCanvas.m @@ -44,8 +44,6 @@ NSArray* diff (NSArray* a, NSArray* b) { GLRenderData *_renderData; - BOOL _captureFrameRequested; - NSArray *_contentData; NSArray *_contentTextures; NSDictionary *_images; // This caches the currently used images (imageSrc -> GLReactImage) @@ -63,6 +61,9 @@ NSArray* diff (NSArray* a, NSArray* b) { NSTimer *animationTimer; BOOL _needSync; + + NSMutableArray *_captureConfigs; + BOOL _captureScheduled; } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -71,7 +72,8 @@ NSArray* diff (NSArray* a, NSArray* b) { _bridge = bridge; _images = @{}; _preloaded = [[NSMutableArray alloc] init]; - _captureFrameRequested = false; + _captureConfigs = [[NSMutableArray alloc] init]; + _captureScheduled = false; _dirtyOnLoad = true; _neverRendered = true; self.context = [bridge.rnglContext getContext]; @@ -83,10 +85,15 @@ RCT_NOT_IMPLEMENTED(-init) //// Props Setters -- (void) requestCaptureFrame +- (void) requestCaptureFrame: (CaptureConfig *)config { - _captureFrameRequested = true; [self setNeedsDisplay]; + for (CaptureConfig *existing in _captureConfigs) { + if ([existing isEqualToCaptureConfig:config]) { + return; + } + } + [_captureConfigs addObject:config]; } -(void)setImagesToPreload:(NSArray *)imagesToPreload @@ -383,21 +390,63 @@ RCT_NOT_IMPLEMENTED(-init) if (willRender) { [self render]; - if (_captureFrameRequested) { - _captureFrameRequested = false; + if (!_captureScheduled && [_captureConfigs count] > 0) { + _captureScheduled = true; [self performSelectorOnMainThread:@selector(capture) withObject:nil waitUntilDone:NO]; } } } --(void)capture +-(void) capture { + _captureScheduled = false; + if (!self.onGLCaptureFrame) return; + UIImage *frameImage = [self snapshot]; - NSData *frameData = UIImagePNGRepresentation(frameImage); - NSString *frame = - [NSString stringWithFormat:@"data:image/png;base64,%@", - [frameData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]]; - if (self.onGLCaptureFrame) self.onGLCaptureFrame(@{ @"frame": frame }); + + for (CaptureConfig *config in _captureConfigs) { + id result; + id error; + + BOOL isPng = [config.type isEqualToString:@"png"]; + BOOL isJpeg = !isPng && ([config.type isEqualToString:@"jpeg"] || [config.type isEqualToString:@"jpg"]); + + BOOL isBase64 = [config.format isEqualToString:@"base64"]; + BOOL isFile = !isBase64 && [config.format isEqualToString:@"file"]; + + NSData *frameData = + isPng ? UIImagePNGRepresentation(frameImage) : + isJpeg ? UIImageJPEGRepresentation(frameImage, [config.quality floatValue]) : + nil; + + if (!frameData) { + error = [NSString stringWithFormat:@"Unsupported capture type '%@'", config.type]; + } + else if (isBase64) { + NSString *base64 = [frameData base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength]; + result = [NSString stringWithFormat:@"data:image/%@;base64,%@", config.type, base64]; + } + else if (isFile) { + NSError *e; + if (![frameData writeToFile:config.filePath options:0 error:&e]) { + error = [NSString stringWithFormat:@"Could not write file: %@", e.localizedDescription]; + } + else { + result = [NSString stringWithFormat:@"file://%@", config.filePath]; + } + } + else { + error = [NSString stringWithFormat:@"Unsupported capture format '%@'", config.format]; + } + + NSMutableDictionary *response = [[NSMutableDictionary alloc] init]; + response[@"config"] = [config dictionary]; + if (error) response[@"error"] = error; + if (result) response[@"result"] = result; + self.onGLCaptureFrame(response); + } + + _captureConfigs = [[NSMutableArray alloc] init]; } - (void)render diff --git a/ios/GLCanvasManager.m b/ios/GLCanvasManager.m index 65970b0..efc7dbc 100644 --- a/ios/GLCanvasManager.m +++ b/ios/GLCanvasManager.m @@ -1,6 +1,7 @@ #import "GLCanvasManager.h" #import "GLCanvas.h" #import "RCTConvert+GLData.h" +#import "RCTConvert+CaptureConfig.h" #import "RCTUIManager.h" #import "RCTLog.h" #import @@ -28,7 +29,7 @@ RCT_EXPORT_VIEW_PROPERTY(onGLLoad, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onGLProgress, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onGLCaptureFrame, RCTBubblingEventBlock); -RCT_EXPORT_METHOD(capture: (nonnull NSNumber *)reactTag) +RCT_EXPORT_METHOD(capture: (nonnull NSNumber *)reactTag withConfig:(id)config) { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { UIView *view = viewRegistry[reactTag]; @@ -37,7 +38,7 @@ RCT_EXPORT_METHOD(capture: (nonnull NSNumber *)reactTag) } else { GLCanvas *glCanvas = (GLCanvas *)view; - [glCanvas requestCaptureFrame]; + [glCanvas requestCaptureFrame:[RCTConvert CaptureConfig:config]]; } }]; } diff --git a/ios/RCTConvert+CaptureConfig.h b/ios/RCTConvert+CaptureConfig.h new file mode 100644 index 0000000..d276714 --- /dev/null +++ b/ios/RCTConvert+CaptureConfig.h @@ -0,0 +1,8 @@ +#import "RCTConvert.h" +#import "CaptureConfig.h" + +@interface RCTConvert (CaptureConfig) + ++ (CaptureConfig *)CaptureConfig:(id)json; + +@end diff --git a/ios/RCTConvert+CaptureConfig.m b/ios/RCTConvert+CaptureConfig.m new file mode 100644 index 0000000..f72159d --- /dev/null +++ b/ios/RCTConvert+CaptureConfig.m @@ -0,0 +1,22 @@ +// +// RCTConvert+CaptureConfig.m +// RNGL +// +// Created by Gaetan Renaudeau on 30/12/15. +// +// + +#import "RCTConvert+CaptureConfig.h" + +@implementation RCTConvert (CaptureConfig) + ++ (CaptureConfig *)CaptureConfig:(id)json +{ + return [[CaptureConfig alloc] + initWithFormat:[RCTConvert NSString:json[@"format"]] + withType:[RCTConvert NSString:json[@"type"]] + withQuality:[RCTConvert NSNumber:json[@"quality"]] + withFilePath:[RCTConvert NSString:json[@"filePath"]]]; +} + +@end diff --git a/ios/RNGL.xcodeproj/project.pbxproj b/ios/RNGL.xcodeproj/project.pbxproj index 359f9ce..e9470b6 100644 --- a/ios/RNGL.xcodeproj/project.pbxproj +++ b/ios/RNGL.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 346089D81BEFD0A500C90DB5 /* GLTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 346089CB1BEFD0A500C90DB5 /* GLTexture.m */; }; 346089D91BEFD0A500C90DB5 /* RCTConvert+GLData.m in Sources */ = {isa = PBXBuildFile; fileRef = 346089CD1BEFD0A500C90DB5 /* RCTConvert+GLData.m */; }; 346089DA1BEFD0A500C90DB5 /* RNGLContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 346089CF1BEFD0A500C90DB5 /* RNGLContext.m */; }; + 34C990421C34939C002F49FC /* RCTConvert+CaptureConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C990411C34939C002F49FC /* RCTConvert+CaptureConfig.m */; }; + 34C990471C349422002F49FC /* CaptureConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C990461C349422002F49FC /* CaptureConfig.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -55,6 +57,10 @@ 346089CD1BEFD0A500C90DB5 /* RCTConvert+GLData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+GLData.m"; sourceTree = ""; }; 346089CE1BEFD0A500C90DB5 /* RNGLContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNGLContext.h; sourceTree = ""; }; 346089CF1BEFD0A500C90DB5 /* RNGLContext.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RNGLContext.m; sourceTree = ""; tabWidth = 2; }; + 34C990401C34939C002F49FC /* RCTConvert+CaptureConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+CaptureConfig.h"; sourceTree = ""; }; + 34C990411C34939C002F49FC /* RCTConvert+CaptureConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+CaptureConfig.m"; sourceTree = ""; tabWidth = 2; }; + 34C990451C34941C002F49FC /* CaptureConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CaptureConfig.h; sourceTree = ""; }; + 34C990461C349422002F49FC /* CaptureConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = CaptureConfig.m; sourceTree = ""; tabWidth = 2; }; 4107012F1ACB723B00C6AA39 /* libRNGL.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNGL.a; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -92,6 +98,10 @@ 346089CB1BEFD0A500C90DB5 /* GLTexture.m */, 346089CC1BEFD0A500C90DB5 /* RCTConvert+GLData.h */, 346089CD1BEFD0A500C90DB5 /* RCTConvert+GLData.m */, + 34C990461C349422002F49FC /* CaptureConfig.m */, + 34C990451C34941C002F49FC /* CaptureConfig.h */, + 34C990401C34939C002F49FC /* RCTConvert+CaptureConfig.h */, + 34C990411C34939C002F49FC /* RCTConvert+CaptureConfig.m */, 346089CE1BEFD0A500C90DB5 /* RNGLContext.h */, 346089CF1BEFD0A500C90DB5 /* RNGLContext.m */, 410701301ACB723B00C6AA39 /* Products */, @@ -161,10 +171,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 34C990421C34939C002F49FC /* RCTConvert+CaptureConfig.m in Sources */, 346089D31BEFD0A500C90DB5 /* GLFBO.m in Sources */, 346089DA1BEFD0A500C90DB5 /* RNGLContext.m in Sources */, 346089D21BEFD0A500C90DB5 /* GLData.m in Sources */, 346089D91BEFD0A500C90DB5 /* RCTConvert+GLData.m in Sources */, + 34C990471C349422002F49FC /* CaptureConfig.m in Sources */, 346089D51BEFD0A500C90DB5 /* GLImageData.m in Sources */, 346089D41BEFD0A500C90DB5 /* GLImage.m in Sources */, 346089D61BEFD0A500C90DB5 /* GLRenderData.m in Sources */, diff --git a/src/GLCanvas.captureFrame.android.js b/src/GLCanvas.captureFrame.android.js index ceb938b..f16ee53 100644 --- a/src/GLCanvas.captureFrame.android.js +++ b/src/GLCanvas.captureFrame.android.js @@ -12,4 +12,4 @@ See README install instructions. React.NativeModules.UIManager.GLCanvas is %s`, GLCanvas); const {Commands} = GLCanvas; -module.exports = handle => UIManager.dispatchViewManagerCommand(handle, Commands.captureFrame, []); +module.exports = (handle, config) => UIManager.dispatchViewManagerCommand(handle, Commands.captureFrame, [ config ]); diff --git a/src/GLCanvas.captureFrame.ios.js b/src/GLCanvas.captureFrame.ios.js index 24a716e..80c2a53 100644 --- a/src/GLCanvas.captureFrame.ios.js +++ b/src/GLCanvas.captureFrame.ios.js @@ -10,4 +10,4 @@ See README install instructions. React.NativeModules.GLCanvasManager is %s`, GLCanvasManager); -module.exports = handle => GLCanvasManager.capture(handle); +module.exports = (handle, config) => GLCanvasManager.capture(handle, config); diff --git a/src/GLCanvas.js b/src/GLCanvas.js index 79d8dc7..85cfc49 100644 --- a/src/GLCanvas.js +++ b/src/GLCanvas.js @@ -1,3 +1,4 @@ +const invariant = require("invariant"); const React = require("react-native"); const { Component, @@ -6,6 +7,9 @@ const { const captureFrame = require("./GLCanvas.captureFrame"); +const serializeOption = config => +config.format + ":" + config.type + ":" + config.quality; + const GLCanvasNative = requireNativeComponent("GLCanvas", GLCanvas, { nativeOnly: { onGLChange: true, @@ -25,21 +29,85 @@ function defer() { } class GLCanvas extends Component { - captureFrame (cb) { - const promise = ( - this._pendingCaptureFrame || // use pending capture OR create a new captureFrame pending - (captureFrame(React.findNodeHandle(this.refs.native)), this._pendingCaptureFrame = defer()) - ).promise; - if (typeof cb === "function") { - console.warn("GLSurface: callback parameter of captureFrame is deprecated, use the returned promise instead"); // eslint-disable-line no-console - promise.then(cb); + + componentWillMount () { + this._pendingCaptureFrame = {}; + } + + componentWillUnmount () { + Object.keys(this._pendingCaptureFrame).forEach(key => + this._pendingCaptureFrame[key].reject(new Error("GLCanvas is unmounting"))); + this._pendingCaptureFrame = null; + } + + addPendingCaptureFrame (config) { + const key = serializeOption(config); + return this._pendingCaptureFrame[key] || ( + (captureFrame(React.findNodeHandle(this.refs.native), config), + this._pendingCaptureFrame[key] = defer()) + ); + } + + captureFrame (configArg) { + let config; + if (configArg) { + invariant(typeof configArg==="object", "captureFrame takes an object option in parameter"); + let nb = 0; + if ("format" in configArg) { + invariant( + typeof configArg.format === "string", + "captureFrame({format}): format must be a string (e.g: 'base64'), Got: '%s'", + configArg.format); + if (configArg.format === "file") invariant( + typeof configArg.filePath === "string" && configArg.filePath, + "captureFrame({filePath}): filePath must be defined when using 'file' format and be an non-empty string, Got: '%s'", + configArg.filePath); + nb ++; + } + if ("type" in configArg) { + invariant( + typeof configArg.type === "string", + "captureFrame({type}): type must be a string (e.g: 'png', 'jpg'), Got: '%s'", + configArg.type); + nb ++; + } + if ("quality" in configArg) { + invariant( + typeof configArg.quality === "number" && + configArg.quality >= 0 && + configArg.quality <= 1, + "captureFrame({quality}): quality must be a number between 0 and 1, Got: '%s'", + configArg.quality); + nb ++; + } + if ("filePath" in configArg) { + nb ++; + } + const keys = Object.keys(configArg); + invariant(keys.length === nb, "captureFrame(config): config must be an object with {format, type, quality, filePath}, found some invalid keys in '%s'", keys); + config = configArg; } - return promise; + return this.addPendingCaptureFrame({ + format: "base64", + type: "png", + quality: 1, + filePath: "", + ...config + }).promise; } - onGLCaptureFrame = ({ nativeEvent: {frame} }) => { - this._pendingCaptureFrame.resolve(frame); - this._pendingCaptureFrame = undefined; + + onGLCaptureFrame = ({ nativeEvent: { error, result, config } }) => { + const key = serializeOption(config); + invariant(key in this._pendingCaptureFrame, "capture '%s' is not scheduled in this._pendingCaptureFrame", key); + if (error) { + this._pendingCaptureFrame[key].reject(error); + } + else { + this._pendingCaptureFrame[key].resolve(result); + } + delete this._pendingCaptureFrame[key]; } + render () { const { width, height, onLoad, onProgress, eventsThrough, ...restProps } = this.props; return