feat: Create C++/OpenGL-based Video Pipeline for more efficient Recording and Frame Processing (#1721)
* Create `VideoPipeline` c++ * Remove folly C++ dependency * Create `VideoPipeline` HybridClass * Set up OpenGL * Add outputs * Update VideoPipeline.kt * Bum `minSdkVersion` to `26` * Create `VideoPipelineOutput` * Create output funcs * Set output pipelines * Add FP/Recording on Output change * Update VideoPipeline.cpp * Create `PassThroughShader` * Try to draw? I have honestly no idea * fix: Fix `setFrameProcessor` nameclash * fix: Fix `high-res-sizes` being null * Add preview output * Create `OpenGLContext.cpp` * Make screen red * This _should_ work (MESSY) * FINALLY RENDER TEXTURE * Rotate * Mirror * Clean up a bit * Add `getWidth()`/`getHeight()` * Cleanup * fix: Use uniforms instead of attributes * Draw with passed rotation/mirror mode * feat: Use SurfaceTexture's transformMatrix in OpenGL pipeline (#1727) * feat: Use Transform Matrix from SurfaceTexture * Renam * feat: Fix OpenGL Shader * Update VideoPipeline.kt * Measure elapsed time * fix: Fix low resolution * Render to offscreen * Render to every context * Release `SurfaceTexture` on close * Use one OpenGL context to render to multiple EGLSurfaces * Clean up a bit * fix: Fix recording pipeline not triggering * fix: Synchronize close to prevent nulls * Update OpenGLRenderer.cpp * fix: Hardcode Android recorder size
This commit is contained in:
		@@ -7,10 +7,6 @@ set(BUILD_DIR ${CMAKE_SOURCE_DIR}/build)
 | 
				
			|||||||
set(CMAKE_VERBOSE_MAKEFILE ON)
 | 
					set(CMAKE_VERBOSE_MAKEFILE ON)
 | 
				
			||||||
set(CMAKE_CXX_STANDARD 17)
 | 
					set(CMAKE_CXX_STANDARD 17)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Folly
 | 
					 | 
				
			||||||
include("${NODE_MODULES_DIR}/react-native/ReactAndroid/cmake-utils/folly-flags.cmake")
 | 
					 | 
				
			||||||
add_compile_options(${folly_FLAGS})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Third party libraries (Prefabs)
 | 
					# Third party libraries (Prefabs)
 | 
				
			||||||
find_package(ReactAndroid REQUIRED CONFIG)
 | 
					find_package(ReactAndroid REQUIRED CONFIG)
 | 
				
			||||||
find_package(fbjni REQUIRED CONFIG)
 | 
					find_package(fbjni REQUIRED CONFIG)
 | 
				
			||||||
@@ -25,6 +21,10 @@ add_library(
 | 
				
			|||||||
        SHARED
 | 
					        SHARED
 | 
				
			||||||
        ../cpp/JSITypedArray.cpp
 | 
					        ../cpp/JSITypedArray.cpp
 | 
				
			||||||
        src/main/cpp/VisionCamera.cpp
 | 
					        src/main/cpp/VisionCamera.cpp
 | 
				
			||||||
 | 
					        src/main/cpp/VideoPipeline.cpp
 | 
				
			||||||
 | 
					        src/main/cpp/PassThroughShader.cpp
 | 
				
			||||||
 | 
					        src/main/cpp/OpenGLContext.cpp
 | 
				
			||||||
 | 
					        src/main/cpp/OpenGLRenderer.cpp
 | 
				
			||||||
        # Frame Processor
 | 
					        # Frame Processor
 | 
				
			||||||
        src/main/cpp/frameprocessor/FrameHostObject.cpp
 | 
					        src/main/cpp/frameprocessor/FrameHostObject.cpp
 | 
				
			||||||
        src/main/cpp/frameprocessor/FrameProcessorPluginHostObject.cpp
 | 
					        src/main/cpp/frameprocessor/FrameProcessorPluginHostObject.cpp
 | 
				
			||||||
@@ -60,7 +60,6 @@ target_link_libraries(
 | 
				
			|||||||
        android                             # <-- Android JNI core
 | 
					        android                             # <-- Android JNI core
 | 
				
			||||||
        ReactAndroid::jsi                   # <-- RN: JSI
 | 
					        ReactAndroid::jsi                   # <-- RN: JSI
 | 
				
			||||||
        ReactAndroid::reactnativejni        # <-- RN: React Native JNI bindings
 | 
					        ReactAndroid::reactnativejni        # <-- RN: React Native JNI bindings
 | 
				
			||||||
        ReactAndroid::folly_runtime         # <-- RN: For casting JSI <> Java objects
 | 
					 | 
				
			||||||
        fbjni::fbjni                        # <-- fbjni
 | 
					        fbjni::fbjni                        # <-- fbjni
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -93,7 +93,7 @@ android {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defaultConfig {
 | 
					  defaultConfig {
 | 
				
			||||||
    minSdkVersion safeExtGet('minSdkVersion', 21)
 | 
					    minSdkVersion safeExtGet('minSdkVersion', 26)
 | 
				
			||||||
    compileSdkVersion safeExtGet('compileSdkVersion', 33)
 | 
					    compileSdkVersion safeExtGet('compileSdkVersion', 33)
 | 
				
			||||||
    targetSdkVersion safeExtGet('targetSdkVersion', 33)
 | 
					    targetSdkVersion safeExtGet('targetSdkVersion', 33)
 | 
				
			||||||
    versionCode 1
 | 
					    versionCode 1
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,10 +14,6 @@ org.gradle.configureondemand=true
 | 
				
			|||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
 | 
					# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
 | 
				
			||||||
# org.gradle.parallel=true
 | 
					# org.gradle.parallel=true
 | 
				
			||||||
#Fri Feb 19 20:46:14 CET 2021
 | 
					#Fri Feb 19 20:46:14 CET 2021
 | 
				
			||||||
VisionCamera_buildToolsVersion=30.0.0
 | 
					 | 
				
			||||||
VisionCamera_compileSdkVersion=31
 | 
					 | 
				
			||||||
VisionCamera_kotlinVersion=1.7.20
 | 
					VisionCamera_kotlinVersion=1.7.20
 | 
				
			||||||
VisionCamera_targetSdkVersion=31
 | 
					 | 
				
			||||||
VisionCamera_ndkVersion=21.4.7075529
 | 
					 | 
				
			||||||
android.enableJetifier=true
 | 
					android.enableJetifier=true
 | 
				
			||||||
android.useAndroidX=true
 | 
					android.useAndroidX=true
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										133
									
								
								android/src/main/cpp/OpenGLContext.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								android/src/main/cpp/OpenGLContext.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// Created by Marc Rousavy on 29.08.23.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "OpenGLContext.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <EGL/egl.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2ext.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <android/native_window.h>
 | 
				
			||||||
 | 
					#include <android/log.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "OpenGLError.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace vision {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::shared_ptr<OpenGLContext> OpenGLContext::CreateWithOffscreenSurface(int width, int height) {
 | 
				
			||||||
 | 
					  return std::unique_ptr<OpenGLContext>(new OpenGLContext(width, height));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OpenGLContext::OpenGLContext(int width, int height) {
 | 
				
			||||||
 | 
					  _width = width;
 | 
				
			||||||
 | 
					  _height = height;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OpenGLContext::~OpenGLContext() {
 | 
				
			||||||
 | 
					  destroy();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OpenGLContext::destroy() {
 | 
				
			||||||
 | 
					  if (display != EGL_NO_DISPLAY) {
 | 
				
			||||||
 | 
					    eglMakeCurrent(display, offscreenSurface, offscreenSurface, context);
 | 
				
			||||||
 | 
					    if (offscreenSurface != EGL_NO_SURFACE) {
 | 
				
			||||||
 | 
					      __android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGL Surface...");
 | 
				
			||||||
 | 
					      eglDestroySurface(display, offscreenSurface);
 | 
				
			||||||
 | 
					      offscreenSurface = EGL_NO_SURFACE;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (context != EGL_NO_CONTEXT) {
 | 
				
			||||||
 | 
					      __android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGL Context...");
 | 
				
			||||||
 | 
					      eglDestroyContext(display, context);
 | 
				
			||||||
 | 
					      context = EGL_NO_CONTEXT;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    __android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGL Display...");
 | 
				
			||||||
 | 
					    eglTerminate(display);
 | 
				
			||||||
 | 
					    display = EGL_NO_DISPLAY;
 | 
				
			||||||
 | 
					    config = nullptr;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OpenGLContext::ensureOpenGL() {
 | 
				
			||||||
 | 
					  bool successful;
 | 
				
			||||||
 | 
					  // EGLDisplay
 | 
				
			||||||
 | 
					  if (display == EGL_NO_DISPLAY) {
 | 
				
			||||||
 | 
					    __android_log_print(ANDROID_LOG_INFO, TAG, "Initializing EGLDisplay..");
 | 
				
			||||||
 | 
					    display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
 | 
				
			||||||
 | 
					    if (display == EGL_NO_DISPLAY) throw OpenGLError("Failed to get default OpenGL Display!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    EGLint major;
 | 
				
			||||||
 | 
					    EGLint minor;
 | 
				
			||||||
 | 
					    successful = eglInitialize(display, &major, &minor);
 | 
				
			||||||
 | 
					    if (!successful) throw OpenGLError("Failed to initialize OpenGL!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // EGLConfig
 | 
				
			||||||
 | 
					  if (config == nullptr) {
 | 
				
			||||||
 | 
					    __android_log_print(ANDROID_LOG_INFO, TAG, "Initializing EGLConfig..");
 | 
				
			||||||
 | 
					    EGLint attributes[] = {EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
 | 
				
			||||||
 | 
					                           EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
 | 
				
			||||||
 | 
					                           EGL_ALPHA_SIZE, 8,
 | 
				
			||||||
 | 
					                           EGL_BLUE_SIZE, 8,
 | 
				
			||||||
 | 
					                           EGL_GREEN_SIZE, 8,
 | 
				
			||||||
 | 
					                           EGL_RED_SIZE, 8,
 | 
				
			||||||
 | 
					                           EGL_DEPTH_SIZE, 0,
 | 
				
			||||||
 | 
					                           EGL_STENCIL_SIZE, 0,
 | 
				
			||||||
 | 
					                           EGL_NONE};
 | 
				
			||||||
 | 
					    EGLint numConfigs;
 | 
				
			||||||
 | 
					    successful = eglChooseConfig(display, attributes, &config, 1, &numConfigs);
 | 
				
			||||||
 | 
					    if (!successful || numConfigs == 0) throw OpenGLError("Failed to choose OpenGL config!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // EGLContext
 | 
				
			||||||
 | 
					  if (context == EGL_NO_CONTEXT) {
 | 
				
			||||||
 | 
					    __android_log_print(ANDROID_LOG_INFO, TAG, "Initializing EGLContext..");
 | 
				
			||||||
 | 
					    EGLint contextAttributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
 | 
				
			||||||
 | 
					    context = eglCreateContext(display, config, nullptr, contextAttributes);
 | 
				
			||||||
 | 
					    if (context == EGL_NO_CONTEXT) throw OpenGLError("Failed to create OpenGL context!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // EGLSurface
 | 
				
			||||||
 | 
					  if (offscreenSurface == EGL_NO_SURFACE) {
 | 
				
			||||||
 | 
					    // If we don't have a surface at all
 | 
				
			||||||
 | 
					    __android_log_print(ANDROID_LOG_INFO, TAG, "Initializing %i x %i offscreen pbuffer EGLSurface..", _width, _height);
 | 
				
			||||||
 | 
					    EGLint attributes[] = {EGL_WIDTH, _width,
 | 
				
			||||||
 | 
					                           EGL_HEIGHT, _height,
 | 
				
			||||||
 | 
					                           EGL_NONE};
 | 
				
			||||||
 | 
					    offscreenSurface = eglCreatePbufferSurface(display, config, attributes);
 | 
				
			||||||
 | 
					    if (offscreenSurface == EGL_NO_SURFACE) throw OpenGLError("Failed to create OpenGL Surface!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OpenGLContext::use() {
 | 
				
			||||||
 | 
					  this->use(offscreenSurface);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OpenGLContext::use(EGLSurface surface) {
 | 
				
			||||||
 | 
					  if (surface == EGL_NO_SURFACE) throw OpenGLError("Cannot render to a null Surface!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 1. Make sure the OpenGL context is initialized
 | 
				
			||||||
 | 
					  this->ensureOpenGL();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 2. Make the OpenGL context current
 | 
				
			||||||
 | 
					  bool successful = eglMakeCurrent(display, surface, surface, context);
 | 
				
			||||||
 | 
					  if (!successful || eglGetError() != EGL_SUCCESS) throw OpenGLError("Failed to use current OpenGL context!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 3. Caller can now render to this surface
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GLuint OpenGLContext::createTexture() {
 | 
				
			||||||
 | 
					  // 1. Make sure the OpenGL context is initialized
 | 
				
			||||||
 | 
					  this->ensureOpenGL();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 2. Make the OpenGL context current
 | 
				
			||||||
 | 
					  bool successful = eglMakeCurrent(display, offscreenSurface, offscreenSurface, context);
 | 
				
			||||||
 | 
					  if (!successful || eglGetError() != EGL_SUCCESS) throw OpenGLError("Failed to use current OpenGL context!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  GLuint textureId;
 | 
				
			||||||
 | 
					  glGenTextures(1, &textureId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return textureId;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} // namespace vision
 | 
				
			||||||
							
								
								
									
										68
									
								
								android/src/main/cpp/OpenGLContext.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								android/src/main/cpp/OpenGLContext.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// Created by Marc Rousavy on 29.08.23.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <EGL/egl.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <memory>
 | 
				
			||||||
 | 
					#include <functional>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "PassThroughShader.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace vision {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * An OpenGL Context that can be used to render to different surfaces.
 | 
				
			||||||
 | 
					 * By default, it creates an off-screen PixelBuffer surface.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class OpenGLContext {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Create a new instance of the OpenGLContext that draws to an off-screen PixelBuffer surface.
 | 
				
			||||||
 | 
					   * This will not perform any OpenGL operations yet, and is therefore safe to call from any Thread.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static std::shared_ptr<OpenGLContext> CreateWithOffscreenSurface(int width, int height);
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Destroy the OpenGL Context. This needs to be called on the same thread that `use()` was called.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  ~OpenGLContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Use this OpenGL Context to render to the given EGLSurface.
 | 
				
			||||||
 | 
					   * After the `renderFunc` returns, the default offscreen PixelBuffer surface becomes active again.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  void use(EGLSurface surface);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Use this OpenGL Context to render to the offscreen PixelBuffer surface.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  void use();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Create a new texture on this context
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  GLuint createTexture();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  EGLDisplay display = EGL_NO_DISPLAY;
 | 
				
			||||||
 | 
					  EGLContext context = EGL_NO_CONTEXT;
 | 
				
			||||||
 | 
					  EGLSurface offscreenSurface = EGL_NO_SURFACE;
 | 
				
			||||||
 | 
					  EGLConfig config = nullptr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  int _width = 0, _height = 0;
 | 
				
			||||||
 | 
					  explicit OpenGLContext(int width, int height);
 | 
				
			||||||
 | 
					  void destroy();
 | 
				
			||||||
 | 
					  void ensureOpenGL();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  PassThroughShader _passThroughShader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  static constexpr auto TAG = "OpenGLContext";
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} // namespace vision
 | 
				
			||||||
							
								
								
									
										74
									
								
								android/src/main/cpp/OpenGLRenderer.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								android/src/main/cpp/OpenGLRenderer.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// Created by Marc Rousavy on 29.08.23.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "OpenGLRenderer.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <EGL/egl.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2ext.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <android/native_window.h>
 | 
				
			||||||
 | 
					#include <android/log.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <utility>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "OpenGLError.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace vision {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::unique_ptr<OpenGLRenderer> OpenGLRenderer::CreateWithWindowSurface(std::shared_ptr<OpenGLContext> context, ANativeWindow* surface) {
 | 
				
			||||||
 | 
					  return std::unique_ptr<OpenGLRenderer>(new OpenGLRenderer(std::move(context), surface));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OpenGLRenderer::OpenGLRenderer(std::shared_ptr<OpenGLContext> context, ANativeWindow* surface) {
 | 
				
			||||||
 | 
					  _context = std::move(context);
 | 
				
			||||||
 | 
					  _outputSurface = surface;
 | 
				
			||||||
 | 
					  _width = ANativeWindow_getWidth(surface);
 | 
				
			||||||
 | 
					  _height = ANativeWindow_getHeight(surface);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OpenGLRenderer::~OpenGLRenderer() {
 | 
				
			||||||
 | 
					  if (_outputSurface != nullptr) {
 | 
				
			||||||
 | 
					    ANativeWindow_release(_outputSurface);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  destroy();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OpenGLRenderer::destroy() {
 | 
				
			||||||
 | 
					  if (_context != nullptr && _surface != EGL_NO_DISPLAY) {
 | 
				
			||||||
 | 
					    __android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGL Surface...");
 | 
				
			||||||
 | 
					    eglDestroySurface(_context->display, _surface);
 | 
				
			||||||
 | 
					    _surface = EGL_NO_SURFACE;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OpenGLRenderer::renderTextureToSurface(GLuint textureId, float* transformMatrix) {
 | 
				
			||||||
 | 
					  if (_surface == EGL_NO_SURFACE) {
 | 
				
			||||||
 | 
					    __android_log_print(ANDROID_LOG_INFO, TAG, "Creating Window Surface...");
 | 
				
			||||||
 | 
					    _context->use();
 | 
				
			||||||
 | 
					    _surface = eglCreateWindowSurface(_context->display, _context->config, _outputSurface, nullptr);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 1. Activate the OpenGL context for this surface
 | 
				
			||||||
 | 
					  _context->use(_surface);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 2. Set the viewport for rendering
 | 
				
			||||||
 | 
					  glViewport(0, 0, _width, _height);
 | 
				
			||||||
 | 
					  glDisable(GL_BLEND);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 3. Bind the input texture
 | 
				
			||||||
 | 
					  glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureId);
 | 
				
			||||||
 | 
					  glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 | 
				
			||||||
 | 
					  glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 | 
				
			||||||
 | 
					  glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 | 
				
			||||||
 | 
					  glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 4. Draw it using the pass-through shader which also applies transforms
 | 
				
			||||||
 | 
					  _passThroughShader.draw(textureId, transformMatrix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 5. Swap buffers to pass it to the window surface
 | 
				
			||||||
 | 
					  eglSwapBuffers(_context->display, _surface);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} // namespace vision
 | 
				
			||||||
							
								
								
									
										58
									
								
								android/src/main/cpp/OpenGLRenderer.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								android/src/main/cpp/OpenGLRenderer.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// Created by Marc Rousavy on 29.08.23.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <EGL/egl.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2.h>
 | 
				
			||||||
 | 
					#include <android/native_window.h>
 | 
				
			||||||
 | 
					#include <memory>
 | 
				
			||||||
 | 
					#include "PassThroughShader.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "OpenGLContext.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace vision {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OpenGLRenderer {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Create a new instance of the OpenGLRenderer that draws to an on-screen window surface.
 | 
				
			||||||
 | 
					   * This will not perform any OpenGL operations yet, and is therefore safe to call from any Thread.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * Note: The `surface` is considered moved, and the OpenGL context will release it when it is being deleted.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static std::unique_ptr<OpenGLRenderer> CreateWithWindowSurface(std::shared_ptr<OpenGLContext> context, ANativeWindow* surface);
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Destroy the OpenGL Context. This needs to be called on the same thread that `use()` was called.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  ~OpenGLRenderer();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Renders the given Texture ID to the Surface
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  void renderTextureToSurface(GLuint textureId, float* transformMatrix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Destroys the OpenGL context. This needs to be called on the same thread that `use()` was called.
 | 
				
			||||||
 | 
					   * After calling `destroy()`, it is legal to call `use()` again, which will re-construct everything.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  void destroy();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  explicit OpenGLRenderer(std::shared_ptr<OpenGLContext> context, ANativeWindow* surface);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  int _width = 0, _height = 0;
 | 
				
			||||||
 | 
					  std::shared_ptr<OpenGLContext> _context;
 | 
				
			||||||
 | 
					  ANativeWindow* _outputSurface;
 | 
				
			||||||
 | 
					  EGLSurface _surface = EGL_NO_SURFACE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  PassThroughShader _passThroughShader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  static constexpr auto TAG = "OpenGLRenderer";
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} // namespace vision
 | 
				
			||||||
							
								
								
									
										119
									
								
								android/src/main/cpp/PassThroughShader.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								android/src/main/cpp/PassThroughShader.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// Created by Marc Rousavy on 28.08.23.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "PassThroughShader.h"
 | 
				
			||||||
 | 
					#include <EGL/egl.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2ext.h>
 | 
				
			||||||
 | 
					#include <memory>
 | 
				
			||||||
 | 
					#include "OpenGLError.h"
 | 
				
			||||||
 | 
					#include <string>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace vision {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PassThroughShader::~PassThroughShader() {
 | 
				
			||||||
 | 
					  if (_programId != NO_SHADER) {
 | 
				
			||||||
 | 
					    glDeleteProgram(_programId);
 | 
				
			||||||
 | 
					    _programId = NO_SHADER;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (_vertexBuffer != NO_BUFFER) {
 | 
				
			||||||
 | 
					    glDeleteBuffers(1, &_vertexBuffer);
 | 
				
			||||||
 | 
					    _vertexBuffer = NO_BUFFER;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void PassThroughShader::draw(GLuint textureId, float* transformMatrix) {
 | 
				
			||||||
 | 
					  // 1. Set up Shader Program
 | 
				
			||||||
 | 
					  if (_programId == NO_SHADER) {
 | 
				
			||||||
 | 
					    _programId = createProgram();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  glUseProgram(_programId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (_vertexParameters.aPosition == NO_POSITION) {
 | 
				
			||||||
 | 
					    _vertexParameters = {
 | 
				
			||||||
 | 
					        .aPosition = glGetAttribLocation(_programId, "aPosition"),
 | 
				
			||||||
 | 
					        .aTexCoord = glGetAttribLocation(_programId, "aTexCoord"),
 | 
				
			||||||
 | 
					        .uTransformMatrix = glGetUniformLocation(_programId, "uTransformMatrix"),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    _fragmentParameters = {
 | 
				
			||||||
 | 
					        .uTexture = glGetUniformLocation(_programId, "uTexture"),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 2. Set up Vertices Buffer
 | 
				
			||||||
 | 
					  if (_vertexBuffer == NO_BUFFER) {
 | 
				
			||||||
 | 
					    glGenBuffers(1, &_vertexBuffer);
 | 
				
			||||||
 | 
					    glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
 | 
				
			||||||
 | 
					    glBufferData(GL_ARRAY_BUFFER, sizeof(VERTICES), VERTICES, GL_STATIC_DRAW);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 3. Pass all uniforms/attributes for vertex shader
 | 
				
			||||||
 | 
					  glEnableVertexAttribArray(_vertexParameters.aPosition);
 | 
				
			||||||
 | 
					  glVertexAttribPointer(_vertexParameters.aPosition,
 | 
				
			||||||
 | 
					                        2,
 | 
				
			||||||
 | 
					                        GL_FLOAT,
 | 
				
			||||||
 | 
					                        GL_FALSE,
 | 
				
			||||||
 | 
					                        sizeof(Vertex),
 | 
				
			||||||
 | 
					                        reinterpret_cast<void*>(offsetof(Vertex, position)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  glEnableVertexAttribArray(_vertexParameters.aTexCoord);
 | 
				
			||||||
 | 
					  glVertexAttribPointer(_vertexParameters.aTexCoord,
 | 
				
			||||||
 | 
					                        2,
 | 
				
			||||||
 | 
					                        GL_FLOAT,
 | 
				
			||||||
 | 
					                        GL_FALSE,
 | 
				
			||||||
 | 
					                        sizeof(Vertex),
 | 
				
			||||||
 | 
					                        reinterpret_cast<void*>(offsetof(Vertex, texCoord)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  glUniformMatrix4fv(_vertexParameters.uTransformMatrix, 1, GL_FALSE, transformMatrix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 4. Pass texture to fragment shader
 | 
				
			||||||
 | 
					  glActiveTexture(GL_TEXTURE0);
 | 
				
			||||||
 | 
					  glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureId);
 | 
				
			||||||
 | 
					  glUniform1i(_fragmentParameters.uTexture, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 5. Draw!
 | 
				
			||||||
 | 
					  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GLuint PassThroughShader::loadShader(GLenum shaderType, const char* shaderCode) {
 | 
				
			||||||
 | 
					  GLuint shader = glCreateShader(shaderType);
 | 
				
			||||||
 | 
					  if (shader == 0) throw OpenGLError("Failed to load shader!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  glShaderSource(shader, 1, &shaderCode, nullptr);
 | 
				
			||||||
 | 
					  glCompileShader(shader);
 | 
				
			||||||
 | 
					  GLint compileStatus = GL_FALSE;
 | 
				
			||||||
 | 
					  glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
 | 
				
			||||||
 | 
					  if (compileStatus == GL_FALSE) {
 | 
				
			||||||
 | 
					    glDeleteShader(shader);
 | 
				
			||||||
 | 
					    throw OpenGLError("Failed to compile shader!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return shader;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GLuint PassThroughShader::createProgram() {
 | 
				
			||||||
 | 
					  GLuint vertexShader = loadShader(GL_VERTEX_SHADER, VERTEX_SHADER);
 | 
				
			||||||
 | 
					  GLuint fragmentShader = loadShader(GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  GLuint program = glCreateProgram();
 | 
				
			||||||
 | 
					  if (program == 0) throw OpenGLError("Failed to create pass-through program!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  glAttachShader(program, vertexShader);
 | 
				
			||||||
 | 
					  if (glGetError() != GL_NO_ERROR) throw OpenGLError("Failed to attach Vertex Shader!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  glAttachShader(program, fragmentShader);
 | 
				
			||||||
 | 
					  if (glGetError() != GL_NO_ERROR) throw OpenGLError("Failed to attach Fragment Shader!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  glLinkProgram(program);
 | 
				
			||||||
 | 
					  GLint linkStatus = GL_FALSE;
 | 
				
			||||||
 | 
					  glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
 | 
				
			||||||
 | 
					  if (!linkStatus) {
 | 
				
			||||||
 | 
					    glDeleteProgram(program);
 | 
				
			||||||
 | 
					    throw OpenGLError("Failed to load pass-through program!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return program;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} // namespace vision
 | 
				
			||||||
							
								
								
									
										81
									
								
								android/src/main/cpp/PassThroughShader.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								android/src/main/cpp/PassThroughShader.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// Created by Marc Rousavy on 28.08.23.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <EGL/egl.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace vision {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define NO_SHADER 0
 | 
				
			||||||
 | 
					#define NO_POSITION 0
 | 
				
			||||||
 | 
					#define NO_BUFFER 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Vertex {
 | 
				
			||||||
 | 
					  GLfloat position[2];
 | 
				
			||||||
 | 
					  GLfloat texCoord[2];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PassThroughShader {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  PassThroughShader() = default;
 | 
				
			||||||
 | 
					  ~PassThroughShader();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Draw the texture using this shader.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  void draw(GLuint textureId, float* transformMatrix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  // Loading
 | 
				
			||||||
 | 
					  static GLuint loadShader(GLenum shaderType, const char* shaderCode);
 | 
				
			||||||
 | 
					  static GLuint createProgram();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  // Parameters
 | 
				
			||||||
 | 
					  GLuint _programId = NO_SHADER;
 | 
				
			||||||
 | 
					  GLuint _vertexBuffer = NO_BUFFER;
 | 
				
			||||||
 | 
					  struct VertexParameters {
 | 
				
			||||||
 | 
					    GLint aPosition = NO_POSITION;
 | 
				
			||||||
 | 
					    GLint aTexCoord = NO_POSITION;
 | 
				
			||||||
 | 
					    GLint uTransformMatrix = NO_POSITION;
 | 
				
			||||||
 | 
					  } _vertexParameters;
 | 
				
			||||||
 | 
					  struct FragmentParameters {
 | 
				
			||||||
 | 
					    GLint uTexture = NO_POSITION;
 | 
				
			||||||
 | 
					  } _fragmentParameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  // Statics
 | 
				
			||||||
 | 
					  static constexpr Vertex VERTICES[] = {
 | 
				
			||||||
 | 
					      {{-1.0f, -1.0f}, {0.0f, 0.0f}}, // bottom-left
 | 
				
			||||||
 | 
					      {{1.0f, -1.0f}, {1.0f, 0.0f}},  // bottom-right
 | 
				
			||||||
 | 
					      {{-1.0f, 1.0f}, {0.0f, 1.0f}},  // top-left
 | 
				
			||||||
 | 
					      {{1.0f, 1.0f}, {1.0f, 1.0f}}    // top-right
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static constexpr char VERTEX_SHADER[] = R"(
 | 
				
			||||||
 | 
					    attribute vec4 aPosition;
 | 
				
			||||||
 | 
					    attribute vec2 aTexCoord;
 | 
				
			||||||
 | 
					    uniform mat4 uTransformMatrix;
 | 
				
			||||||
 | 
					    varying vec2 vTexCoord;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void main() {
 | 
				
			||||||
 | 
					        gl_Position = aPosition;
 | 
				
			||||||
 | 
					        vTexCoord = (uTransformMatrix * vec4(aTexCoord, 0.0, 1.0)).xy;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )";
 | 
				
			||||||
 | 
					  static constexpr char FRAGMENT_SHADER[] = R"(
 | 
				
			||||||
 | 
					    #extension GL_OES_EGL_image_external : require
 | 
				
			||||||
 | 
					    precision mediump float;
 | 
				
			||||||
 | 
					    varying vec2 vTexCoord;
 | 
				
			||||||
 | 
					    uniform samplerExternalOES uTexture;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void main() {
 | 
				
			||||||
 | 
					        gl_FragColor = texture2D(uTexture, vTexCoord);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )";
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} // namespace vision
 | 
				
			||||||
							
								
								
									
										133
									
								
								android/src/main/cpp/VideoPipeline.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								android/src/main/cpp/VideoPipeline.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// Created by Marc Rousavy on 25.08.23.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "VideoPipeline.h"
 | 
				
			||||||
 | 
					#include "OpenGLError.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <android/native_window_jni.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2.h>
 | 
				
			||||||
 | 
					#include <GLES2/gl2ext.h>
 | 
				
			||||||
 | 
					#include <EGL/egl.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace vision {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jni::local_ref<VideoPipeline::jhybriddata> VideoPipeline::initHybrid(jni::alias_ref<jhybridobject> jThis, int width, int height) {
 | 
				
			||||||
 | 
					  return makeCxxInstance(jThis, width, height);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					VideoPipeline::VideoPipeline(jni::alias_ref<jhybridobject> jThis, int width, int height): _javaPart(jni::make_global(jThis)) {
 | 
				
			||||||
 | 
					  _width = width;
 | 
				
			||||||
 | 
					  _height = height;
 | 
				
			||||||
 | 
					  _context = OpenGLContext::CreateWithOffscreenSurface(width, height);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					VideoPipeline::~VideoPipeline() {
 | 
				
			||||||
 | 
					  // 1. Remove output surfaces
 | 
				
			||||||
 | 
					  removeFrameProcessorOutputSurface();
 | 
				
			||||||
 | 
					  removeRecordingSessionOutputSurface();
 | 
				
			||||||
 | 
					  removePreviewOutputSurface();
 | 
				
			||||||
 | 
					  // 2. Delete the input textures
 | 
				
			||||||
 | 
					  if (_inputTextureId != NO_TEXTURE) {
 | 
				
			||||||
 | 
					    glDeleteTextures(1, &_inputTextureId);
 | 
				
			||||||
 | 
					    _inputTextureId = NO_TEXTURE;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // 4. Destroy all surfaces
 | 
				
			||||||
 | 
					  _previewOutput = nullptr;
 | 
				
			||||||
 | 
					  _frameProcessorOutput = nullptr;
 | 
				
			||||||
 | 
					  _recordingSessionOutput = nullptr;
 | 
				
			||||||
 | 
					  // 5. Destroy the OpenGL context
 | 
				
			||||||
 | 
					  _context = nullptr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void VideoPipeline::removeFrameProcessorOutputSurface() {
 | 
				
			||||||
 | 
					  if (_frameProcessorOutput) _frameProcessorOutput->destroy();
 | 
				
			||||||
 | 
					  _frameProcessorOutput = nullptr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void VideoPipeline::setFrameProcessorOutputSurface(jobject surface) {
 | 
				
			||||||
 | 
					  // 1. Delete existing output surface
 | 
				
			||||||
 | 
					  removeFrameProcessorOutputSurface();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 2. Set new output surface if it is not null
 | 
				
			||||||
 | 
					  ANativeWindow* window = ANativeWindow_fromSurface(jni::Environment::current(), surface);
 | 
				
			||||||
 | 
					  _frameProcessorOutput = OpenGLRenderer::CreateWithWindowSurface(_context, window);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void VideoPipeline::removeRecordingSessionOutputSurface() {
 | 
				
			||||||
 | 
					  if (_recordingSessionOutput) _recordingSessionOutput->destroy();
 | 
				
			||||||
 | 
					  _recordingSessionOutput = nullptr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void VideoPipeline::setRecordingSessionOutputSurface(jobject surface) {
 | 
				
			||||||
 | 
					  // 1. Delete existing output surface
 | 
				
			||||||
 | 
					  removeRecordingSessionOutputSurface();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 2. Set new output surface if it is not null
 | 
				
			||||||
 | 
					  ANativeWindow* window = ANativeWindow_fromSurface(jni::Environment::current(), surface);
 | 
				
			||||||
 | 
					  _recordingSessionOutput = OpenGLRenderer::CreateWithWindowSurface(_context, window);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void VideoPipeline::removePreviewOutputSurface() {
 | 
				
			||||||
 | 
					  if (_previewOutput) _previewOutput->destroy();
 | 
				
			||||||
 | 
					  _previewOutput = nullptr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void VideoPipeline::setPreviewOutputSurface(jobject surface) {
 | 
				
			||||||
 | 
					  // 1. Delete existing output surface
 | 
				
			||||||
 | 
					  removePreviewOutputSurface();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 2. Set new output surface if it is not null
 | 
				
			||||||
 | 
					  ANativeWindow* window = ANativeWindow_fromSurface(jni::Environment::current(), surface);
 | 
				
			||||||
 | 
					  _previewOutput = OpenGLRenderer::CreateWithWindowSurface(_context, window);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int VideoPipeline::getInputTextureId() {
 | 
				
			||||||
 | 
					  if (_inputTextureId != NO_TEXTURE) return static_cast<int>(_inputTextureId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  _inputTextureId = _context->createTexture();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return static_cast<int>(_inputTextureId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void VideoPipeline::onBeforeFrame() {
 | 
				
			||||||
 | 
					  _context->use();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  glBindTexture(GL_TEXTURE_EXTERNAL_OES, _inputTextureId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void VideoPipeline::onFrame(jni::alias_ref<jni::JArrayFloat> transformMatrixParam) {
 | 
				
			||||||
 | 
					  // Get the OpenGL transform Matrix (transforms, scales, rotations)
 | 
				
			||||||
 | 
					  float transformMatrix[16];
 | 
				
			||||||
 | 
					  transformMatrixParam->getRegion(0, 16, transformMatrix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (_previewOutput) {
 | 
				
			||||||
 | 
					    __android_log_print(ANDROID_LOG_INFO, TAG, "Rendering to Preview..");
 | 
				
			||||||
 | 
					    _previewOutput->renderTextureToSurface(_inputTextureId, transformMatrix);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (_frameProcessorOutput) {
 | 
				
			||||||
 | 
					    __android_log_print(ANDROID_LOG_INFO, TAG, "Rendering to FrameProcessor..");
 | 
				
			||||||
 | 
					    _frameProcessorOutput->renderTextureToSurface(_inputTextureId, transformMatrix);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (_recordingSessionOutput) {
 | 
				
			||||||
 | 
					    __android_log_print(ANDROID_LOG_INFO, TAG, "Rendering to RecordingSession..");
 | 
				
			||||||
 | 
					    _recordingSessionOutput->renderTextureToSurface(_inputTextureId, transformMatrix);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void VideoPipeline::registerNatives() {
 | 
				
			||||||
 | 
					  registerHybrid({
 | 
				
			||||||
 | 
					    makeNativeMethod("initHybrid", VideoPipeline::initHybrid),
 | 
				
			||||||
 | 
					    makeNativeMethod("setFrameProcessorOutputSurface", VideoPipeline::setFrameProcessorOutputSurface),
 | 
				
			||||||
 | 
					    makeNativeMethod("removeFrameProcessorOutputSurface", VideoPipeline::removeFrameProcessorOutputSurface),
 | 
				
			||||||
 | 
					    makeNativeMethod("setRecordingSessionOutputSurface", VideoPipeline::setRecordingSessionOutputSurface),
 | 
				
			||||||
 | 
					    makeNativeMethod("removeRecordingSessionOutputSurface", VideoPipeline::removeRecordingSessionOutputSurface),
 | 
				
			||||||
 | 
					    makeNativeMethod("setPreviewOutputSurface", VideoPipeline::setPreviewOutputSurface),
 | 
				
			||||||
 | 
					    makeNativeMethod("removePreviewOutputSurface", VideoPipeline::removePreviewOutputSurface),
 | 
				
			||||||
 | 
					    makeNativeMethod("getInputTextureId", VideoPipeline::getInputTextureId),
 | 
				
			||||||
 | 
					    makeNativeMethod("onBeforeFrame", VideoPipeline::onBeforeFrame),
 | 
				
			||||||
 | 
					    makeNativeMethod("onFrame", VideoPipeline::onFrame),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} // namespace vision
 | 
				
			||||||
							
								
								
									
										72
									
								
								android/src/main/cpp/VideoPipeline.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								android/src/main/cpp/VideoPipeline.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// Created by Marc Rousavy on 25.08.23.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <jni.h>
 | 
				
			||||||
 | 
					#include <fbjni/fbjni.h>
 | 
				
			||||||
 | 
					#include <EGL/egl.h>
 | 
				
			||||||
 | 
					#include <android/native_window.h>
 | 
				
			||||||
 | 
					#include "PassThroughShader.h"
 | 
				
			||||||
 | 
					#include "OpenGLRenderer.h"
 | 
				
			||||||
 | 
					#include "OpenGLContext.h"
 | 
				
			||||||
 | 
					#include <memory>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace vision {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define NO_TEXTURE 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using namespace facebook;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class VideoPipeline: public jni::HybridClass<VideoPipeline> {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  static auto constexpr kJavaDescriptor = "Lcom/mrousavy/camera/utils/VideoPipeline;";
 | 
				
			||||||
 | 
					  static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis, int width, int height);
 | 
				
			||||||
 | 
					  static void registerNatives();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  ~VideoPipeline();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // -> SurfaceTexture input
 | 
				
			||||||
 | 
					  int getInputTextureId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // <- Frame Processor output
 | 
				
			||||||
 | 
					  void setFrameProcessorOutputSurface(jobject surface);
 | 
				
			||||||
 | 
					  void removeFrameProcessorOutputSurface();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // <- MediaRecorder output
 | 
				
			||||||
 | 
					  void setRecordingSessionOutputSurface(jobject surface);
 | 
				
			||||||
 | 
					  void removeRecordingSessionOutputSurface();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // <- Preview output
 | 
				
			||||||
 | 
					  void setPreviewOutputSurface(jobject surface);
 | 
				
			||||||
 | 
					  void removePreviewOutputSurface();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Frame callbacks
 | 
				
			||||||
 | 
					  void onBeforeFrame();
 | 
				
			||||||
 | 
					  void onFrame(jni::alias_ref<jni::JArrayFloat> transformMatrix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  // Private constructor. Use `create(..)` to create new instances.
 | 
				
			||||||
 | 
					  explicit VideoPipeline(jni::alias_ref<jhybridobject> jThis, int width, int height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  // Input Surface Texture
 | 
				
			||||||
 | 
					  GLuint _inputTextureId = NO_TEXTURE;
 | 
				
			||||||
 | 
					  int _width = 0;
 | 
				
			||||||
 | 
					  int _height = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Output Contexts
 | 
				
			||||||
 | 
					  std::shared_ptr<OpenGLContext> _context = nullptr;
 | 
				
			||||||
 | 
					  std::unique_ptr<OpenGLRenderer> _frameProcessorOutput = nullptr;
 | 
				
			||||||
 | 
					  std::unique_ptr<OpenGLRenderer> _recordingSessionOutput = nullptr;
 | 
				
			||||||
 | 
					  std::unique_ptr<OpenGLRenderer> _previewOutput = nullptr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  friend HybridBase;
 | 
				
			||||||
 | 
					  jni::global_ref<javaobject> _javaPart;
 | 
				
			||||||
 | 
					  static constexpr auto TAG = "VideoPipeline";
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} // namespace vision
 | 
				
			||||||
@@ -5,12 +5,14 @@
 | 
				
			|||||||
#include "JVisionCameraProxy.h"
 | 
					#include "JVisionCameraProxy.h"
 | 
				
			||||||
#include "VisionCameraProxy.h"
 | 
					#include "VisionCameraProxy.h"
 | 
				
			||||||
#include "SkiaRenderer.h"
 | 
					#include "SkiaRenderer.h"
 | 
				
			||||||
 | 
					#include "VideoPipeline.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
 | 
					JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
 | 
				
			||||||
  return facebook::jni::initialize(vm, [] {
 | 
					  return facebook::jni::initialize(vm, [] {
 | 
				
			||||||
    vision::VisionCameraInstaller::registerNatives();
 | 
					    vision::VisionCameraInstaller::registerNatives();
 | 
				
			||||||
    vision::JVisionCameraProxy::registerNatives();
 | 
					    vision::JVisionCameraProxy::registerNatives();
 | 
				
			||||||
    vision::JVisionCameraScheduler::registerNatives();
 | 
					    vision::JVisionCameraScheduler::registerNatives();
 | 
				
			||||||
 | 
					    vision::VideoPipeline::registerNatives();
 | 
				
			||||||
#if VISION_CAMERA_ENABLE_FRAME_PROCESSORS
 | 
					#if VISION_CAMERA_ENABLE_FRAME_PROCESSORS
 | 
				
			||||||
    vision::JFrameProcessor::registerNatives();
 | 
					    vision::JFrameProcessor::registerNatives();
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#include "FrameHostObject.h"
 | 
					#include "FrameHostObject.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <android/log.h>
 | 
					 | 
				
			||||||
#include <fbjni/fbjni.h>
 | 
					#include <fbjni/fbjni.h>
 | 
				
			||||||
#include <jni.h>
 | 
					#include <jni.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,9 +27,6 @@ class JSI_EXPORT FrameHostObject : public jsi::HostObject {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 public:
 | 
					 public:
 | 
				
			||||||
  jni::global_ref<JFrame> frame;
 | 
					  jni::global_ref<JFrame> frame;
 | 
				
			||||||
 | 
					 | 
				
			||||||
 private:
 | 
					 | 
				
			||||||
  static auto constexpr TAG = "VisionCamera";
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
} // namespace vision
 | 
					} // namespace vision
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,7 +68,6 @@ jni::local_ref<jni::JMap<jstring, jobject>> JSIJNIConversion::convertJSIObjectTo
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        auto map = convertJSIObjectToJNIMap(runtime, valueAsObject);
 | 
					        auto map = convertJSIObjectToJNIMap(runtime, valueAsObject);
 | 
				
			||||||
        hashMap->put(key, map);
 | 
					        hashMap->put(key, map);
 | 
				
			||||||
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@@ -139,8 +138,7 @@ jsi::Value JSIJNIConversion::convertJNIObjectToJSIValue(jsi::Runtime &runtime, c
 | 
				
			|||||||
      result.setProperty(runtime, key.c_str(), jsiValue);
 | 
					      result.setProperty(runtime, key.c_str(), jsiValue);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return result;
 | 
					    return result;
 | 
				
			||||||
 | 
					  } else if (object->isInstanceOf(JFrame::javaClassStatic())) {
 | 
				
			||||||
  } if (object->isInstanceOf(JFrame::javaClassStatic())) {
 | 
					 | 
				
			||||||
    // Frame
 | 
					    // Frame
 | 
				
			||||||
    auto frame = static_ref_cast<JFrame>(object);
 | 
					    auto frame = static_ref_cast<JFrame>(object);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,8 +22,8 @@ void JVisionCameraScheduler::dispatchAsync(const std::function<void()>& job) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
void JVisionCameraScheduler::scheduleTrigger() {
 | 
					void JVisionCameraScheduler::scheduleTrigger() {
 | 
				
			||||||
  // 2. schedule `triggerUI` to be called on the java thread
 | 
					  // 2. schedule `triggerUI` to be called on the java thread
 | 
				
			||||||
  static auto method = javaPart_->getClass()->getMethod<void()>("scheduleTrigger");
 | 
					  static auto method = _javaPart->getClass()->getMethod<void()>("scheduleTrigger");
 | 
				
			||||||
  method(javaPart_.get());
 | 
					  method(_javaPart.get());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void JVisionCameraScheduler::trigger() {
 | 
					void JVisionCameraScheduler::trigger() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,12 +33,12 @@ class JVisionCameraScheduler : public jni::HybridClass<JVisionCameraScheduler> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 private:
 | 
					 private:
 | 
				
			||||||
  friend HybridBase;
 | 
					  friend HybridBase;
 | 
				
			||||||
  jni::global_ref<JVisionCameraScheduler::javaobject> javaPart_;
 | 
					  jni::global_ref<JVisionCameraScheduler::javaobject> _javaPart;
 | 
				
			||||||
  std::queue<std::function<void()>> _jobs;
 | 
					  std::queue<std::function<void()>> _jobs;
 | 
				
			||||||
  std::mutex _mutex;
 | 
					  std::mutex _mutex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  explicit JVisionCameraScheduler(jni::alias_ref<JVisionCameraScheduler::jhybridobject> jThis):
 | 
					  explicit JVisionCameraScheduler(jni::alias_ref<JVisionCameraScheduler::jhybridobject> jThis):
 | 
				
			||||||
    javaPart_(jni::make_global(jThis)) {}
 | 
					    _javaPart(jni::make_global(jThis)) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Schedules a call to `trigger` on the VisionCamera FP Thread
 | 
					  // Schedules a call to `trigger` on the VisionCamera FP Thread
 | 
				
			||||||
  void scheduleTrigger();
 | 
					  void scheduleTrigger();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,6 @@ import com.mrousavy.camera.extensions.createPhotoCaptureRequest
 | 
				
			|||||||
import com.mrousavy.camera.extensions.openCamera
 | 
					import com.mrousavy.camera.extensions.openCamera
 | 
				
			||||||
import com.mrousavy.camera.extensions.tryClose
 | 
					import com.mrousavy.camera.extensions.tryClose
 | 
				
			||||||
import com.mrousavy.camera.extensions.zoomed
 | 
					import com.mrousavy.camera.extensions.zoomed
 | 
				
			||||||
import com.mrousavy.camera.frameprocessor.Frame
 | 
					 | 
				
			||||||
import com.mrousavy.camera.frameprocessor.FrameProcessor
 | 
					import com.mrousavy.camera.frameprocessor.FrameProcessor
 | 
				
			||||||
import com.mrousavy.camera.parsers.Flash
 | 
					import com.mrousavy.camera.parsers.Flash
 | 
				
			||||||
import com.mrousavy.camera.parsers.Orientation
 | 
					import com.mrousavy.camera.parsers.Orientation
 | 
				
			||||||
@@ -88,8 +87,17 @@ class CameraSession(private val context: Context,
 | 
				
			|||||||
  private val mutex = Mutex()
 | 
					  private val mutex = Mutex()
 | 
				
			||||||
  private var isRunning = false
 | 
					  private var isRunning = false
 | 
				
			||||||
  private var enableTorch = false
 | 
					  private var enableTorch = false
 | 
				
			||||||
 | 
					  // Video Outputs
 | 
				
			||||||
  private var recording: RecordingSession? = null
 | 
					  private var recording: RecordingSession? = null
 | 
				
			||||||
  private var frameProcessor: FrameProcessor? = null
 | 
					    set(value) {
 | 
				
			||||||
 | 
					      field = value
 | 
				
			||||||
 | 
					      updateVideoOutputs()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  var frameProcessor: FrameProcessor? = null
 | 
				
			||||||
 | 
					    set(value) {
 | 
				
			||||||
 | 
					      field = value
 | 
				
			||||||
 | 
					      updateVideoOutputs()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override val coroutineContext: CoroutineContext = CameraQueues.cameraQueue.coroutineDispatcher
 | 
					  override val coroutineContext: CoroutineContext = CameraQueues.cameraQueue.coroutineDispatcher
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -130,8 +138,14 @@ class CameraSession(private val context: Context,
 | 
				
			|||||||
      Log.i(TAG, "Nothing changed in configuration, canceling..")
 | 
					      Log.i(TAG, "Nothing changed in configuration, canceling..")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.cameraId = cameraId
 | 
					    // 1. Close previous outputs
 | 
				
			||||||
 | 
					    this.outputs?.close()
 | 
				
			||||||
 | 
					    // 2. Assign new outputs
 | 
				
			||||||
    this.outputs = outputs
 | 
					    this.outputs = outputs
 | 
				
			||||||
 | 
					    // 3. Update with existing render targets (surfaces)
 | 
				
			||||||
 | 
					    updateVideoOutputs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.cameraId = cameraId
 | 
				
			||||||
    launch {
 | 
					    launch {
 | 
				
			||||||
      startRunning()
 | 
					      startRunning()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -183,8 +197,12 @@ class CameraSession(private val context: Context,
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun setFrameProcessor(frameProcessor: FrameProcessor?) {
 | 
					  private fun updateVideoOutputs() {
 | 
				
			||||||
    this.frameProcessor = frameProcessor
 | 
					    val videoPipeline = outputs?.videoOutput?.videoPipeline ?: return
 | 
				
			||||||
 | 
					    val previewOutput = outputs?.previewOutput
 | 
				
			||||||
 | 
					    videoPipeline.setRecordingSessionOutput(this.recording)
 | 
				
			||||||
 | 
					    videoPipeline.setFrameProcessorOutput(this.frameProcessor)
 | 
				
			||||||
 | 
					    videoPipeline.setPreviewOutput(previewOutput?.surface)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  suspend fun takePhoto(qualityPrioritization: QualityPrioritization,
 | 
					  suspend fun takePhoto(qualityPrioritization: QualityPrioritization,
 | 
				
			||||||
@@ -229,20 +247,6 @@ class CameraSession(private val context: Context,
 | 
				
			|||||||
    photoOutputSynchronizer.set(image.timestamp, image)
 | 
					    photoOutputSynchronizer.set(image.timestamp, image)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override fun onVideoFrameCaptured(image: Image) {
 | 
					 | 
				
			||||||
    // TODO: Correctly get orientation and everything
 | 
					 | 
				
			||||||
    val frame = Frame(image, System.currentTimeMillis(), Orientation.PORTRAIT, false)
 | 
					 | 
				
			||||||
    frame.incrementRefCount()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Call (Skia-) Frame Processor
 | 
					 | 
				
			||||||
    frameProcessor?.call(frame)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Write Image to the Recording
 | 
					 | 
				
			||||||
    recording?.appendImage(image)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    frame.decrementRefCount()
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  suspend fun startRecording(enableAudio: Boolean,
 | 
					  suspend fun startRecording(enableAudio: Boolean,
 | 
				
			||||||
                             codec: VideoCodec,
 | 
					                             codec: VideoCodec,
 | 
				
			||||||
                             fileType: VideoFileType,
 | 
					                             fileType: VideoFileType,
 | 
				
			||||||
@@ -253,7 +257,7 @@ class CameraSession(private val context: Context,
 | 
				
			|||||||
      val outputs = outputs ?: throw CameraNotReadyError()
 | 
					      val outputs = outputs ?: throw CameraNotReadyError()
 | 
				
			||||||
      val videoOutput = outputs.videoOutput ?: throw VideoNotEnabledError()
 | 
					      val videoOutput = outputs.videoOutput ?: throw VideoNotEnabledError()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      val recording = RecordingSession(context, enableAudio, videoOutput.size, fps, codec, orientation, fileType, callback, onError)
 | 
					      val recording = RecordingSession(context, videoOutput.size, enableAudio, fps, codec, orientation, fileType, callback, onError)
 | 
				
			||||||
      recording.start()
 | 
					      recording.start()
 | 
				
			||||||
      this.recording = recording
 | 
					      this.recording = recording
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -497,7 +501,8 @@ class CameraSession(private val context: Context,
 | 
				
			|||||||
        val captureRequest = camera.createCaptureRequest(template)
 | 
					        val captureRequest = camera.createCaptureRequest(template)
 | 
				
			||||||
        outputs.previewOutput?.let { output ->
 | 
					        outputs.previewOutput?.let { output ->
 | 
				
			||||||
          Log.i(TAG, "Adding output surface ${output.outputType}..")
 | 
					          Log.i(TAG, "Adding output surface ${output.outputType}..")
 | 
				
			||||||
          captureRequest.addTarget(output.surface)
 | 
					          // TODO: Add here again?
 | 
				
			||||||
 | 
					          // captureRequest.addTarget(output.surface)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        outputs.videoOutput?.let { output ->
 | 
					        outputs.videoOutput?.let { output ->
 | 
				
			||||||
          Log.i(TAG, "Adding output surface ${output.outputType}..")
 | 
					          Log.i(TAG, "Adding output surface ${output.outputType}..")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,7 +96,7 @@ class CameraView(context: Context) : FrameLayout(context) {
 | 
				
			|||||||
  internal var frameProcessor: FrameProcessor? = null
 | 
					  internal var frameProcessor: FrameProcessor? = null
 | 
				
			||||||
    set(value) {
 | 
					    set(value) {
 | 
				
			||||||
      field = value
 | 
					      field = value
 | 
				
			||||||
      cameraSession.setFrameProcessor(frameProcessor)
 | 
					      cameraSession.frameProcessor = frameProcessor
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private val inputOrientation: Orientation
 | 
					  private val inputOrientation: Orientation
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -180,11 +180,6 @@ class CameraViewModule(reactContext: ReactApplicationContext): ReactContextBaseJ
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @ReactMethod
 | 
					  @ReactMethod
 | 
				
			||||||
  fun requestCameraPermission(promise: Promise) {
 | 
					  fun requestCameraPermission(promise: Promise) {
 | 
				
			||||||
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
 | 
					 | 
				
			||||||
      // API 21 and below always grants permission on app install
 | 
					 | 
				
			||||||
      return promise.resolve(PermissionStatus.GRANTED.unionValue)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    val activity = reactApplicationContext.currentActivity
 | 
					    val activity = reactApplicationContext.currentActivity
 | 
				
			||||||
    if (activity is PermissionAwareActivity) {
 | 
					    if (activity is PermissionAwareActivity) {
 | 
				
			||||||
      val currentRequestCode = RequestCode++
 | 
					      val currentRequestCode = RequestCode++
 | 
				
			||||||
@@ -205,11 +200,6 @@ class CameraViewModule(reactContext: ReactApplicationContext): ReactContextBaseJ
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @ReactMethod
 | 
					  @ReactMethod
 | 
				
			||||||
  fun requestMicrophonePermission(promise: Promise) {
 | 
					  fun requestMicrophonePermission(promise: Promise) {
 | 
				
			||||||
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
 | 
					 | 
				
			||||||
      // API 21 and below always grants permission on app install
 | 
					 | 
				
			||||||
      return promise.resolve(PermissionStatus.GRANTED.unionValue)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    val activity = reactApplicationContext.currentActivity
 | 
					    val activity = reactApplicationContext.currentActivity
 | 
				
			||||||
    if (activity is PermissionAwareActivity) {
 | 
					    if (activity is PermissionAwareActivity) {
 | 
				
			||||||
      val currentRequestCode = RequestCode++
 | 
					      val currentRequestCode = RequestCode++
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,10 +63,6 @@ fun CameraCharacteristics.getVideoSizes(cameraId: String, format: Int): List<Siz
 | 
				
			|||||||
fun CameraCharacteristics.getPhotoSizes(format: Int): List<Size> {
 | 
					fun CameraCharacteristics.getPhotoSizes(format: Int): List<Size> {
 | 
				
			||||||
  val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
 | 
					  val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
 | 
				
			||||||
  val sizes = config.getOutputSizes(format) ?: emptyArray()
 | 
					  val sizes = config.getOutputSizes(format) ?: emptyArray()
 | 
				
			||||||
  val highResSizes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 | 
					  val highResSizes = config.getHighResolutionOutputSizes(format) ?: emptyArray()
 | 
				
			||||||
    config.getHighResolutionOutputSizes(format)
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    null
 | 
					 | 
				
			||||||
  } ?: emptyArray()
 | 
					 | 
				
			||||||
  return sizes.plus(highResSizes).toList()
 | 
					  return sizes.plus(highResSizes).toList()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,6 @@ import android.hardware.camera2.params.OutputConfiguration
 | 
				
			|||||||
import android.hardware.camera2.params.SessionConfiguration
 | 
					import android.hardware.camera2.params.SessionConfiguration
 | 
				
			||||||
import android.os.Build
 | 
					import android.os.Build
 | 
				
			||||||
import android.util.Log
 | 
					import android.util.Log
 | 
				
			||||||
import android.view.Surface
 | 
					 | 
				
			||||||
import androidx.annotation.RequiresApi
 | 
					import androidx.annotation.RequiresApi
 | 
				
			||||||
import com.mrousavy.camera.CameraQueues
 | 
					import com.mrousavy.camera.CameraQueues
 | 
				
			||||||
import com.mrousavy.camera.CameraSessionCannotBeConfiguredError
 | 
					import com.mrousavy.camera.CameraSessionCannotBeConfiguredError
 | 
				
			||||||
@@ -63,47 +62,35 @@ suspend fun CameraDevice.createCaptureSession(cameraManager: CameraManager,
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 | 
					    val outputConfigurations = arrayListOf<OutputConfiguration>()
 | 
				
			||||||
      // API >= 24
 | 
					    outputs.previewOutput?.let { output ->
 | 
				
			||||||
      val outputConfigurations = arrayListOf<OutputConfiguration>()
 | 
					      // TODO: add here again?
 | 
				
			||||||
      outputs.previewOutput?.let { output ->
 | 
					      // outputConfigurations.add(output.toOutputConfiguration(characteristics))
 | 
				
			||||||
        outputConfigurations.add(output.toOutputConfiguration(characteristics))
 | 
					    }
 | 
				
			||||||
      }
 | 
					    outputs.photoOutput?.let { output ->
 | 
				
			||||||
      outputs.photoOutput?.let { output ->
 | 
					      outputConfigurations.add(output.toOutputConfiguration(characteristics))
 | 
				
			||||||
        outputConfigurations.add(output.toOutputConfiguration(characteristics))
 | 
					    }
 | 
				
			||||||
      }
 | 
					    outputs.videoOutput?.let { output ->
 | 
				
			||||||
      outputs.videoOutput?.let { output ->
 | 
					      outputConfigurations.add(output.toOutputConfiguration(characteristics))
 | 
				
			||||||
        outputConfigurations.add(output.toOutputConfiguration(characteristics))
 | 
					    }
 | 
				
			||||||
      }
 | 
					    if (outputs.enableHdr == true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
 | 
				
			||||||
      if (outputs.enableHdr == true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
 | 
					      val supportedProfiles = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES)
 | 
				
			||||||
        val supportedProfiles = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES)
 | 
					      val hdrProfile = supportedProfiles?.bestProfile ?: supportedProfiles?.supportedProfiles?.firstOrNull()
 | 
				
			||||||
        val hdrProfile = supportedProfiles?.bestProfile ?: supportedProfiles?.supportedProfiles?.firstOrNull()
 | 
					      if (hdrProfile != null) {
 | 
				
			||||||
        if (hdrProfile != null) {
 | 
					        Log.i(TAG, "Camera $id: Using HDR Profile $hdrProfile...")
 | 
				
			||||||
          Log.i(TAG, "Camera $id: Using HDR Profile $hdrProfile...")
 | 
					        outputConfigurations.forEach { it.dynamicRangeProfile = hdrProfile }
 | 
				
			||||||
          outputConfigurations.forEach { it.dynamicRangeProfile = hdrProfile }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          Log.w(TAG, "Camera $id: HDR was enabled, but the device does not support any matching HDR profile!")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
 | 
					 | 
				
			||||||
        // API >=28
 | 
					 | 
				
			||||||
        Log.i(TAG, "Using new API (>=28)")
 | 
					 | 
				
			||||||
        val config = SessionConfiguration(sessionType.toSessionType(), outputConfigurations, queue.executor, callback)
 | 
					 | 
				
			||||||
        this.createCaptureSession(config)
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        // API >=24
 | 
					        Log.w(TAG, "Camera $id: HDR was enabled, but the device does not support any matching HDR profile!")
 | 
				
			||||||
        Log.i(TAG, "Using legacy API (<28)")
 | 
					 | 
				
			||||||
        this.createCaptureSessionByOutputConfigurations(outputConfigurations, callback, queue.handler)
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
 | 
				
			||||||
 | 
					      Log.i(TAG, "Using new API (>=28)")
 | 
				
			||||||
 | 
					      val config = SessionConfiguration(sessionType.toSessionType(), outputConfigurations, queue.executor, callback)
 | 
				
			||||||
 | 
					      this.createCaptureSession(config)
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // API <24
 | 
					      Log.i(TAG, "Using legacy API (<28)")
 | 
				
			||||||
      Log.i(TAG, "Using legacy API (<24)")
 | 
					      this.createCaptureSessionByOutputConfigurations(outputConfigurations, callback, queue.handler)
 | 
				
			||||||
      val surfaces = arrayListOf<Surface>()
 | 
					 | 
				
			||||||
      outputs.previewOutput?.let { surfaces.add(it.surface) }
 | 
					 | 
				
			||||||
      outputs.photoOutput?.let { surfaces.add(it.surface) }
 | 
					 | 
				
			||||||
      outputs.videoOutput?.let { surfaces.add(it.surface) }
 | 
					 | 
				
			||||||
      this.createCaptureSession(surfaces, callback, queue.handler)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
package com.mrousavy.camera.utils
 | 
					package com.mrousavy.camera.utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.content.Context
 | 
					import android.content.Context
 | 
				
			||||||
import android.media.Image
 | 
					 | 
				
			||||||
import android.media.ImageWriter
 | 
					import android.media.ImageWriter
 | 
				
			||||||
import android.media.MediaCodec
 | 
					import android.media.MediaCodec
 | 
				
			||||||
import android.media.MediaRecorder
 | 
					import android.media.MediaRecorder
 | 
				
			||||||
@@ -13,12 +12,11 @@ import com.mrousavy.camera.RecorderError
 | 
				
			|||||||
import com.mrousavy.camera.parsers.Orientation
 | 
					import com.mrousavy.camera.parsers.Orientation
 | 
				
			||||||
import com.mrousavy.camera.parsers.VideoCodec
 | 
					import com.mrousavy.camera.parsers.VideoCodec
 | 
				
			||||||
import com.mrousavy.camera.parsers.VideoFileType
 | 
					import com.mrousavy.camera.parsers.VideoFileType
 | 
				
			||||||
import com.mrousavy.camera.utils.outputs.CameraOutputs
 | 
					 | 
				
			||||||
import java.io.File
 | 
					import java.io.File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RecordingSession(context: Context,
 | 
					class RecordingSession(context: Context,
 | 
				
			||||||
 | 
					                       val size: Size,
 | 
				
			||||||
                       private val enableAudio: Boolean,
 | 
					                       private val enableAudio: Boolean,
 | 
				
			||||||
                       private val videoSize: Size,
 | 
					 | 
				
			||||||
                       private val fps: Int? = null,
 | 
					                       private val fps: Int? = null,
 | 
				
			||||||
                       private val codec: VideoCodec = VideoCodec.H264,
 | 
					                       private val codec: VideoCodec = VideoCodec.H264,
 | 
				
			||||||
                       private val orientation: Orientation,
 | 
					                       private val orientation: Orientation,
 | 
				
			||||||
@@ -40,14 +38,9 @@ class RecordingSession(context: Context,
 | 
				
			|||||||
  private val outputFile: File
 | 
					  private val outputFile: File
 | 
				
			||||||
  private var startTime: Long? = null
 | 
					  private var startTime: Long? = null
 | 
				
			||||||
  private var imageWriter: ImageWriter? = null
 | 
					  private var imageWriter: ImageWriter? = null
 | 
				
			||||||
  val surface: Surface
 | 
					  val surface: Surface = MediaCodec.createPersistentInputSurface()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  init {
 | 
					  init {
 | 
				
			||||||
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
 | 
					 | 
				
			||||||
      throw Error("Video Recording is only supported on Devices running Android version 23 (M) or newer.")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    surface = MediaCodec.createPersistentInputSurface()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    outputFile = File.createTempFile("mrousavy", fileType.toExtension(), context.cacheDir)
 | 
					    outputFile = File.createTempFile("mrousavy", fileType.toExtension(), context.cacheDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -61,7 +54,7 @@ class RecordingSession(context: Context,
 | 
				
			|||||||
    recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
 | 
					    recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
 | 
				
			||||||
    recorder.setOutputFile(outputFile.absolutePath)
 | 
					    recorder.setOutputFile(outputFile.absolutePath)
 | 
				
			||||||
    recorder.setVideoEncodingBitRate(VIDEO_BIT_RATE)
 | 
					    recorder.setVideoEncodingBitRate(VIDEO_BIT_RATE)
 | 
				
			||||||
    recorder.setVideoSize(videoSize.width, videoSize.height)
 | 
					    recorder.setVideoSize(size.height, size.width)
 | 
				
			||||||
    if (fps != null) recorder.setVideoFrameRate(fps)
 | 
					    if (fps != null) recorder.setVideoFrameRate(fps)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Log.i(TAG, "Using $codec Video Codec..")
 | 
					    Log.i(TAG, "Using $codec Video Codec..")
 | 
				
			||||||
@@ -74,7 +67,7 @@ class RecordingSession(context: Context,
 | 
				
			|||||||
      recorder.setAudioChannels(AUDIO_CHANNELS)
 | 
					      recorder.setAudioChannels(AUDIO_CHANNELS)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    recorder.setInputSurface(surface)
 | 
					    recorder.setInputSurface(surface)
 | 
				
			||||||
    recorder.setOrientationHint(orientation.toDegrees())
 | 
					    //recorder.setOrientationHint(orientation.toDegrees())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    recorder.setOnErrorListener { _, what, extra ->
 | 
					    recorder.setOnErrorListener { _, what, extra ->
 | 
				
			||||||
      Log.e(TAG, "MediaRecorder Error: $what ($extra)")
 | 
					      Log.e(TAG, "MediaRecorder Error: $what ($extra)")
 | 
				
			||||||
@@ -109,10 +102,8 @@ class RecordingSession(context: Context,
 | 
				
			|||||||
        recorder.stop()
 | 
					        recorder.stop()
 | 
				
			||||||
        recorder.release()
 | 
					        recorder.release()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 | 
					        imageWriter?.close()
 | 
				
			||||||
          imageWriter?.close()
 | 
					        imageWriter = null
 | 
				
			||||||
          imageWriter = null
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } catch (e: Error) {
 | 
					      } catch (e: Error) {
 | 
				
			||||||
        Log.e(TAG, "Failed to stop MediaRecorder!", e)
 | 
					        Log.e(TAG, "Failed to stop MediaRecorder!", e)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -125,9 +116,6 @@ class RecordingSession(context: Context,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  fun pause() {
 | 
					  fun pause() {
 | 
				
			||||||
    synchronized(this) {
 | 
					    synchronized(this) {
 | 
				
			||||||
      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
 | 
					 | 
				
			||||||
        throw Error("Pausing a recording is only supported on Devices running Android version 24 (N) or newer.")
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      Log.i(TAG, "Pausing Recording Session..")
 | 
					      Log.i(TAG, "Pausing Recording Session..")
 | 
				
			||||||
      recorder.pause()
 | 
					      recorder.pause()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -135,32 +123,13 @@ class RecordingSession(context: Context,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  fun resume() {
 | 
					  fun resume() {
 | 
				
			||||||
    synchronized(this) {
 | 
					    synchronized(this) {
 | 
				
			||||||
      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
 | 
					 | 
				
			||||||
        throw Error("Resuming a recording is only supported on Devices running Android version 24 (N) or newer.")
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      Log.i(TAG, "Resuming Recording Session..")
 | 
					      Log.i(TAG, "Resuming Recording Session..")
 | 
				
			||||||
      recorder.resume()
 | 
					      recorder.resume()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fun appendImage(image: Image) {
 | 
					 | 
				
			||||||
    synchronized(this) {
 | 
					 | 
				
			||||||
      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
 | 
					 | 
				
			||||||
        throw Error("Video Recording is only supported on Devices running Android version 23 (M) or newer.")
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // TODO: Correctly mirror/flip Image in OpenGL pipeline, otherwise flipping camera while recording results in inverted frames
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (imageWriter == null) {
 | 
					 | 
				
			||||||
        imageWriter = ImageWriter.newInstance(surface, CameraOutputs.VIDEO_OUTPUT_BUFFER_SIZE)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      image.timestamp = System.nanoTime()
 | 
					 | 
				
			||||||
      imageWriter!!.queueInputImage(image)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  override fun toString(): String {
 | 
					  override fun toString(): String {
 | 
				
			||||||
    val audio = if (enableAudio) "with audio" else "without audio"
 | 
					    val audio = if (enableAudio) "with audio" else "without audio"
 | 
				
			||||||
    return "${videoSize.width} x ${videoSize.height} @ $fps FPS $codec $fileType $orientation RecordingSession ($audio)"
 | 
					    return "${size.width} x ${size.height} @ $fps FPS $codec $fileType $orientation RecordingSession ($audio)"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										185
									
								
								android/src/main/java/com/mrousavy/camera/utils/VideoPipeline.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								android/src/main/java/com/mrousavy/camera/utils/VideoPipeline.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,185 @@
 | 
				
			|||||||
 | 
					package com.mrousavy.camera.utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.graphics.ImageFormat
 | 
				
			||||||
 | 
					import android.graphics.SurfaceTexture
 | 
				
			||||||
 | 
					import android.media.ImageReader
 | 
				
			||||||
 | 
					import android.media.ImageWriter
 | 
				
			||||||
 | 
					import android.media.MediaRecorder
 | 
				
			||||||
 | 
					import android.util.Log
 | 
				
			||||||
 | 
					import android.view.Surface
 | 
				
			||||||
 | 
					import com.facebook.jni.HybridData
 | 
				
			||||||
 | 
					import com.mrousavy.camera.frameprocessor.Frame
 | 
				
			||||||
 | 
					import com.mrousavy.camera.frameprocessor.FrameProcessor
 | 
				
			||||||
 | 
					import com.mrousavy.camera.parsers.Orientation
 | 
				
			||||||
 | 
					import java.io.Closeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * An OpenGL pipeline for streaming Camera Frames to one or more outputs.
 | 
				
			||||||
 | 
					 * Currently, [VideoPipeline] can stream to a [FrameProcessor] and a [MediaRecorder].
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param [width] The width of the Frames to stream (> 0)
 | 
				
			||||||
 | 
					 * @param [height] The height of the Frames to stream (> 0)
 | 
				
			||||||
 | 
					 * @param [format] The format of the Frames to stream. ([ImageFormat.PRIVATE], [ImageFormat.YUV_420_888] or [ImageFormat.JPEG])
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Suppress("KotlinJniMissingFunction")
 | 
				
			||||||
 | 
					class VideoPipeline(val width: Int,
 | 
				
			||||||
 | 
					                    val height: Int,
 | 
				
			||||||
 | 
					                    val format: Int = ImageFormat.PRIVATE): SurfaceTexture.OnFrameAvailableListener, Closeable {
 | 
				
			||||||
 | 
					  companion object {
 | 
				
			||||||
 | 
					    private const val MAX_IMAGES = 5
 | 
				
			||||||
 | 
					    private const val TAG = "VideoPipeline"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val mHybridData: HybridData
 | 
				
			||||||
 | 
					  private var openGLTextureId: Int? = null
 | 
				
			||||||
 | 
					  private var transformMatrix = FloatArray(16)
 | 
				
			||||||
 | 
					  private var isActive = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Output 1
 | 
				
			||||||
 | 
					  private var frameProcessor: FrameProcessor? = null
 | 
				
			||||||
 | 
					  private var imageReader: ImageReader? = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Output 2
 | 
				
			||||||
 | 
					  private var recordingSession: RecordingSession? = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Output 3
 | 
				
			||||||
 | 
					  private var previewSurface: Surface? = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Input
 | 
				
			||||||
 | 
					  private val surfaceTexture: SurfaceTexture
 | 
				
			||||||
 | 
					  val surface: Surface
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  init {
 | 
				
			||||||
 | 
					    mHybridData = initHybrid(width, height)
 | 
				
			||||||
 | 
					    surfaceTexture = SurfaceTexture(false)
 | 
				
			||||||
 | 
					    surfaceTexture.setDefaultBufferSize(width, height)
 | 
				
			||||||
 | 
					    surfaceTexture.setOnFrameAvailableListener(this)
 | 
				
			||||||
 | 
					    surface = Surface(surfaceTexture)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override fun close() {
 | 
				
			||||||
 | 
					    synchronized(this) {
 | 
				
			||||||
 | 
					      isActive = false
 | 
				
			||||||
 | 
					      imageReader?.close()
 | 
				
			||||||
 | 
					      imageReader = null
 | 
				
			||||||
 | 
					      frameProcessor = null
 | 
				
			||||||
 | 
					      recordingSession = null
 | 
				
			||||||
 | 
					      surfaceTexture.release()
 | 
				
			||||||
 | 
					      mHybridData.resetNative()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override fun onFrameAvailable(surfaceTexture: SurfaceTexture) {
 | 
				
			||||||
 | 
					    synchronized(this) {
 | 
				
			||||||
 | 
					      if (!isActive) return@synchronized
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 1. Attach Surface to OpenGL context
 | 
				
			||||||
 | 
					      if (openGLTextureId == null) {
 | 
				
			||||||
 | 
					        openGLTextureId = getInputTextureId()
 | 
				
			||||||
 | 
					        surfaceTexture.attachToGLContext(openGLTextureId!!)
 | 
				
			||||||
 | 
					        Log.i(TAG, "Attached Texture to Context $openGLTextureId")
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 2. Prepare the OpenGL context (eglMakeCurrent)
 | 
				
			||||||
 | 
					      onBeforeFrame()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 3. Update the OpenGL texture
 | 
				
			||||||
 | 
					      surfaceTexture.updateTexImage()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 4. Get the transform matrix from the SurfaceTexture (rotations/scales applied by Camera)
 | 
				
			||||||
 | 
					      surfaceTexture.getTransformMatrix(transformMatrix)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 5. Draw it with applied rotation/mirroring
 | 
				
			||||||
 | 
					      onFrame(transformMatrix)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private fun getImageReader(): ImageReader {
 | 
				
			||||||
 | 
					    val imageReader = ImageReader.newInstance(width, height, format, MAX_IMAGES)
 | 
				
			||||||
 | 
					    imageReader.setOnImageAvailableListener({ reader ->
 | 
				
			||||||
 | 
					      Log.i("VideoPipeline", "ImageReader::onImageAvailable!")
 | 
				
			||||||
 | 
					      val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // TODO: Get correct orientation and isMirrored
 | 
				
			||||||
 | 
					      val frame = Frame(image, image.timestamp, Orientation.PORTRAIT, false)
 | 
				
			||||||
 | 
					      frame.incrementRefCount()
 | 
				
			||||||
 | 
					      frameProcessor?.call(frame)
 | 
				
			||||||
 | 
					      frame.decrementRefCount()
 | 
				
			||||||
 | 
					    }, null)
 | 
				
			||||||
 | 
					    return imageReader
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Configures the Pipeline to also call the given [FrameProcessor].
 | 
				
			||||||
 | 
					   * * If the [frameProcessor] is `null`, this output channel will be removed.
 | 
				
			||||||
 | 
					   * * If the [frameProcessor] is not `null`, the [VideoPipeline] will create Frames
 | 
				
			||||||
 | 
					   * using an [ImageWriter] and call the [FrameProcessor] with those Frames.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  fun setFrameProcessorOutput(frameProcessor: FrameProcessor?) {
 | 
				
			||||||
 | 
					    synchronized(this) {
 | 
				
			||||||
 | 
					      Log.i(TAG, "Setting $width x $height FrameProcessor Output...")
 | 
				
			||||||
 | 
					      this.frameProcessor = frameProcessor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (frameProcessor != null) {
 | 
				
			||||||
 | 
					        if (this.imageReader == null) {
 | 
				
			||||||
 | 
					          // 1. Create new ImageReader that just calls the Frame Processor
 | 
				
			||||||
 | 
					          this.imageReader = getImageReader()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 2. Configure OpenGL pipeline to stream Frames into the ImageReader's surface
 | 
				
			||||||
 | 
					        setFrameProcessorOutputSurface(imageReader!!.surface)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // 1. Configure OpenGL pipeline to stop streaming Frames into the ImageReader's surface
 | 
				
			||||||
 | 
					        removeFrameProcessorOutputSurface()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 2. Close the ImageReader
 | 
				
			||||||
 | 
					        this.imageReader?.close()
 | 
				
			||||||
 | 
					        this.imageReader = null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Configures the Pipeline to also write Frames to a Surface from a [MediaRecorder].
 | 
				
			||||||
 | 
					   * * If the [surface] is `null`, this output channel will be removed.
 | 
				
			||||||
 | 
					   * * If the [surface] is not `null`, the [VideoPipeline] will write Frames to this Surface.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  fun setRecordingSessionOutput(recordingSession: RecordingSession?) {
 | 
				
			||||||
 | 
					    synchronized(this) {
 | 
				
			||||||
 | 
					      Log.i(TAG, "Setting $width x $height RecordingSession Output...")
 | 
				
			||||||
 | 
					      if (recordingSession != null) {
 | 
				
			||||||
 | 
					        // Configure OpenGL pipeline to stream Frames into the Recording Session's surface
 | 
				
			||||||
 | 
					        setRecordingSessionOutputSurface(recordingSession.surface)
 | 
				
			||||||
 | 
					        this.recordingSession = recordingSession
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Configure OpenGL pipeline to stop streaming Frames into the Recording Session's surface
 | 
				
			||||||
 | 
					        removeRecordingSessionOutputSurface()
 | 
				
			||||||
 | 
					        this.recordingSession = null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fun setPreviewOutput(surface: Surface?) {
 | 
				
			||||||
 | 
					    synchronized(this) {
 | 
				
			||||||
 | 
					      Log.i(TAG, "Setting Preview Output...")
 | 
				
			||||||
 | 
					      if (surface != null) {
 | 
				
			||||||
 | 
					        setPreviewOutputSurface(surface)
 | 
				
			||||||
 | 
					        this.previewSurface = surface
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        removePreviewOutputSurface()
 | 
				
			||||||
 | 
					        this.previewSurface = null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private external fun getInputTextureId(): Int
 | 
				
			||||||
 | 
					  private external fun onBeforeFrame()
 | 
				
			||||||
 | 
					  private external fun onFrame(transformMatrix: FloatArray)
 | 
				
			||||||
 | 
					  private external fun setFrameProcessorOutputSurface(surface: Any)
 | 
				
			||||||
 | 
					  private external fun removeFrameProcessorOutputSurface()
 | 
				
			||||||
 | 
					  private external fun setRecordingSessionOutputSurface(surface: Any)
 | 
				
			||||||
 | 
					  private external fun removeRecordingSessionOutputSurface()
 | 
				
			||||||
 | 
					  private external fun setPreviewOutputSurface(surface: Any)
 | 
				
			||||||
 | 
					  private external fun removePreviewOutputSurface()
 | 
				
			||||||
 | 
					  private external fun initHybrid(width: Int, height: Int): HybridData
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,11 +1,9 @@
 | 
				
			|||||||
package com.mrousavy.camera.utils.outputs
 | 
					package com.mrousavy.camera.utils.outputs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.graphics.ImageFormat
 | 
					import android.graphics.ImageFormat
 | 
				
			||||||
import android.hardware.HardwareBuffer
 | 
					 | 
				
			||||||
import android.hardware.camera2.CameraManager
 | 
					import android.hardware.camera2.CameraManager
 | 
				
			||||||
import android.media.Image
 | 
					import android.media.Image
 | 
				
			||||||
import android.media.ImageReader
 | 
					import android.media.ImageReader
 | 
				
			||||||
import android.os.Build
 | 
					 | 
				
			||||||
import android.util.Log
 | 
					import android.util.Log
 | 
				
			||||||
import android.util.Size
 | 
					import android.util.Size
 | 
				
			||||||
import android.view.Surface
 | 
					import android.view.Surface
 | 
				
			||||||
@@ -14,6 +12,7 @@ import com.mrousavy.camera.extensions.closestToOrMax
 | 
				
			|||||||
import com.mrousavy.camera.extensions.getPhotoSizes
 | 
					import com.mrousavy.camera.extensions.getPhotoSizes
 | 
				
			||||||
import com.mrousavy.camera.extensions.getPreviewSize
 | 
					import com.mrousavy.camera.extensions.getPreviewSize
 | 
				
			||||||
import com.mrousavy.camera.extensions.getVideoSizes
 | 
					import com.mrousavy.camera.extensions.getVideoSizes
 | 
				
			||||||
 | 
					import com.mrousavy.camera.utils.VideoPipeline
 | 
				
			||||||
import java.io.Closeable
 | 
					import java.io.Closeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CameraOutputs(val cameraId: String,
 | 
					class CameraOutputs(val cameraId: String,
 | 
				
			||||||
@@ -25,7 +24,6 @@ class CameraOutputs(val cameraId: String,
 | 
				
			|||||||
                    val callback: Callback): Closeable {
 | 
					                    val callback: Callback): Closeable {
 | 
				
			||||||
  companion object {
 | 
					  companion object {
 | 
				
			||||||
    private const val TAG = "CameraOutputs"
 | 
					    private const val TAG = "CameraOutputs"
 | 
				
			||||||
    const val VIDEO_OUTPUT_BUFFER_SIZE = 3
 | 
					 | 
				
			||||||
    const val PHOTO_OUTPUT_BUFFER_SIZE = 3
 | 
					    const val PHOTO_OUTPUT_BUFFER_SIZE = 3
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -39,14 +37,13 @@ class CameraOutputs(val cameraId: String,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  interface Callback {
 | 
					  interface Callback {
 | 
				
			||||||
    fun onPhotoCaptured(image: Image)
 | 
					    fun onPhotoCaptured(image: Image)
 | 
				
			||||||
    fun onVideoFrameCaptured(image: Image)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var previewOutput: SurfaceOutput? = null
 | 
					  var previewOutput: SurfaceOutput? = null
 | 
				
			||||||
    private set
 | 
					    private set
 | 
				
			||||||
  var photoOutput: ImageReaderOutput? = null
 | 
					  var photoOutput: ImageReaderOutput? = null
 | 
				
			||||||
    private set
 | 
					    private set
 | 
				
			||||||
  var videoOutput: SurfaceOutput? = null
 | 
					  var videoOutput: VideoPipelineOutput? = null
 | 
				
			||||||
    private set
 | 
					    private set
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val size: Int
 | 
					  val size: Int
 | 
				
			||||||
@@ -118,23 +115,11 @@ class CameraOutputs(val cameraId: String,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Video output: High resolution repeating images (startRecording() or useFrameProcessor())
 | 
					    // Video output: High resolution repeating images (startRecording() or useFrameProcessor())
 | 
				
			||||||
    if (video != null) {
 | 
					    if (video != null) {
 | 
				
			||||||
      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) throw Error("Video Recordings and/or Frame Processors are only available on API 29 and above!")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      val size = characteristics.getVideoSizes(cameraId, video.format).closestToOrMax(video.targetSize)
 | 
					      val size = characteristics.getVideoSizes(cameraId, video.format).closestToOrMax(video.targetSize)
 | 
				
			||||||
 | 
					      val videoPipeline = VideoPipeline(size.width, size.height, video.format)
 | 
				
			||||||
      val flags = HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or HardwareBuffer.USAGE_VIDEO_ENCODE
 | 
					 | 
				
			||||||
      val imageReader = ImageReader.newInstance(size.width, size.height, video.format, VIDEO_OUTPUT_BUFFER_SIZE, flags)
 | 
					 | 
				
			||||||
      imageReader.setOnImageAvailableListener({ reader ->
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
          val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener
 | 
					 | 
				
			||||||
          callback.onVideoFrameCaptured(image)
 | 
					 | 
				
			||||||
        } catch (e: IllegalStateException) {
 | 
					 | 
				
			||||||
          Log.e(TAG, "Failed to acquire a new Image, dropping a Frame.. The Frame Processor cannot keep up with the Camera's FPS!", e)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }, CameraQueues.videoQueue.handler)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Log.i(TAG, "Adding ${size.width}x${size.height} video output. (Format: ${video.format})")
 | 
					      Log.i(TAG, "Adding ${size.width}x${size.height} video output. (Format: ${video.format})")
 | 
				
			||||||
      videoOutput = ImageReaderOutput(imageReader, SurfaceOutput.OutputType.VIDEO)
 | 
					      videoOutput = VideoPipelineOutput(videoPipeline, SurfaceOutput.OutputType.VIDEO)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Log.i(TAG, "Prepared $size Outputs for Camera $cameraId!")
 | 
					    Log.i(TAG, "Prepared $size Outputs for Camera $cameraId!")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,7 +35,6 @@ open class SurfaceOutput(val surface: Surface,
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @RequiresApi(Build.VERSION_CODES.N)
 | 
					 | 
				
			||||||
  fun toOutputConfiguration(characteristics: CameraCharacteristics): OutputConfiguration {
 | 
					  fun toOutputConfiguration(characteristics: CameraCharacteristics): OutputConfiguration {
 | 
				
			||||||
    val result = OutputConfiguration(surface)
 | 
					    val result = OutputConfiguration(surface)
 | 
				
			||||||
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
 | 
					    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					package com.mrousavy.camera.utils.outputs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.util.Log
 | 
				
			||||||
 | 
					import android.util.Size
 | 
				
			||||||
 | 
					import com.mrousavy.camera.utils.VideoPipeline
 | 
				
			||||||
 | 
					import java.io.Closeable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * A [SurfaceOutput] that uses a [VideoPipeline] as it's surface.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class VideoPipelineOutput(val videoPipeline: VideoPipeline,
 | 
				
			||||||
 | 
					                          outputType: OutputType,
 | 
				
			||||||
 | 
					                          dynamicRangeProfile: Long? = null): Closeable, SurfaceOutput(videoPipeline.surface, Size(videoPipeline.width, videoPipeline.height), outputType, dynamicRangeProfile) {
 | 
				
			||||||
 | 
					  override fun close() {
 | 
				
			||||||
 | 
					    Log.i(TAG, "Closing ${videoPipeline.width}x${videoPipeline.height} Video Pipeline..")
 | 
				
			||||||
 | 
					    videoPipeline.close()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override fun toString(): String {
 | 
				
			||||||
 | 
					    return "$outputType (${videoPipeline.width} x ${videoPipeline.height} in format #${videoPipeline.format})"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -75,7 +75,7 @@ Before opening an issue, make sure you try the following:
 | 
				
			|||||||
   2. Set `buildToolsVersion` to `33.0.0` or higher
 | 
					   2. Set `buildToolsVersion` to `33.0.0` or higher
 | 
				
			||||||
   3. Set `compileSdkVersion` to `33` or higher
 | 
					   3. Set `compileSdkVersion` to `33` or higher
 | 
				
			||||||
   4. Set `targetSdkVersion` to `33` or higher
 | 
					   4. Set `targetSdkVersion` to `33` or higher
 | 
				
			||||||
   5. Set `minSdkVersion` to `21` or higher
 | 
					   5. Set `minSdkVersion` to `26` or higher
 | 
				
			||||||
   6. Set `ndkVersion` to `"23.1.7779620"` or higher
 | 
					   6. Set `ndkVersion` to `"23.1.7779620"` or higher
 | 
				
			||||||
   7. Update the Gradle Build-Tools version to `7.3.1` or higher:
 | 
					   7. Update the Gradle Build-Tools version to `7.3.1` or higher:
 | 
				
			||||||
      ```
 | 
					      ```
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
buildscript {
 | 
					buildscript {
 | 
				
			||||||
    ext {
 | 
					    ext {
 | 
				
			||||||
        buildToolsVersion = "33.0.0"
 | 
					        buildToolsVersion = "33.0.0"
 | 
				
			||||||
        minSdkVersion = 21
 | 
					        minSdkVersion = 26
 | 
				
			||||||
        compileSdkVersion = 33
 | 
					        compileSdkVersion = 33
 | 
				
			||||||
        targetSdkVersion = 33
 | 
					        targetSdkVersion = 33
 | 
				
			||||||
        ndkVersion = "23.1.7779620"
 | 
					        ndkVersion = "23.1.7779620"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user