Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create C++/OpenGL-based Video Pipeline for more efficient Recording and Frame Processing #1721

Merged
merged 42 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
499d913
Create `VideoPipeline` c++
mrousavy Aug 25, 2023
34f139e
Remove folly C++ dependency
mrousavy Aug 25, 2023
c1cd8f6
Create `VideoPipeline` HybridClass
mrousavy Aug 25, 2023
7b5a0b4
Set up OpenGL
mrousavy Aug 25, 2023
a2a73b8
Add outputs
mrousavy Aug 25, 2023
642ada5
Update VideoPipeline.kt
mrousavy Aug 25, 2023
febb735
Bum `minSdkVersion` to `26`
mrousavy Aug 25, 2023
d5b007c
Create `VideoPipelineOutput`
mrousavy Aug 25, 2023
601492a
Create output funcs
mrousavy Aug 25, 2023
3e2dc5e
Set output pipelines
mrousavy Aug 25, 2023
b574b4a
Add FP/Recording on Output change
mrousavy Aug 25, 2023
5294726
Update VideoPipeline.cpp
mrousavy Aug 28, 2023
6e38032
Create `PassThroughShader`
mrousavy Aug 28, 2023
1d064ee
Try to draw? I have honestly no idea
mrousavy Aug 28, 2023
7512892
fix: Fix `setFrameProcessor` nameclash
mrousavy Aug 28, 2023
6316b11
fix: Fix `high-res-sizes` being null
mrousavy Aug 28, 2023
0475424
Add preview output
mrousavy Aug 29, 2023
3cb2175
Create `OpenGLContext.cpp`
mrousavy Aug 29, 2023
3dff39e
Make screen red
mrousavy Aug 29, 2023
85d09a5
This _should_ work (MESSY)
mrousavy Aug 29, 2023
29e51d9
FINALLY RENDER TEXTURE
mrousavy Aug 29, 2023
7223e1c
Rotate
mrousavy Aug 29, 2023
f5a9f54
Mirror
mrousavy Aug 29, 2023
eb2e779
Clean up a bit
mrousavy Aug 29, 2023
c1d2041
Add `getWidth()`/`getHeight()`
mrousavy Aug 29, 2023
47b5d75
Cleanup
mrousavy Aug 29, 2023
b9407de
fix: Use uniforms instead of attributes
mrousavy Aug 29, 2023
771ac77
Draw with passed rotation/mirror mode
mrousavy Aug 29, 2023
30332f5
feat: Use SurfaceTexture's transformMatrix in OpenGL pipeline (#1727)
mrousavy Aug 29, 2023
19f7999
Update VideoPipeline.kt
mrousavy Aug 29, 2023
6f10e10
Measure elapsed time
mrousavy Aug 29, 2023
cc83e9e
fix: Fix low resolution
mrousavy Aug 29, 2023
9174bd5
Render to offscreen
mrousavy Aug 29, 2023
e0935e0
Render to every context
mrousavy Aug 29, 2023
de1a2af
Release `SurfaceTexture` on close
mrousavy Aug 29, 2023
5ad75a1
Use one OpenGL context to render to multiple EGLSurfaces
mrousavy Aug 29, 2023
8e138fe
Clean up a bit
mrousavy Aug 29, 2023
859a86b
fix: Fix recording pipeline not triggering
mrousavy Aug 29, 2023
b417ee0
fix: Synchronize close to prevent nulls
mrousavy Aug 29, 2023
3d08576
Update OpenGLRenderer.cpp
mrousavy Aug 29, 2023
885b7b8
fix: Hardcode Android recorder size
mrousavy Aug 29, 2023
0f18bb2
fix: Fix codestyle
mrousavy Aug 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ set(BUILD_DIR ${CMAKE_SOURCE_DIR}/build)
set(CMAKE_VERBOSE_MAKEFILE ON)
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)
find_package(ReactAndroid REQUIRED CONFIG)
find_package(fbjni REQUIRED CONFIG)
Expand All @@ -25,6 +21,7 @@ add_library(
SHARED
../cpp/JSITypedArray.cpp
src/main/cpp/VisionCamera.cpp
src/main/cpp/VideoPipeline.cpp
# Frame Processor
src/main/cpp/frameprocessor/FrameHostObject.cpp
src/main/cpp/frameprocessor/FrameProcessorPluginHostObject.cpp
Expand Down Expand Up @@ -60,7 +57,6 @@ target_link_libraries(
android # <-- Android JNI core
ReactAndroid::jsi # <-- RN: JSI
ReactAndroid::reactnativejni # <-- RN: React Native JNI bindings
ReactAndroid::folly_runtime # <-- RN: For casting JSI <> Java objects
fbjni::fbjni # <-- fbjni
)

Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ android {
}

defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 21)
minSdkVersion safeExtGet('minSdkVersion', 26)
compileSdkVersion safeExtGet('compileSdkVersion', 33)
targetSdkVersion safeExtGet('targetSdkVersion', 33)
versionCode 1
Expand Down
4 changes: 0 additions & 4 deletions android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ org.gradle.configureondemand=true
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Fri Feb 19 20:46:14 CET 2021
VisionCamera_buildToolsVersion=30.0.0
VisionCamera_compileSdkVersion=31
VisionCamera_kotlinVersion=1.7.20
VisionCamera_targetSdkVersion=31
VisionCamera_ndkVersion=21.4.7075529
android.enableJetifier=true
android.useAndroidX=true
131 changes: 131 additions & 0 deletions android/src/main/cpp/VideoPipeline.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//
// Created by Marc Rousavy on 25.08.23.
//

