feat: Make Reanimated optional (disable Frame Processors if REA v2 is not installed) (#412)
* Fix building iOS without Reanimated * Conditionally compile Frame Processors (gradle) * Conditionally use externalNativeBuild * Remove Reanimated import * fix: Conditionally load REA/VisionCamera libraries * fix: Add disable FP to docs * fix: Fix dummy placeholder for Scheduler.mm * fix: Fix dummy `Scheduler` declaration * fix: Only init `CameraView` C++ side if frame processors are enabled * fix: Install JSI Bindings on Frame Processor Manager ctor * fix: Wrong conditional * whoops
This commit is contained in:
parent
77e065d961
commit
be5ec69b02
@ -84,6 +84,9 @@ def getExtOrIntegerDefault(name) {
|
|||||||
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['VisionCamera_' + name]).toInteger()
|
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['VisionCamera_' + name]).toInteger()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def reanimated = rootProject.subprojects.find { it.name == 'react-native-reanimated' }
|
||||||
|
def ENABLE_FRAME_PROCESSORS = !getExtOrDefault("disableFrameProcessors") && reanimated != null
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
|
compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
|
||||||
buildToolsVersion getExtOrDefault('buildToolsVersion')
|
buildToolsVersion getExtOrDefault('buildToolsVersion')
|
||||||
@ -93,14 +96,16 @@ android {
|
|||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
|
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
|
||||||
|
|
||||||
externalNativeBuild {
|
if (ENABLE_FRAME_PROCESSORS) {
|
||||||
cmake {
|
externalNativeBuild {
|
||||||
cppFlags "-fexceptions", "-frtti", "-std=c++1y", "-DONANDROID"
|
cmake {
|
||||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
cppFlags "-fexceptions", "-frtti", "-std=c++1y", "-DONANDROID"
|
||||||
arguments '-DANDROID_STL=c++_shared',
|
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
"-DREACT_NATIVE_VERSION=${REACT_NATIVE_VERSION}",
|
arguments '-DANDROID_STL=c++_shared',
|
||||||
"-DNODE_MODULES_DIR=${rootDir}/../node_modules",
|
"-DREACT_NATIVE_VERSION=${REACT_NATIVE_VERSION}",
|
||||||
"-DFOR_HERMES=${FOR_HERMES}"
|
"-DNODE_MODULES_DIR=${rootDir}/../node_modules",
|
||||||
|
"-DFOR_HERMES=${FOR_HERMES}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,9 +114,11 @@ android {
|
|||||||
javaMaxHeapSize "4g"
|
javaMaxHeapSize "4g"
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
if (ENABLE_FRAME_PROCESSORS) {
|
||||||
cmake {
|
externalNativeBuild {
|
||||||
path "CMakeLists.txt"
|
cmake {
|
||||||
|
path "CMakeLists.txt"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,25 +224,28 @@ def kotlin_version = getExtOrDefault('kotlinVersion')
|
|||||||
dependencies {
|
dependencies {
|
||||||
// noinspection GradleDynamicVersion
|
// noinspection GradleDynamicVersion
|
||||||
implementation 'com.facebook.react:react-native:+'
|
implementation 'com.facebook.react:react-native:+'
|
||||||
implementation project(':react-native-reanimated')
|
|
||||||
|
|
||||||
//noinspection GradleDynamicVersion
|
if (ENABLE_FRAME_PROCESSORS) {
|
||||||
extractHeaders("com.facebook.fbjni:fbjni:+:headers")
|
implementation project(':react-native-reanimated')
|
||||||
//noinspection GradleDynamicVersion
|
|
||||||
extractJNI("com.facebook.fbjni:fbjni:+")
|
|
||||||
|
|
||||||
def rnAAR = fileTree("${rootDir}/../node_modules/react-native/android").matching({ it.include "**/**/*.aar" }).singleFile
|
//noinspection GradleDynamicVersion
|
||||||
def jscAAR = fileTree("${rootDir}/../node_modules/jsc-android/dist/org/webkit/android-jsc").matching({ it.include "**/**/*.aar" }).singleFile
|
extractHeaders("com.facebook.fbjni:fbjni:+:headers")
|
||||||
|
//noinspection GradleDynamicVersion
|
||||||
|
extractJNI("com.facebook.fbjni:fbjni:+")
|
||||||
|
|
||||||
def inputFile = new File(rootDir, '../node_modules/react-native/package.json')
|
def rnAAR = fileTree("${rootDir}/../node_modules/react-native/android").matching({ it.include "**/**/*.aar" }).singleFile
|
||||||
def json = new JsonSlurper().parseText(inputFile.text)
|
def jscAAR = fileTree("${rootDir}/../node_modules/jsc-android/dist/org/webkit/android-jsc").matching({ it.include "**/**/*.aar" }).singleFile
|
||||||
def reactNativeVersion = json.version as String
|
|
||||||
def (major, minor, patch) = reactNativeVersion.tokenize('.')
|
|
||||||
|
|
||||||
def jsEngine = FOR_HERMES ? "hermes" : "jsc"
|
def inputFile = new File(rootDir, '../node_modules/react-native/package.json')
|
||||||
def reaAAR = "${rootDir}/../node_modules/react-native-reanimated/android/react-native-reanimated-${minor}-${jsEngine}.aar"
|
def json = new JsonSlurper().parseText(inputFile.text)
|
||||||
|
def reactNativeVersion = json.version as String
|
||||||
|
def (major, minor, patch) = reactNativeVersion.tokenize('.')
|
||||||
|
|
||||||
extractJNI(files(rnAAR, jscAAR, reaAAR))
|
def jsEngine = FOR_HERMES ? "hermes" : "jsc"
|
||||||
|
def reaAAR = "${rootDir}/../node_modules/react-native-reanimated/android/react-native-reanimated-${minor}-${jsEngine}.aar"
|
||||||
|
|
||||||
|
extractJNI(files(rnAAR, jscAAR, reaAAR))
|
||||||
|
}
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.5.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.5.2"
|
||||||
@ -252,236 +262,239 @@ dependencies {
|
|||||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
// third-party-ndk deps headers
|
|
||||||
// mostly a copy of https://github.com/software-mansion/react-native-reanimated/blob/master/android/build.gradle#L115
|
|
||||||
|
|
||||||
def downloadsDir = new File("$buildDir/downloads")
|
if (ENABLE_FRAME_PROCESSORS) {
|
||||||
def thirdPartyNdkDir = new File("$buildDir/third-party-ndk")
|
// third-party-ndk deps headers
|
||||||
def thirdPartyVersionsFile = new File("${rootDir}/../node_modules/react-native/ReactAndroid/gradle.properties")
|
// mostly a copy of https://github.com/software-mansion/react-native-reanimated/blob/master/android/build.gradle#L115
|
||||||
def thirdPartyVersions = new Properties()
|
|
||||||
thirdPartyVersions.load(new FileInputStream(thirdPartyVersionsFile))
|
|
||||||
|
|
||||||
def BOOST_VERSION = thirdPartyVersions["BOOST_VERSION"]
|
def downloadsDir = new File("$buildDir/downloads")
|
||||||
def boost_file = new File(downloadsDir, "boost_${BOOST_VERSION}.tar.gz")
|
def thirdPartyNdkDir = new File("$buildDir/third-party-ndk")
|
||||||
def DOUBLE_CONVERSION_VERSION = thirdPartyVersions["DOUBLE_CONVERSION_VERSION"]
|
def thirdPartyVersionsFile = new File("${rootDir}/../node_modules/react-native/ReactAndroid/gradle.properties")
|
||||||
def double_conversion_file = new File(downloadsDir, "double-conversion-${DOUBLE_CONVERSION_VERSION}.tar.gz")
|
def thirdPartyVersions = new Properties()
|
||||||
def FOLLY_VERSION = thirdPartyVersions["FOLLY_VERSION"]
|
thirdPartyVersions.load(new FileInputStream(thirdPartyVersionsFile))
|
||||||
def folly_file = new File(downloadsDir, "folly-${FOLLY_VERSION}.tar.gz")
|
|
||||||
def GLOG_VERSION = thirdPartyVersions["GLOG_VERSION"]
|
|
||||||
def glog_file = new File(downloadsDir, "glog-${GLOG_VERSION}.tar.gz")
|
|
||||||
|
|
||||||
task createNativeDepsDirectories {
|
def BOOST_VERSION = thirdPartyVersions["BOOST_VERSION"]
|
||||||
doLast {
|
def boost_file = new File(downloadsDir, "boost_${BOOST_VERSION}.tar.gz")
|
||||||
downloadsDir.mkdirs()
|
def DOUBLE_CONVERSION_VERSION = thirdPartyVersions["DOUBLE_CONVERSION_VERSION"]
|
||||||
thirdPartyNdkDir.mkdirs()
|
def double_conversion_file = new File(downloadsDir, "double-conversion-${DOUBLE_CONVERSION_VERSION}.tar.gz")
|
||||||
}
|
def FOLLY_VERSION = thirdPartyVersions["FOLLY_VERSION"]
|
||||||
}
|
def folly_file = new File(downloadsDir, "folly-${FOLLY_VERSION}.tar.gz")
|
||||||
|
def GLOG_VERSION = thirdPartyVersions["GLOG_VERSION"]
|
||||||
|
def glog_file = new File(downloadsDir, "glog-${GLOG_VERSION}.tar.gz")
|
||||||
|
|
||||||
task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) {
|
task createNativeDepsDirectories {
|
||||||
src("https://github.com/react-native-community/boost-for-react-native/releases/download/v${BOOST_VERSION.replace("_", ".")}-0/boost_${BOOST_VERSION}.tar.gz")
|
doLast {
|
||||||
onlyIfNewer(true)
|
downloadsDir.mkdirs()
|
||||||
overwrite(false)
|
thirdPartyNdkDir.mkdirs()
|
||||||
dest(boost_file)
|
|
||||||
}
|
|
||||||
|
|
||||||
task prepareBoost(dependsOn: downloadBoost, type: Copy) {
|
|
||||||
from(tarTree(resources.gzip(downloadBoost.dest)))
|
|
||||||
from("src/main/jni/third-party/boost/Android.mk")
|
|
||||||
include("Android.mk", "boost_${BOOST_VERSION}/boost/**/*.hpp", "boost/boost/**/*.hpp")
|
|
||||||
includeEmptyDirs = false
|
|
||||||
into("$thirdPartyNdkDir") // /boost_X_XX_X
|
|
||||||
doLast {
|
|
||||||
file("$thirdPartyNdkDir/boost_${BOOST_VERSION}").renameTo("$thirdPartyNdkDir/boost")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task downloadDoubleConversion(dependsOn: createNativeDepsDirectories, type: Download) {
|
|
||||||
src("https://github.com/google/double-conversion/archive/v${DOUBLE_CONVERSION_VERSION}.tar.gz")
|
|
||||||
onlyIfNewer(true)
|
|
||||||
overwrite(false)
|
|
||||||
dest(double_conversion_file)
|
|
||||||
}
|
|
||||||
|
|
||||||
task prepareDoubleConversion(dependsOn: downloadDoubleConversion, type: Copy) {
|
|
||||||
from(tarTree(downloadDoubleConversion.dest))
|
|
||||||
from("src/main/jni/third-party/double-conversion/Android.mk")
|
|
||||||
include("double-conversion-${DOUBLE_CONVERSION_VERSION}/src/**/*", "Android.mk")
|
|
||||||
filesMatching("*/src/**/*", { fname -> fname.path = "double-conversion/${fname.name}" })
|
|
||||||
includeEmptyDirs = false
|
|
||||||
into("$thirdPartyNdkDir/double-conversion")
|
|
||||||
}
|
|
||||||
|
|
||||||
task downloadFolly(dependsOn: createNativeDepsDirectories, type: Download) {
|
|
||||||
src("https://github.com/facebook/folly/archive/v${FOLLY_VERSION}.tar.gz")
|
|
||||||
onlyIfNewer(true)
|
|
||||||
overwrite(false)
|
|
||||||
dest(folly_file)
|
|
||||||
}
|
|
||||||
|
|
||||||
task prepareFolly(dependsOn: downloadFolly, type: Copy) {
|
|
||||||
from(tarTree(downloadFolly.dest))
|
|
||||||
from("src/main/jni/third-party/folly/Android.mk")
|
|
||||||
include("folly-${FOLLY_VERSION}/folly/**/*", "Android.mk")
|
|
||||||
eachFile { fname -> fname.path = (fname.path - "folly-${FOLLY_VERSION}/") }
|
|
||||||
includeEmptyDirs = false
|
|
||||||
into("$thirdPartyNdkDir/folly")
|
|
||||||
}
|
|
||||||
|
|
||||||
task downloadGlog(dependsOn: createNativeDepsDirectories, type: Download) {
|
|
||||||
src("https://github.com/google/glog/archive/v${GLOG_VERSION}.tar.gz")
|
|
||||||
onlyIfNewer(true)
|
|
||||||
overwrite(false)
|
|
||||||
dest(glog_file)
|
|
||||||
}
|
|
||||||
|
|
||||||
task prepareGlog(dependsOn: downloadGlog, type: Copy) {
|
|
||||||
from(tarTree(downloadGlog.dest))
|
|
||||||
from("src/main/jni/third-party/glog/")
|
|
||||||
include("glog-${GLOG_VERSION}/src/**/*", "Android.mk", "config.h")
|
|
||||||
includeEmptyDirs = false
|
|
||||||
filesMatching("**/*.h.in") {
|
|
||||||
filter(ReplaceTokens, tokens: [
|
|
||||||
ac_cv_have_unistd_h : "1",
|
|
||||||
ac_cv_have_stdint_h : "1",
|
|
||||||
ac_cv_have_systypes_h : "1",
|
|
||||||
ac_cv_have_inttypes_h : "1",
|
|
||||||
ac_cv_have_libgflags : "0",
|
|
||||||
ac_google_start_namespace : "namespace google {",
|
|
||||||
ac_cv_have_uint16_t : "1",
|
|
||||||
ac_cv_have_u_int16_t : "1",
|
|
||||||
ac_cv_have___uint16 : "0",
|
|
||||||
ac_google_end_namespace : "}",
|
|
||||||
ac_cv_have___builtin_expect : "1",
|
|
||||||
ac_google_namespace : "google",
|
|
||||||
ac_cv___attribute___noinline : "__attribute__ ((noinline))",
|
|
||||||
ac_cv___attribute___noreturn : "__attribute__ ((noreturn))",
|
|
||||||
ac_cv___attribute___printf_4_5: "__attribute__((__format__ (__printf__, 4, 5)))"
|
|
||||||
])
|
|
||||||
it.path = (it.name - ".in")
|
|
||||||
}
|
|
||||||
into("$thirdPartyNdkDir/glog")
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
copy {
|
|
||||||
from(fileTree(dir: "$thirdPartyNdkDir/glog", includes: ["stl_logging.h", "logging.h", "raw_logging.h", "vlog_is_on.h", "**/src/glog/log_severity.h"]).files)
|
|
||||||
includeEmptyDirs = false
|
|
||||||
into("$thirdPartyNdkDir/glog/exported/glog")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
task prepareThirdPartyNdkHeaders {
|
task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||||
if (!boost_file.exists()) {
|
src("https://github.com/react-native-community/boost-for-react-native/releases/download/v${BOOST_VERSION.replace("_", ".")}-0/boost_${BOOST_VERSION}.tar.gz")
|
||||||
dependsOn(prepareBoost)
|
onlyIfNewer(true)
|
||||||
|
overwrite(false)
|
||||||
|
dest(boost_file)
|
||||||
}
|
}
|
||||||
if (!double_conversion_file.exists()) {
|
|
||||||
dependsOn(prepareDoubleConversion)
|
|
||||||
}
|
|
||||||
if (!folly_file.exists()) {
|
|
||||||
dependsOn(prepareFolly)
|
|
||||||
}
|
|
||||||
if (!glog_file.exists()) {
|
|
||||||
dependsOn(prepareGlog)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareThirdPartyNdkHeaders.mustRunAfter createNativeDepsDirectories
|
task prepareBoost(dependsOn: downloadBoost, type: Copy) {
|
||||||
|
from(tarTree(resources.gzip(downloadBoost.dest)))
|
||||||
task prepareHermes() {
|
from("src/main/jni/third-party/boost/Android.mk")
|
||||||
doLast {
|
include("Android.mk", "boost_${BOOST_VERSION}/boost/**/*.hpp", "boost/boost/**/*.hpp")
|
||||||
def hermesPackagePath = findNodeModulePath(projectDir, "hermes-engine")
|
includeEmptyDirs = false
|
||||||
if (!hermesPackagePath) {
|
into("$thirdPartyNdkDir") // /boost_X_XX_X
|
||||||
throw new GradleScriptException("Could not find the hermes-engine npm package", null)
|
doLast {
|
||||||
}
|
file("$thirdPartyNdkDir/boost_${BOOST_VERSION}").renameTo("$thirdPartyNdkDir/boost")
|
||||||
|
|
||||||
def hermesAAR = file("$hermesPackagePath/android/hermes-debug.aar")
|
|
||||||
if (!hermesAAR.exists()) {
|
|
||||||
throw new GradleScriptException("The hermes-engine npm package is missing \"android/hermes-debug.aar\"", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
def soFiles = zipTree(hermesAAR).matching({ it.include "**/*.so" })
|
|
||||||
|
|
||||||
copy {
|
|
||||||
from soFiles
|
|
||||||
from "$reactNative/ReactAndroid/src/main/jni/first-party/hermes/Android.mk"
|
|
||||||
into "$thirdPartyNdkDir/hermes"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
prepareHermes.mustRunAfter prepareThirdPartyNdkHeaders
|
task downloadDoubleConversion(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||||
|
src("https://github.com/google/double-conversion/archive/v${DOUBLE_CONVERSION_VERSION}.tar.gz")
|
||||||
task prepareJSC {
|
onlyIfNewer(true)
|
||||||
doLast {
|
overwrite(false)
|
||||||
def jscPackagePath = findNodeModulePath(projectDir, "jsc-android")
|
dest(double_conversion_file)
|
||||||
if (!jscPackagePath) {
|
|
||||||
throw new GradleScriptException("Could not find the jsc-android npm package", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
def jscDist = file("$jscPackagePath/dist")
|
|
||||||
if (!jscDist.exists()) {
|
|
||||||
throw new GradleScriptException("The jsc-android npm package is missing its \"dist\" directory", null)
|
|
||||||
}
|
|
||||||
|
|
||||||
def jscAAR = fileTree(jscDist).matching({ it.include "**/android-jsc/**/*.aar" }).singleFile
|
|
||||||
def soFiles = zipTree(jscAAR).matching({ it.include "**/*.so" })
|
|
||||||
|
|
||||||
def headerFiles = fileTree(jscDist).matching({ it.include "**/include/*.h" })
|
|
||||||
|
|
||||||
copy {
|
|
||||||
from(soFiles)
|
|
||||||
from(headerFiles)
|
|
||||||
from("$reactNative/ReactAndroid/src/main/jni/third-party/jsc/Android.mk")
|
|
||||||
|
|
||||||
filesMatching("**/*.h", { it.path = "JavaScriptCore/${it.name}" })
|
|
||||||
|
|
||||||
includeEmptyDirs(false)
|
|
||||||
into("$thirdPartyNdkDir/jsc")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
prepareJSC.mustRunAfter prepareHermes
|
task prepareDoubleConversion(dependsOn: downloadDoubleConversion, type: Copy) {
|
||||||
|
from(tarTree(downloadDoubleConversion.dest))
|
||||||
|
from("src/main/jni/third-party/double-conversion/Android.mk")
|
||||||
|
include("double-conversion-${DOUBLE_CONVERSION_VERSION}/src/**/*", "Android.mk")
|
||||||
|
filesMatching("*/src/**/*", { fname -> fname.path = "double-conversion/${fname.name}" })
|
||||||
|
includeEmptyDirs = false
|
||||||
|
into("$thirdPartyNdkDir/double-conversion")
|
||||||
|
}
|
||||||
|
|
||||||
task extractAARHeaders {
|
task downloadFolly(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||||
doLast {
|
src("https://github.com/facebook/folly/archive/v${FOLLY_VERSION}.tar.gz")
|
||||||
configurations.extractHeaders.files.each {
|
onlyIfNewer(true)
|
||||||
def file = it.absoluteFile
|
overwrite(false)
|
||||||
|
dest(folly_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
task prepareFolly(dependsOn: downloadFolly, type: Copy) {
|
||||||
|
from(tarTree(downloadFolly.dest))
|
||||||
|
from("src/main/jni/third-party/folly/Android.mk")
|
||||||
|
include("folly-${FOLLY_VERSION}/folly/**/*", "Android.mk")
|
||||||
|
eachFile { fname -> fname.path = (fname.path - "folly-${FOLLY_VERSION}/") }
|
||||||
|
includeEmptyDirs = false
|
||||||
|
into("$thirdPartyNdkDir/folly")
|
||||||
|
}
|
||||||
|
|
||||||
|
task downloadGlog(dependsOn: createNativeDepsDirectories, type: Download) {
|
||||||
|
src("https://github.com/google/glog/archive/v${GLOG_VERSION}.tar.gz")
|
||||||
|
onlyIfNewer(true)
|
||||||
|
overwrite(false)
|
||||||
|
dest(glog_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
task prepareGlog(dependsOn: downloadGlog, type: Copy) {
|
||||||
|
from(tarTree(downloadGlog.dest))
|
||||||
|
from("src/main/jni/third-party/glog/")
|
||||||
|
include("glog-${GLOG_VERSION}/src/**/*", "Android.mk", "config.h")
|
||||||
|
includeEmptyDirs = false
|
||||||
|
filesMatching("**/*.h.in") {
|
||||||
|
filter(ReplaceTokens, tokens: [
|
||||||
|
ac_cv_have_unistd_h : "1",
|
||||||
|
ac_cv_have_stdint_h : "1",
|
||||||
|
ac_cv_have_systypes_h : "1",
|
||||||
|
ac_cv_have_inttypes_h : "1",
|
||||||
|
ac_cv_have_libgflags : "0",
|
||||||
|
ac_google_start_namespace : "namespace google {",
|
||||||
|
ac_cv_have_uint16_t : "1",
|
||||||
|
ac_cv_have_u_int16_t : "1",
|
||||||
|
ac_cv_have___uint16 : "0",
|
||||||
|
ac_google_end_namespace : "}",
|
||||||
|
ac_cv_have___builtin_expect : "1",
|
||||||
|
ac_google_namespace : "google",
|
||||||
|
ac_cv___attribute___noinline : "__attribute__ ((noinline))",
|
||||||
|
ac_cv___attribute___noreturn : "__attribute__ ((noreturn))",
|
||||||
|
ac_cv___attribute___printf_4_5: "__attribute__((__format__ (__printf__, 4, 5)))"
|
||||||
|
])
|
||||||
|
it.path = (it.name - ".in")
|
||||||
|
}
|
||||||
|
into("$thirdPartyNdkDir/glog")
|
||||||
|
|
||||||
|
doLast {
|
||||||
copy {
|
copy {
|
||||||
from zipTree(file)
|
from(fileTree(dir: "$thirdPartyNdkDir/glog", includes: ["stl_logging.h", "logging.h", "raw_logging.h", "vlog_is_on.h", "**/src/glog/log_severity.h"]).files)
|
||||||
into "$buildDir/$file.name"
|
includeEmptyDirs = false
|
||||||
include "**/*.h"
|
into("$thirdPartyNdkDir/glog/exported/glog")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extractAARHeaders.mustRunAfter prepareJSC
|
task prepareThirdPartyNdkHeaders {
|
||||||
|
if (!boost_file.exists()) {
|
||||||
|
dependsOn(prepareBoost)
|
||||||
|
}
|
||||||
|
if (!double_conversion_file.exists()) {
|
||||||
|
dependsOn(prepareDoubleConversion)
|
||||||
|
}
|
||||||
|
if (!folly_file.exists()) {
|
||||||
|
dependsOn(prepareFolly)
|
||||||
|
}
|
||||||
|
if (!glog_file.exists()) {
|
||||||
|
dependsOn(prepareGlog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
task extractJNIFiles {
|
prepareThirdPartyNdkHeaders.mustRunAfter createNativeDepsDirectories
|
||||||
doLast {
|
|
||||||
configurations.extractJNI.files.each {
|
task prepareHermes() {
|
||||||
def file = it.absoluteFile
|
doLast {
|
||||||
|
def hermesPackagePath = findNodeModulePath(projectDir, "hermes-engine")
|
||||||
|
if (!hermesPackagePath) {
|
||||||
|
throw new GradleScriptException("Could not find the hermes-engine npm package", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
def hermesAAR = file("$hermesPackagePath/android/hermes-debug.aar")
|
||||||
|
if (!hermesAAR.exists()) {
|
||||||
|
throw new GradleScriptException("The hermes-engine npm package is missing \"android/hermes-debug.aar\"", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
def soFiles = zipTree(hermesAAR).matching({ it.include "**/*.so" })
|
||||||
|
|
||||||
copy {
|
copy {
|
||||||
from zipTree(file)
|
from soFiles
|
||||||
into "$buildDir/$file.name"
|
from "$reactNative/ReactAndroid/src/main/jni/first-party/hermes/Android.mk"
|
||||||
include "jni/**/*"
|
into "$thirdPartyNdkDir/hermes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extractJNIFiles.mustRunAfter extractAARHeaders
|
prepareHermes.mustRunAfter prepareThirdPartyNdkHeaders
|
||||||
|
|
||||||
// pre-native build pipeline
|
task prepareJSC {
|
||||||
|
doLast {
|
||||||
|
def jscPackagePath = findNodeModulePath(projectDir, "jsc-android")
|
||||||
|
if (!jscPackagePath) {
|
||||||
|
throw new GradleScriptException("Could not find the jsc-android npm package", null)
|
||||||
|
}
|
||||||
|
|
||||||
tasks.whenTaskAdded { task ->
|
def jscDist = file("$jscPackagePath/dist")
|
||||||
if (!task.name.contains('Clean') && (task.name.contains('externalNative') || task.name.contains('CMake'))) {
|
if (!jscDist.exists()) {
|
||||||
task.dependsOn(extractAARHeaders)
|
throw new GradleScriptException("The jsc-android npm package is missing its \"dist\" directory", null)
|
||||||
task.dependsOn(extractJNIFiles)
|
}
|
||||||
task.dependsOn(prepareJSC)
|
|
||||||
task.dependsOn(prepareHermes)
|
def jscAAR = fileTree(jscDist).matching({ it.include "**/android-jsc/**/*.aar" }).singleFile
|
||||||
task.dependsOn(prepareThirdPartyNdkHeaders)
|
def soFiles = zipTree(jscAAR).matching({ it.include "**/*.so" })
|
||||||
|
|
||||||
|
def headerFiles = fileTree(jscDist).matching({ it.include "**/include/*.h" })
|
||||||
|
|
||||||
|
copy {
|
||||||
|
from(soFiles)
|
||||||
|
from(headerFiles)
|
||||||
|
from("$reactNative/ReactAndroid/src/main/jni/third-party/jsc/Android.mk")
|
||||||
|
|
||||||
|
filesMatching("**/*.h", { it.path = "JavaScriptCore/${it.name}" })
|
||||||
|
|
||||||
|
includeEmptyDirs(false)
|
||||||
|
into("$thirdPartyNdkDir/jsc")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareJSC.mustRunAfter prepareHermes
|
||||||
|
|
||||||
|
task extractAARHeaders {
|
||||||
|
doLast {
|
||||||
|
configurations.extractHeaders.files.each {
|
||||||
|
def file = it.absoluteFile
|
||||||
|
copy {
|
||||||
|
from zipTree(file)
|
||||||
|
into "$buildDir/$file.name"
|
||||||
|
include "**/*.h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractAARHeaders.mustRunAfter prepareJSC
|
||||||
|
|
||||||
|
task extractJNIFiles {
|
||||||
|
doLast {
|
||||||
|
configurations.extractJNI.files.each {
|
||||||
|
def file = it.absoluteFile
|
||||||
|
|
||||||
|
copy {
|
||||||
|
from zipTree(file)
|
||||||
|
into "$buildDir/$file.name"
|
||||||
|
include "jni/**/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractJNIFiles.mustRunAfter extractAARHeaders
|
||||||
|
|
||||||
|
// pre-native build pipeline
|
||||||
|
|
||||||
|
tasks.whenTaskAdded { task ->
|
||||||
|
if (!task.name.contains('Clean') && (task.name.contains('externalNative') || task.name.contains('CMake'))) {
|
||||||
|
task.dependsOn(extractAARHeaders)
|
||||||
|
task.dependsOn(extractJNIFiles)
|
||||||
|
task.dependsOn(prepareJSC)
|
||||||
|
task.dependsOn(prepareHermes)
|
||||||
|
task.dependsOn(prepareThirdPartyNdkHeaders)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,5 +16,6 @@ VisionCamera_compileSdkVersion=31
|
|||||||
VisionCamera_kotlinVersion=1.5.30
|
VisionCamera_kotlinVersion=1.5.30
|
||||||
VisionCamera_targetSdkVersion=31
|
VisionCamera_targetSdkVersion=31
|
||||||
VisionCamera_ndkVersion=21.4.7075529
|
VisionCamera_ndkVersion=21.4.7075529
|
||||||
|
VisionCamera_disableFrameProcessors=false
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
@ -26,6 +26,7 @@ import com.facebook.proguard.annotations.DoNotStrip
|
|||||||
import com.facebook.react.bridge.*
|
import com.facebook.react.bridge.*
|
||||||
import com.facebook.react.uimanager.events.RCTEventEmitter
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
||||||
import com.mrousavy.camera.frameprocessor.FrameProcessorPerformanceDataCollector
|
import com.mrousavy.camera.frameprocessor.FrameProcessorPerformanceDataCollector
|
||||||
|
import com.mrousavy.camera.frameprocessor.FrameProcessorRuntimeManager
|
||||||
import com.mrousavy.camera.utils.*
|
import com.mrousavy.camera.utils.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.guava.await
|
import kotlinx.coroutines.guava.await
|
||||||
@ -159,7 +160,7 @@ class CameraView(context: Context, private val frameProcessorThread: ExecutorSer
|
|||||||
}
|
}
|
||||||
|
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
private var mHybridData: HybridData
|
private var mHybridData: HybridData? = null
|
||||||
|
|
||||||
@Suppress("LiftReturnOrAssignment", "RedundantIf")
|
@Suppress("LiftReturnOrAssignment", "RedundantIf")
|
||||||
internal val fallbackToSnapshot: Boolean
|
internal val fallbackToSnapshot: Boolean
|
||||||
@ -192,7 +193,9 @@ class CameraView(context: Context, private val frameProcessorThread: ExecutorSer
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mHybridData = initHybrid()
|
if (FrameProcessorRuntimeManager.enableFrameProcessors) {
|
||||||
|
mHybridData = initHybrid()
|
||||||
|
}
|
||||||
|
|
||||||
previewView = PreviewView(context)
|
previewView = PreviewView(context)
|
||||||
previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||||
|
@ -57,10 +57,6 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
|
|||||||
if (frameProcessorManager == null) {
|
if (frameProcessorManager == null) {
|
||||||
frameProcessorThread.execute {
|
frameProcessorThread.execute {
|
||||||
frameProcessorManager = FrameProcessorRuntimeManager(reactApplicationContext, frameProcessorThread)
|
frameProcessorManager = FrameProcessorRuntimeManager(reactApplicationContext, frameProcessorThread)
|
||||||
|
|
||||||
reactApplicationContext.runOnJSQueueThread {
|
|
||||||
frameProcessorManager!!.installJSIBindings()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|||||||
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl
|
import com.facebook.react.turbomodule.core.CallInvokerHolderImpl
|
||||||
import com.mrousavy.camera.CameraView
|
import com.mrousavy.camera.CameraView
|
||||||
import com.mrousavy.camera.ViewNotFoundError
|
import com.mrousavy.camera.ViewNotFoundError
|
||||||
import com.swmansion.reanimated.Scheduler
|
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
|
|
||||||
@ -17,39 +16,52 @@ class FrameProcessorRuntimeManager(context: ReactApplicationContext, frameProces
|
|||||||
companion object {
|
companion object {
|
||||||
const val TAG = "FrameProcessorRuntime"
|
const val TAG = "FrameProcessorRuntime"
|
||||||
val Plugins: ArrayList<FrameProcessorPlugin> = ArrayList()
|
val Plugins: ArrayList<FrameProcessorPlugin> = ArrayList()
|
||||||
|
var enableFrameProcessors = true
|
||||||
|
|
||||||
init {
|
init {
|
||||||
System.loadLibrary("reanimated")
|
try {
|
||||||
System.loadLibrary("VisionCamera")
|
System.loadLibrary("reanimated")
|
||||||
|
System.loadLibrary("VisionCamera")
|
||||||
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
|
Log.w(TAG, "Failed to load Reanimated/VisionCamera C++ library. Frame Processors are disabled!")
|
||||||
|
enableFrameProcessors = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
private var mHybridData: HybridData
|
private var mHybridData: HybridData? = null
|
||||||
private var mContext: WeakReference<ReactApplicationContext>
|
private var mContext: WeakReference<ReactApplicationContext>? = null
|
||||||
private var mScheduler: VisionCameraScheduler
|
private var mScheduler: VisionCameraScheduler? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val holder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl
|
if (enableFrameProcessors) {
|
||||||
mScheduler = VisionCameraScheduler(frameProcessorThread)
|
val holder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl
|
||||||
mContext = WeakReference(context)
|
mScheduler = VisionCameraScheduler(frameProcessorThread)
|
||||||
mHybridData = initHybrid(context.javaScriptContextHolder.get(), holder, mScheduler)
|
mContext = WeakReference(context)
|
||||||
initializeRuntime()
|
mHybridData = initHybrid(context.javaScriptContextHolder.get(), holder, mScheduler!!)
|
||||||
|
initializeRuntime()
|
||||||
|
|
||||||
Log.i(TAG, "Installing Frame Processor Plugins...")
|
Log.i(TAG, "Installing Frame Processor Plugins...")
|
||||||
Plugins.forEach { plugin ->
|
Plugins.forEach { plugin ->
|
||||||
registerPlugin(plugin)
|
registerPlugin(plugin)
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Successfully installed ${Plugins.count()} Frame Processor Plugins!")
|
||||||
|
|
||||||
|
Log.i(TAG, "Installing JSI Bindings on JS Thread...")
|
||||||
|
context.runOnJSQueueThread {
|
||||||
|
installJSIBindings()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Successfully installed ${Plugins.count()} Frame Processor Plugins!")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
@Keep
|
@Keep
|
||||||
fun findCameraViewById(viewId: Int): CameraView {
|
fun findCameraViewById(viewId: Int): CameraView {
|
||||||
Log.d(TAG, "finding view $viewId...")
|
Log.d(TAG, "Finding view $viewId...")
|
||||||
val view = mContext.get()?.currentActivity?.findViewById<CameraView>(viewId)
|
val view = mContext?.get()?.currentActivity?.findViewById<CameraView>(viewId)
|
||||||
Log.d(TAG, "found view $viewId! is null: ${view == null}")
|
Log.d(TAG, if (view != null) "Found view $viewId!" else "Couldn't find view $viewId!")
|
||||||
return view ?: throw ViewNotFoundError(viewId)
|
return view ?: throw ViewNotFoundError(viewId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +73,5 @@ class FrameProcessorRuntimeManager(context: ReactApplicationContext, frameProces
|
|||||||
): HybridData
|
): HybridData
|
||||||
private external fun initializeRuntime()
|
private external fun initializeRuntime()
|
||||||
private external fun registerPlugin(plugin: FrameProcessorPlugin)
|
private external fun registerPlugin(plugin: FrameProcessorPlugin)
|
||||||
|
private external fun installJSIBindings()
|
||||||
// public C++ funcs
|
|
||||||
external fun installJSIBindings()
|
|
||||||
}
|
}
|
||||||
|
@ -208,7 +208,21 @@ The Frame Processor gets called with a `Frame` object, which is a **JSI HostObje
|
|||||||
|
|
||||||
### Disabling Frame Processors
|
### Disabling Frame Processors
|
||||||
|
|
||||||
The Frame Processor API spawns a secondary JavaScript Runtime which consumes a small amount of extra CPU and RAM. If you're not using Frame Processors at all, you can disable them by setting the `VISION_CAMERA_DISABLE_FRAME_PROCESSORS` flag. Inside your `project.pbxproj`, find the `GCC_PREPROCESSOR_DEFINITIONS` parameter and add the flag:
|
The Frame Processor API spawns a secondary JavaScript Runtime which consumes a small amount of extra CPU and RAM. Additionally, compile time increases since Frame Processors are written in native C++. If you're not using Frame Processors at all, you can disable them:
|
||||||
|
|
||||||
|
#### Android
|
||||||
|
|
||||||
|
Inside your `gradle.properties` file, add the `disableFrameProcessors` flag:
|
||||||
|
|
||||||
|
```
|
||||||
|
disableFrameProcessors=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, clean and rebuild your project.
|
||||||
|
|
||||||
|
#### iOS
|
||||||
|
|
||||||
|
Inside your `project.pbxproj`, find the `GCC_PREPROCESSOR_DEFINITIONS` group and add the flag:
|
||||||
|
|
||||||
```txt {3}
|
```txt {3}
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
@ -25,7 +25,7 @@ public:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
Frame* frame;
|
Frame* frame;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void assertIsFrameStrong(jsi::Runtime& runtime, const std::string& accessedPropName);
|
void assertIsFrameStrong(jsi::Runtime& runtime, const std::string& accessedPropName);
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
#import <RNReanimated/RuntimeManager.h>
|
#import <RNReanimated/RuntimeManager.h>
|
||||||
#import <RNReanimated/RuntimeDecorator.h>
|
#import <RNReanimated/RuntimeDecorator.h>
|
||||||
#import <RNReanimated/REAIOSErrorHandler.h>
|
#import <RNReanimated/REAIOSErrorHandler.h>
|
||||||
|
#import "VisionCameraScheduler.h"
|
||||||
#define ENABLE_FRAME_PROCESSORS
|
#define ENABLE_FRAME_PROCESSORS
|
||||||
#else
|
#else
|
||||||
#warning Your react-native-reanimated version is not compatible with VisionCamera, Frame Processors are disabled. Make sure you're using reanimated 2.2.0 or above!
|
#warning Your react-native-reanimated version is not compatible with VisionCamera, Frame Processors are disabled. Make sure you're using reanimated 2.2.0 or above!
|
||||||
@ -36,7 +37,6 @@
|
|||||||
|
|
||||||
#import "FrameProcessorUtils.h"
|
#import "FrameProcessorUtils.h"
|
||||||
#import "FrameProcessorCallback.h"
|
#import "FrameProcessorCallback.h"
|
||||||
#import "VisionCameraScheduler.h"
|
|
||||||
#import "../React Utils/MakeJSIRuntime.h"
|
#import "../React Utils/MakeJSIRuntime.h"
|
||||||
#import "../React Utils/JSIUtils.h"
|
#import "../React Utils/JSIUtils.h"
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ __attribute__((objc_runtime_name("_TtC12VisionCamera10CameraView")))
|
|||||||
|
|
||||||
dispatch_async(CameraQueues.frameProcessorQueue, [=]() {
|
dispatch_async(CameraQueues.frameProcessorQueue, [=]() {
|
||||||
NSLog(@"FrameProcessorBindings: Converting worklet to Objective-C callback...");
|
NSLog(@"FrameProcessorBindings: Converting worklet to Objective-C callback...");
|
||||||
|
|
||||||
auto& rt = *runtimeManager->runtime;
|
auto& rt = *runtimeManager->runtime;
|
||||||
auto function = worklet->getValue(rt).asObject(rt).asFunction(rt);
|
auto function = worklet->getValue(rt).asObject(rt).asFunction(rt);
|
||||||
|
|
||||||
|
@ -8,18 +8,30 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#import <RNReanimated/Scheduler.h>
|
#include <functional>
|
||||||
#import <React-callinvoker/ReactCommon/CallInvoker.h>
|
#import <React-callinvoker/ReactCommon/CallInvoker.h>
|
||||||
|
|
||||||
|
#if __has_include(<RNReanimated/RuntimeManager.h>)
|
||||||
|
#import <RNReanimated/Scheduler.h>
|
||||||
|
#else
|
||||||
|
// dummy placeholder
|
||||||
|
namespace reanimated {
|
||||||
|
class Scheduler {
|
||||||
|
public:
|
||||||
|
virtual void scheduleOnUI(std::function<void()> job);
|
||||||
|
protected:
|
||||||
|
std::shared_ptr<facebook::react::CallInvoker> jsCallInvoker_;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace vision {
|
namespace vision {
|
||||||
|
|
||||||
using namespace facebook;
|
|
||||||
|
|
||||||
class VisionCameraScheduler : public reanimated::Scheduler {
|
class VisionCameraScheduler : public reanimated::Scheduler {
|
||||||
public:
|
public:
|
||||||
VisionCameraScheduler(std::shared_ptr<react::CallInvoker> jsInvoker);
|
VisionCameraScheduler(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
|
||||||
virtual ~VisionCameraScheduler();
|
virtual ~VisionCameraScheduler();
|
||||||
|
|
||||||
void scheduleOnUI(std::function<void()> job) override;
|
void scheduleOnUI(std::function<void()> job) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,9 +18,9 @@ __attribute__((objc_runtime_name("_TtC12VisionCamera12CameraQueues")))
|
|||||||
@end
|
@end
|
||||||
|
|
||||||
namespace vision {
|
namespace vision {
|
||||||
|
|
||||||
using namespace facebook;
|
using namespace facebook;
|
||||||
|
|
||||||
VisionCameraScheduler::VisionCameraScheduler(std::shared_ptr<react::CallInvoker> jsInvoker) {
|
VisionCameraScheduler::VisionCameraScheduler(std::shared_ptr<react::CallInvoker> jsInvoker) {
|
||||||
this->jsCallInvoker_ = jsInvoker;
|
this->jsCallInvoker_ = jsInvoker;
|
||||||
}
|
}
|
||||||
@ -34,6 +34,6 @@ void VisionCameraScheduler::scheduleOnUI(std::function<void()> job) {
|
|||||||
|
|
||||||
VisionCameraScheduler::~VisionCameraScheduler(){
|
VisionCameraScheduler::~VisionCameraScheduler(){
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace vision
|
} // namespace vision
|
||||||
|
Loading…
Reference in New Issue
Block a user