ilabs-flir 2.4.10 → 2.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Flir.podspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'Flir'
3
- s.version = '2.4.9'
3
+ s.version = '2.4.11'
4
4
  s.summary = 'FLIR Thermal SDK React Native - Bundled via postinstall'
5
5
  s.description = <<-DESC
6
6
  A React Native wrapper for the FLIR Thermal SDK, providing thermal imaging
@@ -64,6 +64,7 @@ object FlirManager {
64
64
 
65
65
  fun setTextureCallback(callback: TextureUpdateCallback?) {
66
66
  textureCallback = callback
67
+ sdkManager?.setFrameConsumerActive(callback != null)
67
68
  }
68
69
 
69
70
  interface TemperatureUpdateCallback {
@@ -107,6 +108,7 @@ object FlirManager {
107
108
  sdkManager = FlirSdkManager.getInstance(context)
108
109
  sdkManager?.setListener(sdkListener)
109
110
  sdkManager?.initialize()
111
+ sdkManager?.setFrameConsumerActive(textureCallback != null)
110
112
 
111
113
  isInitialized = true
112
114
  Log.i(TAG, "FlirManager initialized")
@@ -56,6 +56,10 @@ public class FlirSdkManager {
56
56
  private final List<Identity> discoveredDevices = Collections.synchronizedList(new ArrayList<>());
57
57
  private final Map<String, DiscoveredCamera> discoveredCameras = Collections.synchronizedMap(new HashMap<>());
58
58
  private volatile Bitmap latestBitmap;
59
+ private final Bitmap[] bitmapRingBuffer = new Bitmap[3];
60
+ private int ringBufferIndex = 0;
61
+ private volatile boolean isFrameConsumerActive = false;
62
+ private int[] scalePixelBuffer = null;
59
63
  private volatile String currentPaletteName = "WhiteHot";
60
64
  private final AtomicBoolean isProcessingFrame = new AtomicBoolean(false);
61
65
  private boolean useHalfScale = false;
@@ -118,6 +122,22 @@ public class FlirSdkManager {
118
122
  }
119
123
  }
120
124
  };
125
+
126
+ // Auto-detect low-end device to enable useHalfScale
127
+ try {
128
+ android.app.ActivityManager.MemoryInfo mi = new android.app.ActivityManager.MemoryInfo();
129
+ android.app.ActivityManager activityManager = (android.app.ActivityManager) this.context.getSystemService(Context.ACTIVITY_SERVICE);
130
+ if (activityManager != null) {
131
+ activityManager.getMemoryInfo(mi);
132
+ boolean isLowRam = activityManager.isLowRamDevice();
133
+ if (isLowRam || mi.totalMem < 3L * 1024 * 1024 * 1024) {
134
+ useHalfScale = true;
135
+ Log.i(TAG, "Low-end device detected (Total RAM: " + (mi.totalMem / (1024 * 1024)) + "MB). Enabling half-scale rendering by default.");
136
+ }
137
+ }
138
+ } catch (Throwable t) {
139
+ Log.e(TAG, "Failed to auto-detect memory constraints", t);
140
+ }
121
141
  }
122
142
 
123
143
  public static synchronized FlirSdkManager getInstance(Context context) {
@@ -145,6 +165,25 @@ public class FlirSdkManager {
145
165
  try {
146
166
  android.os.Looper.loop();
147
167
  } catch (Throwable t) {
168
+ if (t instanceof VirtualMachineError) {
169
+ Log.e(TAG, "🚨 [FLIR LOOPER PROTECTOR] VirtualMachineError/OOM detected! Releasing display bitmap and running GC...");
170
+ try {
171
+ if (instance != null) {
172
+ synchronized (instance) {
173
+ for (int i = 0; i < 3; i++) {
174
+ if (instance.bitmapRingBuffer[i] != null) {
175
+ instance.bitmapRingBuffer[i].recycle();
176
+ instance.bitmapRingBuffer[i] = null;
177
+ }
178
+ }
179
+ instance.latestBitmap = null;
180
+ instance.scalePixelBuffer = null;
181
+ }
182
+ }
183
+ System.gc();
184
+ } catch (Throwable ignored) {}
185
+ continue;
186
+ }
148
187
  String msg = t.getMessage();
149
188
  boolean isSuppressed = false;
150
189
 
@@ -425,6 +464,11 @@ public class FlirSdkManager {
425
464
  });
426
465
  }
427
466
 
467
+ public void setFrameConsumerActive(boolean active) {
468
+ this.isFrameConsumerActive = active;
469
+ Log.d(TAG, "isFrameConsumerActive set to: " + active);
470
+ }
471
+
428
472
  private void startStreamInternal() {
429
473
  if (camera == null) {
430
474
  notifyError("Not connected");
@@ -504,103 +548,164 @@ public class FlirSdkManager {
504
548
  // to ensure the native frame reference remains valid.
505
549
  if (isProcessingFrame.compareAndSet(false, true)) {
506
550
  try {
507
- if (streamer != null && activeStream != null) {
508
- streamer.update();
509
-
510
- final String paletteToApply = currentPaletteName;
511
- final String snapshotPath = pendingSnapshotPath;
512
- pendingSnapshotPath = null;
513
- streamer.withThermalImage(thermalImage -> {
514
- // 1. Apply Palette
515
- if (paletteToApply != null) {
516
- try {
517
- List<Palette> sdkPalettes = cachedSdkPalettes;
518
- if (sdkPalettes == null) {
519
- synchronized (FlirSdkManager.this) {
520
- sdkPalettes = cachedSdkPalettes;
521
- if (sdkPalettes == null) {
522
- try {
523
- sdkPalettes = PaletteManager.getDefaultPalettes();
524
- cachedSdkPalettes = sdkPalettes;
525
- } catch (Throwable t) {
526
- Log.e(TAG, "Failed to get default palettes", t);
551
+ synchronized (FlirSdkManager.this) {
552
+ if (streamer != null && activeStream != null) {
553
+ streamer.update();
554
+ final String paletteToApply = currentPaletteName;
555
+ final String snapshotPath = pendingSnapshotPath;
556
+ pendingSnapshotPath = null;
557
+ streamer.withThermalImage(thermalImage -> {
558
+ if (!isFrameConsumerActive && snapshotPath == null) {
559
+ return;
560
+ }
561
+ // 1. Apply Palette
562
+ if (paletteToApply != null) {
563
+ try {
564
+ List<Palette> sdkPalettes = cachedSdkPalettes;
565
+ if (sdkPalettes == null) {
566
+ synchronized (FlirSdkManager.this) {
567
+ sdkPalettes = cachedSdkPalettes;
568
+ if (sdkPalettes == null) {
569
+ try {
570
+ sdkPalettes = PaletteManager.getDefaultPalettes();
571
+ cachedSdkPalettes = sdkPalettes;
572
+ } catch (Throwable t) {
573
+ Log.e(TAG, "Failed to get default palettes", t);
574
+ }
527
575
  }
528
576
  }
529
577
  }
530
- }
531
-
532
- if (paletteToApply.equalsIgnoreCase("Gray") || paletteToApply.equalsIgnoreCase("grayscale")) {
533
- // User wants Gray - map to WhiteHot which is the SDK's standard grayscale
534
- for (Palette p : sdkPalettes) {
535
- if (p.name.equalsIgnoreCase("WhiteHot") || p.name.equalsIgnoreCase("White hot")) {
536
- thermalImage.setPalette(p);
537
- break;
538
- }
539
- }
540
- } else {
541
- Palette palette = null;
542
- for (Palette p : sdkPalettes) {
543
- if (p.name.equalsIgnoreCase(paletteToApply)) {
544
- palette = p;
545
- break;
546
- }
547
- }
548
578
 
549
- if (palette != null) {
550
- thermalImage.setPalette(palette);
551
- } else if (paletteToApply.equalsIgnoreCase("Wheel")) {
552
- // Fallback for Wheel if not found - some SDKs use different names
579
+ if (paletteToApply.equalsIgnoreCase("Gray") || paletteToApply.equalsIgnoreCase("grayscale")) {
580
+ // User wants Gray - map to WhiteHot which is the SDK's standard grayscale
553
581
  for (Palette p : sdkPalettes) {
554
- if (p.name.contains("Wheel") || p.name.contains("ColorWheel") || p.name.contains("Rainbow")) {
582
+ if (p.name.equalsIgnoreCase("WhiteHot") || p.name.equalsIgnoreCase("White hot")) {
555
583
  thermalImage.setPalette(p);
556
584
  break;
557
585
  }
558
586
  }
587
+ } else {
588
+ Palette palette = null;
589
+ for (Palette p : sdkPalettes) {
590
+ if (p.name.equalsIgnoreCase(paletteToApply)) {
591
+ palette = p;
592
+ break;
593
+ }
594
+ }
595
+
596
+ if (palette != null) {
597
+ thermalImage.setPalette(palette);
598
+ } else if (paletteToApply.equalsIgnoreCase("Wheel")) {
599
+ // Fallback for Wheel if not found - some SDKs use different names
600
+ for (Palette p : sdkPalettes) {
601
+ if (p.name.contains("Wheel") || p.name.contains("ColorWheel") || p.name.contains("Rainbow")) {
602
+ thermalImage.setPalette(p);
603
+ break;
604
+ }
605
+ }
606
+ }
559
607
  }
608
+ } catch (Throwable t) {
609
+ Log.e(TAG, "Failed to apply palette: " + paletteToApply, t);
560
610
  }
561
- } catch (Throwable t) {
562
- Log.e(TAG, "Failed to apply palette: " + paletteToApply, t);
563
611
  }
564
- }
565
612
 
566
- // 2. Save Radiometric Snapshot if requested
567
- if (snapshotPath != null) {
568
- try {
569
- Log.i(TAG, "[SNAPSHOT] Attempting to save radiometric snapshot: " + snapshotPath);
570
- thermalImage.saveAs(snapshotPath);
571
- Log.i(TAG, "[SNAPSHOT] ✅ Success: Radiometric snapshot saved");
572
- if (snapshotCallback != null) {
573
- snapshotCallback.onSnapshotSaved(snapshotPath);
574
- snapshotCallback = null;
575
- }
576
- } catch (java.io.IOException e) {
577
- Log.e(TAG, "Failed to save radiometric snapshot", e);
578
- if (snapshotCallback != null) {
579
- snapshotCallback.onSnapshotError(e.getMessage());
580
- snapshotCallback = null;
613
+ // 2. Save Radiometric Snapshot if requested
614
+ if (snapshotPath != null) {
615
+ try {
616
+ Log.i(TAG, "[SNAPSHOT] Attempting to save radiometric snapshot: " + snapshotPath);
617
+ thermalImage.saveAs(snapshotPath);
618
+ Log.i(TAG, "[SNAPSHOT] ✅ Success: Radiometric snapshot saved");
619
+ if (snapshotCallback != null) {
620
+ snapshotCallback.onSnapshotSaved(snapshotPath);
621
+ snapshotCallback = null;
622
+ }
623
+ } catch (java.io.IOException e) {
624
+ Log.e(TAG, "Failed to save radiometric snapshot", e);
625
+ if (snapshotCallback != null) {
626
+ snapshotCallback.onSnapshotError(e.getMessage());
627
+ snapshotCallback = null;
628
+ }
581
629
  }
582
630
  }
583
- }
584
631
 
585
- // 3. Generate Bitmap for display
586
- // We use streamer.getImage() to get the rendered image with palette applied.
587
- try {
588
- Bitmap newBitmap = BitmapAndroid.createBitmap(streamer.getImage()).getBitMap();
589
- if (newBitmap != null) {
590
- Bitmap oldBitmap = latestBitmap;
591
- latestBitmap = newBitmap;
592
- if (listener != null) {
593
- listener.onFrame(newBitmap);
594
- }
595
- // Recycle old bitmap to prevent memory leak
596
- if (oldBitmap != null && oldBitmap != newBitmap) {
597
- oldBitmap.recycle();
632
+ // 3. Generate Bitmap for display
633
+ if (isFrameConsumerActive) {
634
+ try {
635
+ ImageBuffer imageBuffer = streamer.getImage();
636
+ if (imageBuffer != null) {
637
+ int width = imageBuffer.getWidth();
638
+ int height = imageBuffer.getHeight();
639
+ if (width > 0 && height > 0) {
640
+ synchronized (FlirSdkManager.this) {
641
+ int dstW = useHalfScale ? (width / 2) : width;
642
+ int dstH = useHalfScale ? (height / 2) : height;
643
+
644
+ for (int i = 0; i < 3; i++) {
645
+ if (bitmapRingBuffer[i] == null ||
646
+ bitmapRingBuffer[i].getWidth() != dstW ||
647
+ bitmapRingBuffer[i].getHeight() != dstH) {
648
+
649
+ if (bitmapRingBuffer[i] != null) {
650
+ bitmapRingBuffer[i].recycle();
651
+ }
652
+ bitmapRingBuffer[i] = Bitmap.createBitmap(dstW, dstH, Bitmap.Config.ARGB_8888);
653
+ }
654
+ }
655
+ ringBufferIndex = (ringBufferIndex + 1) % 3;
656
+ Bitmap targetBitmap = bitmapRingBuffer[ringBufferIndex];
657
+
658
+ if (useHalfScale) {
659
+ imageBuffer.with(new com.flir.thermalsdk.utils.Consumer<java.nio.ByteBuffer>() {
660
+ @Override
661
+ public void accept(java.nio.ByteBuffer byteBuffer) {
662
+ if (byteBuffer != null) {
663
+ byteBuffer.rewind();
664
+ java.nio.IntBuffer srcPixels = byteBuffer.asIntBuffer();
665
+
666
+ int totalPixels = dstW * dstH;
667
+ if (scalePixelBuffer == null || scalePixelBuffer.length != totalPixels) {
668
+ scalePixelBuffer = new int[totalPixels];
669
+ }
670
+
671
+ for (int y = 0; y < dstH; y++) {
672
+ int srcY = y * 2;
673
+ int srcRowOffset = srcY * width;
674
+ int dstRowOffset = y * dstW;
675
+ for (int x = 0; x < dstW; x++) {
676
+ int srcX = x * 2;
677
+ scalePixelBuffer[dstRowOffset + x] = srcPixels.get(srcRowOffset + srcX);
678
+ }
679
+ }
680
+ targetBitmap.setPixels(scalePixelBuffer, 0, dstW, 0, 0, dstW, dstH);
681
+ }
682
+ }
683
+ });
684
+ } else {
685
+ imageBuffer.with(new com.flir.thermalsdk.utils.Consumer<java.nio.ByteBuffer>() {
686
+ @Override
687
+ public void accept(java.nio.ByteBuffer byteBuffer) {
688
+ if (byteBuffer != null) {
689
+ byteBuffer.rewind();
690
+ targetBitmap.copyPixelsFromBuffer(byteBuffer);
691
+ }
692
+ }
693
+ });
694
+ }
695
+
696
+ latestBitmap = targetBitmap;
697
+ if (listener != null) {
698
+ listener.onFrame(targetBitmap);
699
+ }
700
+ }
701
+ }
702
+ }
703
+ } catch (Exception e) {
704
+ Log.e(TAG, "Bitmap buffer copy failed", e);
598
705
  }
599
706
  }
600
- } catch (Exception e) {
601
- Log.e(TAG, "Bitmap creation failed", e);
602
- }
603
- });
707
+ });
708
+ }
604
709
  }
605
710
  } catch (Exception e) {
606
711
  Log.e(TAG, "Frame processing error", e);
@@ -629,16 +734,25 @@ public class FlirSdkManager {
629
734
  }
630
735
 
631
736
  private void stopStreamInternal() {
632
- if (activeStream != null) {
633
- try {
634
- activeStream.stop();
635
- } catch (Exception e) {
636
- Log.e(TAG, "Stop stream error", e);
737
+ synchronized (this) {
738
+ if (activeStream != null) {
739
+ try {
740
+ activeStream.stop();
741
+ } catch (Exception e) {
742
+ Log.e(TAG, "Stop stream error", e);
743
+ }
744
+ activeStream = null;
745
+ }
746
+ streamer = null;
747
+ for (int i = 0; i < 3; i++) {
748
+ if (bitmapRingBuffer[i] != null) {
749
+ bitmapRingBuffer[i].recycle();
750
+ bitmapRingBuffer[i] = null;
751
+ }
637
752
  }
638
- activeStream = null;
753
+ latestBitmap = null;
754
+ scalePixelBuffer = null;
639
755
  }
640
- streamer = null;
641
- latestBitmap = null;
642
756
  Log.d(TAG, "Streaming stopped");
643
757
  }
644
758
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilabs-flir",
3
- "version": "2.4.10",
3
+ "version": "2.4.12",
4
4
  "description": "FLIR Thermal SDK for React Native - iOS & Android (bundled at compile time via postinstall)",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -67,4 +67,4 @@
67
67
  "sourceDir": "./android/Flir"
68
68
  }
69
69
  }
70
- }
70
+ }