#include "VideoPipeline.h"
#include "OpenGLError.h"

namespace vision {

jni::local_ref<VideoPipeline::jhybriddata> VideoPipeline::initHybrid(jni::alias_ref<jhybridobject> jThis) {
return makeCxxInstance(jThis);
}

VideoPipeline::VideoPipeline(jni::alias_ref<jhybridobject> jThis): _javaPart(jni::make_global(jThis)) { }

VideoPipeline::~VideoPipeline() {
if (_context.display != EGL_NO_DISPLAY) {
eglMakeCurrent(_context.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (_context.surface != EGL_NO_SURFACE) {
__android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGL Surface...");
eglDestroySurface(_context.display, _context.surface);
_context.surface = EGL_NO_SURFACE;
}
if (_context.context != EGL_NO_CONTEXT) {
__android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGL Context...");
eglDestroyContext(_context.display, _context.context);
_context.context = EGL_NO_CONTEXT;
}
__android_log_print(ANDROID_LOG_INFO, TAG, "Destroying OpenGL Display...");
eglTerminate(_context.display);
_context.display = EGL_NO_DISPLAY;
}
}

void VideoPipeline::setSize(int width, int height) {
_width = width;
_height = height;
}

GLContext& VideoPipeline::getGLContext() {
bool successful;
// EGLDisplay
if (_context.display == EGL_NO_DISPLAY) {
__android_log_print(ANDROID_LOG_INFO, TAG, "Initializing EGLDisplay..");
_context.display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (_context.display == EGL_NO_DISPLAY) throw OpenGLError("Failed to get default OpenGL Display!");

EGLint major;
EGLint minor;
successful = eglInitialize(_context.display, &major, &minor);
if (!successful) throw OpenGLError("Failed to initialize OpenGL!");
}

// EGLConfig
if (_context.config == nullptr) {
__android_log_print(ANDROID_LOG_INFO, TAG, "Initializing EGLConfig..");
EGLint attributes[] = {EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_SURFACE_TYPE, EGL_PBUFFER_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(_context.display, attributes, &_context.config, 1, &numConfigs);
if (!successful || numConfigs == 0) throw OpenGLError("Failed to choose OpenGL config!");
}

// EGLContext
if (_context.context == EGL_NO_CONTEXT) {
__android_log_print(ANDROID_LOG_INFO, TAG, "Initializing EGLContext..");
EGLint contextAttributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
_context.context = eglCreateContext(_context.display, _context.config, nullptr, contextAttributes);
if (_context.context == EGL_NO_CONTEXT) throw OpenGLError("Failed to create OpenGL context!");
}

// EGLSurface
if (_context.surface == EGL_NO_SURFACE) {
// If we don't have a surface at all
__android_log_print(ANDROID_LOG_INFO, TAG, "Initializing EGLSurface..");
EGLint attributes[] = {EGL_WIDTH, _width,
EGL_HEIGHT, _height,
EGL_NONE};
_context.surface = eglCreatePbufferSurface(_context.display, _context.config, attributes);
} else {
// If the surface size has changed we need to resize the surface
int currentWidth = 0, currentHeight = 0;
eglQuerySurface(_context.display, _context.surface, EGL_WIDTH, &currentWidth);
eglQuerySurface(_context.display, _context.surface, EGL_HEIGHT, &currentHeight);
if (currentWidth != _width || currentHeight != _height) {
__android_log_print(ANDROID_LOG_INFO, TAG, "Resizing EGLSurface from (%i x %i) to (%i x %i)..",
currentWidth, currentHeight, _width, _height);
// Destroy current surface
eglDestroySurface(_context.display, _context.surface);
_context.surface = EGL_NO_SURFACE;
// Create new one with new dimensions
EGLint attributes[] = {EGL_WIDTH, _width,
EGL_HEIGHT, _height,
EGL_NONE};
_context.surface = eglCreatePbufferSurface(_context.display, _context.config, attributes);
}
}
if (_context.surface == EGL_NO_SURFACE) {
throw OpenGLError("Failed to create OpenGL Surface!");
}

successful = eglMakeCurrent(_context.display, _context.surface, _context.surface, _context.context);
if (!successful || eglGetError() != EGL_SUCCESS) throw OpenGLError("Failed to use current OpenGL context!");

return _context;
}

void VideoPipeline::setFrameProcessorOutputSurface(jobject surface) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[cpplint] reported by reviewdog 🐶
Redundant blank line at the start of a code block should be deleted. [whitespace/blank_line] [2]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[cpplint] reported by reviewdog 🐶
Redundant blank line at the end of a code block should be deleted. [whitespace/blank_line] [3]

}

void VideoPipeline::setRecordingSessionOutputSurface(jobject surface, jint width, jint height) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[cpplint] reported by reviewdog 🐶
Redundant blank line at the start of a code block should be deleted. [whitespace/blank_line] [2]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[cpplint] reported by reviewdog 🐶
Redundant blank line at the end of a code block should be deleted. [whitespace/blank_line] [3]

}

void VideoPipeline::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", VideoPipeline::initHybrid),
makeNativeMethod("setFrameProcessorOutputSurface", VideoPipeline::setFrameProcessorOutputSurface),
makeNativeMethod("setRecordingSessionOutputSurface", VideoPipeline::setRecordingSessionOutputSurface),
});
}

} // vision
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[cpplint] reported by reviewdog 🐶
Could not find a newline character at the end of the file. [whitespace/ending_newline] [5]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[cpplint] reported by reviewdog 🐶
Namespace should be terminated with "// namespace vision" [readability/namespace] [5]

53 changes: 53 additions & 0 deletions android/src/main/cpp/VideoPipeline.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// Created by Marc Rousavy on 25.08.23.
//

#pragma once

#include <jni.h>
#include <fbjni/fbjni.h>
#include <EGL/egl.h>

namespace vision {

using namespace facebook;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[cpplint] reported by reviewdog 🐶
Redundant blank line at the start of a code block should be deleted. [whitespace/blank_line] [2]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[cpplint] reported by reviewdog 🐶
Redundant blank line at the end of a code block should be deleted. [whitespace/blank_line] [3]

struct GLContext {
EGLDisplay display = EGL_NO_DISPLAY;
EGLSurface surface = EGL_NO_SURFACE;
EGLContext context = EGL_NO_CONTEXT;
EGLConfig config = nullptr;
};

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);
static void registerNatives();

public:
~VideoPipeline();
void setFrameProcessorOutputSurface(jobject surface);
void setRecordingSessionOutputSurface(jobject surface, jint width, jint height);

private:
// Private constructor. Use `create(..)` to create new instances.
explicit VideoPipeline(jni::alias_ref<jhybridobject> jThis);

private:
GLContext& getGLContext();
void setSize(int width, int height);

private:
GLContext _context;
int _width = 0;
int _height = 0;

private:
friend HybridBase;
jni::global_ref<javaobject> _javaPart;
static constexpr auto TAG = "VideoPipeline";
};

} // vision
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[cpplint] reported by reviewdog 🐶
Namespace should be terminated with "// namespace vision" [readability/namespace] [5]


2 changes: 2 additions & 0 deletions android/src/main/cpp/VisionCamera.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
#include "JVisionCameraProxy.h"
#include "VisionCameraProxy.h"
#include "SkiaRenderer.h"
#include "VideoPipeline.h"

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
return facebook::jni::initialize(vm, [] {
vision::VisionCameraInstaller::registerNatives();
vision::JVisionCameraProxy::registerNatives();
vision::JVisionCameraScheduler::registerNatives();
vision::VideoPipeline::registerNatives();
#if VISION_CAMERA_ENABLE_FRAME_PROCESSORS
vision::JFrameProcessor::registerNatives();
#endif
Expand Down
1 change: 0 additions & 1 deletion android/src/main/cpp/frameprocessor/FrameHostObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

#include "FrameHostObject.h"

#include <android/log.h>
#include <fbjni/fbjni.h>
#include <jni.h>

Expand Down
3 changes: 0 additions & 3 deletions android/src/main/cpp/frameprocessor/FrameHostObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ class JSI_EXPORT FrameHostObject : public jsi::HostObject {

public:
jni::global_ref<JFrame> frame;

private:
static auto constexpr TAG = "VisionCamera";
};

} // namespace vision
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ void JVisionCameraScheduler::dispatchAsync(const std::function<void()>& job) {

void JVisionCameraScheduler::scheduleTrigger() {
// 2. schedule `triggerUI` to be called on the java thread
static auto method = javaPart_->getClass()->getMethod<void()>("scheduleTrigger");
method(javaPart_.get());
static auto method = _javaPart->getClass()->getMethod<void()>("scheduleTrigger");
method(_javaPart.get());
}

void JVisionCameraScheduler::trigger() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ class JVisionCameraScheduler : public jni::HybridClass<JVisionCameraScheduler> {

private:
friend HybridBase;
jni::global_ref<JVisionCameraScheduler::javaobject> javaPart_;
jni::global_ref<JVisionCameraScheduler::javaobject> _javaPart;
std::queue<std::function<void()>> _jobs;
std::mutex _mutex;

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
void scheduleTrigger();
Expand Down
2 changes: 1 addition & 1 deletion android/src/main/java/com/mrousavy/camera/CameraSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ class CameraSession(private val context: Context,
val outputs = outputs ?: throw CameraNotReadyError()
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()
this.recording = recording
}
Expand Down
10 changes: 0 additions & 10 deletions android/src/main/java/com/mrousavy/camera/CameraViewModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,6 @@ class CameraViewModule(reactContext: ReactApplicationContext): ReactContextBaseJ

@ReactMethod
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
if (activity is PermissionAwareActivity) {
val currentRequestCode = RequestCode++
Expand All @@ -205,11 +200,6 @@ class CameraViewModule(reactContext: ReactApplicationContext): ReactContextBaseJ

@ReactMethod
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
if (activity is PermissionAwareActivity) {
val currentRequestCode = RequestCode++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,6 @@ fun CameraCharacteristics.getVideoSizes(cameraId: String, format: Int): List<Siz
fun CameraCharacteristics.getPhotoSizes(format: Int): List<Size> {
val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
val sizes = config.getOutputSizes(format) ?: emptyArray()
val highResSizes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
config.getHighResolutionOutputSizes(format)
} else {
null
} ?: emptyArray()
val highResSizes = config.getHighResolutionOutputSizes(format)
return sizes.plus(highResSizes).toList()
}
Loading
Loading