forked from hardcoresushi/DroidFS
Video recording
This commit is contained in:
parent
fd98c42014
commit
dc89c02b9f
@ -1,4 +1,4 @@
|
|||||||
cmake_minimum_required(VERSION 3.4.1)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
add_library(
|
add_library(
|
||||||
gocryptfs
|
gocryptfs
|
||||||
@ -23,6 +23,56 @@ target_link_libraries(
|
|||||||
gocryptfs
|
gocryptfs
|
||||||
)
|
)
|
||||||
|
|
||||||
include_directories(
|
add_library(
|
||||||
${PROJECT_SOURCE_DIR}/libgocryptfs/build/${ANDROID_ABI}/
|
avformat
|
||||||
|
STATIC
|
||||||
|
IMPORTED
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(
|
||||||
|
avformat
|
||||||
|
PROPERTIES IMPORTED_LOCATION
|
||||||
|
${PROJECT_SOURCE_DIR}/ffmpeg/build/${ANDROID_ABI}/libavformat/libavformat.a
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(
|
||||||
|
avcodec
|
||||||
|
STATIC
|
||||||
|
IMPORTED
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(
|
||||||
|
avcodec
|
||||||
|
PROPERTIES IMPORTED_LOCATION
|
||||||
|
${PROJECT_SOURCE_DIR}/ffmpeg/build/${ANDROID_ABI}/libavcodec/libavcodec.a
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(
|
||||||
|
avutil
|
||||||
|
STATIC
|
||||||
|
IMPORTED
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(
|
||||||
|
avutil
|
||||||
|
PROPERTIES IMPORTED_LOCATION
|
||||||
|
${PROJECT_SOURCE_DIR}/ffmpeg/build/${ANDROID_ABI}/libavutil/libavutil.a
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(
|
||||||
|
mux
|
||||||
|
SHARED
|
||||||
|
src/main/native/libmux.c
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
mux
|
||||||
|
avformat
|
||||||
|
avcodec
|
||||||
|
avutil
|
||||||
|
)
|
||||||
|
|
||||||
|
include_directories(
|
||||||
|
${PROJECT_SOURCE_DIR}/libgocryptfs/build/${ANDROID_ABI}
|
||||||
|
${PROJECT_SOURCE_DIR}/ffmpeg/build/${ANDROID_ABI}
|
||||||
)
|
)
|
@ -2,8 +2,8 @@ apply plugin: 'com.android.application'
|
|||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 31
|
||||||
buildToolsVersion "30.0.3"
|
buildToolsVersion "31"
|
||||||
ndkVersion "21.4.7075529"
|
ndkVersion "21.4.7075529"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@ -57,7 +57,7 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation "androidx.core:core-ktx:1.6.0"
|
implementation "androidx.core:core-ktx:1.6.0"
|
||||||
implementation "androidx.appcompat:appcompat:1.3.1"
|
implementation "androidx.appcompat:appcompat:1.3.1"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.1.0"
|
implementation "androidx.constraintlayout:constraintlayout:2.1.1"
|
||||||
|
|
||||||
implementation "androidx.sqlite:sqlite-ktx:2.1.0"
|
implementation "androidx.sqlite:sqlite-ktx:2.1.0"
|
||||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||||
@ -66,14 +66,16 @@ dependencies {
|
|||||||
implementation "com.github.bumptech.glide:glide:4.12.0"
|
implementation "com.github.bumptech.glide:glide:4.12.0"
|
||||||
implementation "androidx.biometric:biometric:1.1.0"
|
implementation "androidx.biometric:biometric:1.1.0"
|
||||||
|
|
||||||
def exoplayer_version = "2.15.0"
|
def exoplayer_version = "2.15.1"
|
||||||
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
|
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
|
||||||
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
|
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
|
||||||
|
|
||||||
def camerax_v1 = "1.1.0-alpha08"
|
implementation "androidx.concurrent:concurrent-futures:1.1.0"
|
||||||
|
|
||||||
|
def camerax_v1 = "1.1.0-alpha09"
|
||||||
implementation "androidx.camera:camera-camera2:$camerax_v1"
|
implementation "androidx.camera:camera-camera2:$camerax_v1"
|
||||||
implementation "androidx.camera:camera-lifecycle:$camerax_v1"
|
implementation "androidx.camera:camera-lifecycle:$camerax_v1"
|
||||||
def camerax_v2 = "1.0.0-alpha28"
|
def camerax_v2 = "1.0.0-alpha29"
|
||||||
implementation "androidx.camera:camera-view:$camerax_v2"
|
implementation "androidx.camera:camera-view:$camerax_v2"
|
||||||
implementation "androidx.camera:camera-extensions:$camerax_v2"
|
implementation "androidx.camera:camera-extensions:$camerax_v2"
|
||||||
}
|
}
|
||||||
|
2
app/ffmpeg/.gitignore
vendored
Normal file
2
app/ffmpeg/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ffmpeg
|
||||||
|
build
|
81
app/ffmpeg/build.sh
Executable file
81
app/ffmpeg/build.sh
Executable file
@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -z ${ANDROID_NDK_HOME+x} ]; then
|
||||||
|
echo "Error: \$ANDROID_NDK_HOME is not defined."
|
||||||
|
else
|
||||||
|
compile_for_arch() {
|
||||||
|
case $1 in
|
||||||
|
"x86_64")
|
||||||
|
CFN="x86_64-linux-android21-clang"
|
||||||
|
ARCH="x86_64"
|
||||||
|
;;
|
||||||
|
"x86")
|
||||||
|
CFN="i686-linux-android21-clang"
|
||||||
|
ARCH="i686"
|
||||||
|
;;
|
||||||
|
"arm64-v8a")
|
||||||
|
CFN="aarch64-linux-android21-clang"
|
||||||
|
ARCH="aarch64"
|
||||||
|
;;
|
||||||
|
"armeabi-v7a")
|
||||||
|
CFN="armv7a-linux-androideabi19-clang"
|
||||||
|
ARCH="arm"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
cd ffmpeg && make clean &&
|
||||||
|
./configure \
|
||||||
|
--cc="$CFN" \
|
||||||
|
--cxx="$CFN++" \
|
||||||
|
--arch="$ARCH" \
|
||||||
|
--target-os=android \
|
||||||
|
--enable-cross-compile \
|
||||||
|
--enable-version3 \
|
||||||
|
--disable-programs \
|
||||||
|
--disable-bsfs \
|
||||||
|
--disable-parsers \
|
||||||
|
--disable-demuxers \
|
||||||
|
--disable-decoders \
|
||||||
|
--disable-avdevice \
|
||||||
|
--disable-swresample \
|
||||||
|
--disable-swscale \
|
||||||
|
--disable-postproc \
|
||||||
|
--disable-avfilter \
|
||||||
|
--disable-network \
|
||||||
|
--disable-doc \
|
||||||
|
--disable-htmlpages \
|
||||||
|
--disable-manpages \
|
||||||
|
--disable-podpages \
|
||||||
|
--disable-txtpages \
|
||||||
|
--disable-sndio \
|
||||||
|
--disable-schannel \
|
||||||
|
--disable-securetransport \
|
||||||
|
--disable-xlib \
|
||||||
|
--disable-zlib \
|
||||||
|
--disable-cuvid \
|
||||||
|
--disable-nvenc \
|
||||||
|
--disable-vdpau \
|
||||||
|
--disable-videotoolbox \
|
||||||
|
--disable-audiotoolbox \
|
||||||
|
--disable-appkit \
|
||||||
|
--disable-alsa \
|
||||||
|
--disable-debug \
|
||||||
|
>/dev/null &&
|
||||||
|
make -j 8 >/dev/null &&
|
||||||
|
mkdir -p ../build/$1/libavformat ../build/$1/libavcodec ../build/$1/libavutil &&
|
||||||
|
cp libavformat/*.h libavformat/libavformat.a ../build/$1/libavformat &&
|
||||||
|
cp libavcodec/*.h libavcodec/libavcodec.a ../build/$1/libavcodec &&
|
||||||
|
cp libavutil/*.h libavutil/libavutil.a ../build/$1/libavutil &&
|
||||||
|
cd ..
|
||||||
|
}
|
||||||
|
|
||||||
|
export PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH
|
||||||
|
if [ "$#" -eq 1 ]; then
|
||||||
|
compile_for_arch $1
|
||||||
|
else
|
||||||
|
declare -a ABIs=("x86_64" "x86" "arm64-v8a" "armeabi-v7a")
|
||||||
|
for abi in ${ABIs[@]}; do
|
||||||
|
echo "Compiling for $abi..."
|
||||||
|
compile_for_arch $abi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
@ -11,6 +11,7 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.camera.any"
|
android:name="android.hardware.camera.any"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package sushi.hardcore.droidfs
|
package sushi.hardcore.droidfs
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
import android.hardware.camera2.CameraCharacteristics
|
||||||
@ -20,26 +21,31 @@ import android.widget.EditText
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.camera.camera2.interop.Camera2CameraInfo
|
import androidx.camera.camera2.interop.Camera2CameraInfo
|
||||||
import androidx.camera.core.*
|
import androidx.camera.core.*
|
||||||
|
//import androidx.camera.core.VideoCapture
|
||||||
import androidx.camera.extensions.ExtensionMode
|
import androidx.camera.extensions.ExtensionMode
|
||||||
import androidx.camera.extensions.ExtensionsManager
|
import androidx.camera.extensions.ExtensionsManager
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import sushi.hardcore.droidfs.adapters.DialogSingleChoiceAdapter
|
import sushi.hardcore.droidfs.adapters.DialogSingleChoiceAdapter
|
||||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||||
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
|
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
|
||||||
import sushi.hardcore.droidfs.util.PathUtils
|
import sushi.hardcore.droidfs.util.PathUtils
|
||||||
|
import sushi.hardcore.droidfs.video_recording.SeekableWriter
|
||||||
|
import sushi.hardcore.droidfs.video_recording.VideoCapture
|
||||||
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
|
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.*
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||||
companion object {
|
companion object {
|
||||||
private const val CAMERA_PERMISSION_REQUEST_CODE = 1
|
private const val CAMERA_PERMISSION_REQUEST_CODE = 0
|
||||||
|
private const val AUDIO_PERMISSION_REQUEST_CODE = 1
|
||||||
private const val fileNameRandomMin = 100000
|
private const val fileNameRandomMin = 100000
|
||||||
private const val fileNameRandomMax = 999999
|
private const val fileNameRandomMax = 999999
|
||||||
private val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
|
private val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
|
||||||
@ -61,19 +67,23 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
private lateinit var orientedIcons: List<ImageView>
|
private lateinit var orientedIcons: List<ImageView>
|
||||||
private lateinit var gocryptfsVolume: GocryptfsVolume
|
private lateinit var gocryptfsVolume: GocryptfsVolume
|
||||||
private lateinit var outputDirectory: String
|
private lateinit var outputDirectory: String
|
||||||
private lateinit var fileName: String
|
|
||||||
private var isFinishingIntentionally = false
|
private var isFinishingIntentionally = false
|
||||||
|
private var isAskingPermissions = false
|
||||||
private var permissionsGranted = false
|
private var permissionsGranted = false
|
||||||
private lateinit var executor: Executor
|
private lateinit var executor: Executor
|
||||||
private lateinit var cameraProvider: ProcessCameraProvider
|
private lateinit var cameraProvider: ProcessCameraProvider
|
||||||
private lateinit var extensionsManager: ExtensionsManager
|
private lateinit var extensionsManager: ExtensionsManager
|
||||||
|
private lateinit var cameraSelector: CameraSelector
|
||||||
private val cameraPreview = Preview.Builder().build()
|
private val cameraPreview = Preview.Builder().build()
|
||||||
private var imageCapture: ImageCapture? = null
|
private var imageCapture: ImageCapture? = null
|
||||||
|
private var videoCapture: VideoCapture? = null
|
||||||
private var camera: Camera? = null
|
private var camera: Camera? = null
|
||||||
private var resolutions: List<Size>? = null
|
private var resolutions: List<Size>? = null
|
||||||
private var currentResolutionIndex: Int = 0
|
private var currentResolutionIndex: Int = 0
|
||||||
private var captureMode = ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
|
private var captureMode = ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
|
||||||
private var isBackCamera = true
|
private var isBackCamera = true
|
||||||
|
private var isInVideoMode = false
|
||||||
|
private var isRecording = false
|
||||||
private lateinit var binding: ActivityCameraBinding
|
private lateinit var binding: ActivityCameraBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@ -88,6 +98,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED){
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED){
|
||||||
permissionsGranted = true
|
permissionsGranted = true
|
||||||
} else {
|
} else {
|
||||||
|
isAskingPermissions = true
|
||||||
requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
|
requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -174,10 +185,6 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
binding.imageClose.setOnClickListener {
|
|
||||||
isFinishingIntentionally = true
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
binding.imageFlash.setOnClickListener {
|
binding.imageFlash.setOnClickListener {
|
||||||
binding.imageFlash.setImageResource(when (imageCapture?.flashMode) {
|
binding.imageFlash.setImageResource(when (imageCapture?.flashMode) {
|
||||||
ImageCapture.FLASH_MODE_AUTO -> {
|
ImageCapture.FLASH_MODE_AUTO -> {
|
||||||
@ -194,6 +201,22 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
binding.imageModeSwitch.setOnClickListener {
|
||||||
|
isInVideoMode = !isInVideoMode
|
||||||
|
if (isInVideoMode) {
|
||||||
|
binding.recordVideoButton.visibility = View.VISIBLE
|
||||||
|
binding.takePhotoButton.visibility = View.GONE
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
isAskingPermissions = true
|
||||||
|
requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), AUDIO_PERMISSION_REQUEST_CODE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.recordVideoButton.visibility = View.GONE
|
||||||
|
binding.takePhotoButton.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
binding.imageCameraSwitch.setOnClickListener {
|
binding.imageCameraSwitch.setOnClickListener {
|
||||||
isBackCamera = if (isBackCamera) {
|
isBackCamera = if (isBackCamera) {
|
||||||
binding.imageCameraSwitch.setImageResource(R.drawable.icon_camera_back)
|
binding.imageCameraSwitch.setImageResource(R.drawable.icon_camera_back)
|
||||||
@ -205,7 +228,8 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
setupCamera()
|
setupCamera()
|
||||||
}
|
}
|
||||||
binding.takePhotoButton.onClick = ::onClickTakePhoto
|
binding.takePhotoButton.onClick = ::onClickTakePhoto
|
||||||
orientedIcons = listOf(binding.imageRatio, binding.imageTimer, binding.imageClose, binding.imageFlash, binding.imageCameraSwitch)
|
binding.recordVideoButton.setOnClickListener { onClickRecordVideo() }
|
||||||
|
orientedIcons = listOf(binding.imageRatio, binding.imageTimer, binding.imageCaptureMode, binding.imageFlash, binding.imageModeSwitch, binding.imageCameraSwitch)
|
||||||
sensorOrientationListener = SensorOrientationListener(this)
|
sensorOrientationListener = SensorOrientationListener(this)
|
||||||
|
|
||||||
val scaleGestureDetector = ScaleGestureDetector(this, object : ScaleGestureDetector.SimpleOnScaleGestureListener(){
|
val scaleGestureDetector = ScaleGestureDetector(this, object : ScaleGestureDetector.SimpleOnScaleGestureListener(){
|
||||||
@ -232,11 +256,13 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
when (requestCode) {
|
isAskingPermissions = false
|
||||||
CAMERA_PERMISSION_REQUEST_CODE -> if (grantResults.size == 1) {
|
if (grantResults.size == 1) {
|
||||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
when (requestCode) {
|
||||||
|
CAMERA_PERMISSION_REQUEST_CODE -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
permissionsGranted = true
|
permissionsGranted = true
|
||||||
setupCamera()
|
setupCamera()
|
||||||
} else {
|
} else {
|
||||||
@ -249,6 +275,12 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
finish()
|
finish()
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
AUDIO_PERMISSION_REQUEST_CODE -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
if (videoCapture != null) {
|
||||||
|
cameraProvider.unbind(videoCapture)
|
||||||
|
camera = cameraProvider.bindToLifecycle(this, cameraSelector, videoCapture)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,6 +298,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
(binding.cameraPreview.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.CENTER_IN_PARENT)
|
(binding.cameraPreview.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.CENTER_IN_PARENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
private fun setupCamera(resolution: Size? = null){
|
private fun setupCamera(resolution: Size? = null){
|
||||||
if (permissionsGranted && ::extensionsManager.isInitialized && ::cameraProvider.isInitialized) {
|
if (permissionsGranted && ::extensionsManager.isInitialized && ::cameraProvider.isInitialized) {
|
||||||
imageCapture = ImageCapture.Builder()
|
imageCapture = ImageCapture.Builder()
|
||||||
@ -278,13 +311,19 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
var cameraSelector = if (isBackCamera){ CameraSelector.DEFAULT_BACK_CAMERA } else { CameraSelector.DEFAULT_FRONT_CAMERA }
|
videoCapture = VideoCapture.Builder().apply {
|
||||||
|
resolution?.let {
|
||||||
|
setTargetResolution(it)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
cameraSelector = if (isBackCamera){ CameraSelector.DEFAULT_BACK_CAMERA } else { CameraSelector.DEFAULT_FRONT_CAMERA }
|
||||||
if (extensionsManager.isExtensionAvailable(cameraProvider, cameraSelector, ExtensionMode.HDR)) {
|
if (extensionsManager.isExtensionAvailable(cameraProvider, cameraSelector, ExtensionMode.HDR)) {
|
||||||
cameraSelector = extensionsManager.getExtensionEnabledCameraSelector(cameraProvider, cameraSelector, ExtensionMode.HDR)
|
cameraSelector = extensionsManager.getExtensionEnabledCameraSelector(cameraProvider, cameraSelector, ExtensionMode.HDR)
|
||||||
}
|
}
|
||||||
|
|
||||||
cameraProvider.unbindAll()
|
cameraProvider.unbindAll()
|
||||||
camera = cameraProvider.bindToLifecycle(this, cameraSelector, cameraPreview, imageCapture)
|
camera = cameraProvider.bindToLifecycle(this, cameraSelector, cameraPreview, imageCapture, videoCapture)
|
||||||
|
|
||||||
adaptPreviewSize(resolution ?: imageCapture!!.attachedSurfaceResolution!!.swap())
|
adaptPreviewSize(resolution ?: imageCapture!!.attachedSurfaceResolution!!.swap())
|
||||||
|
|
||||||
@ -299,15 +338,15 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun takePhoto() {
|
private fun takePhoto(outputPath: String) {
|
||||||
val imageCapture = imageCapture ?: return
|
val imageCapture = imageCapture ?: return
|
||||||
val outputBuff = ByteArrayOutputStream()
|
val outputBuff = ByteArrayOutputStream()
|
||||||
val outputOptions = ImageCapture.OutputFileOptions.Builder(outputBuff).build()
|
val outputOptions = ImageCapture.OutputFileOptions.Builder(outputBuff).build()
|
||||||
imageCapture.takePicture(outputOptions, executor, object : ImageCapture.OnImageSavedCallback {
|
imageCapture.takePicture(outputOptions, executor, object : ImageCapture.OnImageSavedCallback {
|
||||||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||||||
binding.takePhotoButton.onPhotoTaken()
|
binding.takePhotoButton.onPhotoTaken()
|
||||||
if (gocryptfsVolume.importFile(ByteArrayInputStream(outputBuff.toByteArray()), PathUtils.pathJoin(outputDirectory, fileName))){
|
if (gocryptfsVolume.importFile(ByteArrayInputStream(outputBuff.toByteArray()), outputPath)){
|
||||||
Toast.makeText(applicationContext, getString(R.string.picture_save_success, fileName), Toast.LENGTH_SHORT).show()
|
Toast.makeText(applicationContext, getString(R.string.picture_save_success, outputPath), Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
ColoredAlertDialogBuilder(this@CameraActivity)
|
ColoredAlertDialogBuilder(this@CameraActivity)
|
||||||
.setTitle(R.string.error)
|
.setTitle(R.string.error)
|
||||||
@ -327,11 +366,17 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onClickTakePhoto() {
|
private fun getOutputPath(isVideo: Boolean): String {
|
||||||
val baseName = "IMG_"+dateFormat.format(Date())+"_"
|
val baseName = if (isVideo) {"VID"} else {"IMG"}+'_'+dateFormat.format(Date())+'_'
|
||||||
|
var fileName: String
|
||||||
do {
|
do {
|
||||||
fileName = baseName+(random.nextInt(fileNameRandomMax-fileNameRandomMin)+fileNameRandomMin)+".jpg"
|
fileName = baseName+(random.nextInt(fileNameRandomMax-fileNameRandomMin)+fileNameRandomMin)+'.'+ if (isVideo) {"mp4"} else {"jpg"}
|
||||||
} while (gocryptfsVolume.pathExists(fileName))
|
} while (gocryptfsVolume.pathExists(fileName))
|
||||||
|
return PathUtils.pathJoin(outputDirectory, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onClickTakePhoto() {
|
||||||
|
val path = getOutputPath(false)
|
||||||
if (timerDuration > 0){
|
if (timerDuration > 0){
|
||||||
binding.textTimer.visibility = View.VISIBLE
|
binding.textTimer.visibility = View.VISIBLE
|
||||||
Thread{
|
Thread{
|
||||||
@ -340,12 +385,47 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
Thread.sleep(1000)
|
Thread.sleep(1000)
|
||||||
}
|
}
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
takePhoto()
|
takePhoto(path)
|
||||||
binding.textTimer.visibility = View.GONE
|
binding.textTimer.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}.start()
|
}.start()
|
||||||
} else {
|
} else {
|
||||||
takePhoto()
|
takePhoto(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun onClickRecordVideo() {
|
||||||
|
isRecording = if (isRecording) {
|
||||||
|
videoCapture?.stopRecording()
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val path = getOutputPath(true)
|
||||||
|
val handleId = gocryptfsVolume.openWriteMode(path)
|
||||||
|
videoCapture?.startRecording(VideoCapture.OutputFileOptions(object : SeekableWriter {
|
||||||
|
var offset = 0L
|
||||||
|
override fun write(byteArray: ByteArray) {
|
||||||
|
offset += gocryptfsVolume.writeFile(handleId, offset, byteArray, byteArray.size)
|
||||||
|
}
|
||||||
|
override fun seek(offset: Long) {
|
||||||
|
this.offset = offset
|
||||||
|
}
|
||||||
|
override fun close() {
|
||||||
|
gocryptfsVolume.closeFile(handleId)
|
||||||
|
}
|
||||||
|
}), executor, object : VideoCapture.OnVideoSavedCallback {
|
||||||
|
override fun onVideoSaved() {
|
||||||
|
Toast.makeText(applicationContext, getString(R.string.video_save_success, path), Toast.LENGTH_SHORT).show()
|
||||||
|
binding.recordVideoButton.setImageResource(R.drawable.record_video_button)
|
||||||
|
}
|
||||||
|
override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
|
||||||
|
Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show()
|
||||||
|
cause?.printStackTrace()
|
||||||
|
binding.recordVideoButton.setImageResource(R.drawable.record_video_button)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
binding.recordVideoButton.setImageResource(R.drawable.stop_recording_video_button)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,10 +447,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
sensorOrientationListener.remove(this)
|
sensorOrientationListener.remove(this)
|
||||||
if (
|
if (!isAskingPermissions && !usf_keep_open) {
|
||||||
(ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) //not asking for permission
|
|
||||||
&& !usf_keep_open
|
|
||||||
){
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,7 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder),
|
listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder),
|
||||||
listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown),
|
listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown),
|
||||||
listOf("createFolder", R.string.mkdir, R.drawable.icon_create_new_folder),
|
listOf("createFolder", R.string.mkdir, R.drawable.icon_create_new_folder),
|
||||||
listOf("takePhoto", R.string.take_photo, R.drawable.icon_camera)
|
listOf("camera", R.string.camera, R.drawable.icon_camera)
|
||||||
)
|
)
|
||||||
ColoredAlertDialogBuilder(this)
|
ColoredAlertDialogBuilder(this)
|
||||||
.setSingleChoiceItems(adapter, -1){ thisDialog, which ->
|
.setSingleChoiceItems(adapter, -1){ thisDialog, which ->
|
||||||
@ -215,7 +215,7 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
"createFolder" -> {
|
"createFolder" -> {
|
||||||
openDialogCreateFolder()
|
openDialogCreateFolder()
|
||||||
}
|
}
|
||||||
"takePhoto" -> {
|
"camera" -> {
|
||||||
val intent = Intent(this, CameraActivity::class.java)
|
val intent = Intent(this, CameraActivity::class.java)
|
||||||
intent.putExtra("path", currentDirectoryPath)
|
intent.putExtra("path", currentDirectoryPath)
|
||||||
intent.putExtra("sessionID", gocryptfsVolume.sessionID)
|
intent.putExtra("sessionID", gocryptfsVolume.sessionID)
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
package sushi.hardcore.droidfs.video_recording
|
||||||
|
|
||||||
|
import android.media.MediaCodec
|
||||||
|
import android.media.MediaFormat
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
class MediaMuxer(val writer: SeekableWriter) {
|
||||||
|
external fun allocContext(): Long
|
||||||
|
external fun addVideoTrack(formatContext: Long, bitrate: Int, width: Int, height: Int): Int
|
||||||
|
external fun addAudioTrack(formatContext: Long, bitrate: Int, sampleRate: Int, channelCount: Int): Int
|
||||||
|
external fun writeHeaders(formatContext: Long): Int
|
||||||
|
external fun writePacket(formatContext: Long, buffer: ByteArray, pts: Long, streamIndex: Int, isKeyFrame: Boolean)
|
||||||
|
external fun writeTrailer(formatContext: Long)
|
||||||
|
external fun release(formatContext: Long)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val VIDEO_TRACK_INDEX = 0
|
||||||
|
const val AUDIO_TRACK_INDEX = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatContext: Long?
|
||||||
|
|
||||||
|
var realVideoTrackIndex: Int? = null
|
||||||
|
var audioFrameSize: Int? = null
|
||||||
|
var firstPts: Long? = null
|
||||||
|
private var audioPts = 0L
|
||||||
|
|
||||||
|
init {
|
||||||
|
System.loadLibrary("mux")
|
||||||
|
formatContext = allocContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeSampleData(trackIndex: Int, buffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
|
||||||
|
val byteArray = ByteArray(bufferInfo.size)
|
||||||
|
buffer.get(byteArray)
|
||||||
|
if (firstPts == null) {
|
||||||
|
firstPts = bufferInfo.presentationTimeUs
|
||||||
|
}
|
||||||
|
if (trackIndex == AUDIO_TRACK_INDEX) {
|
||||||
|
writePacket(formatContext!!, byteArray, audioPts, -1, false)
|
||||||
|
audioPts += audioFrameSize!!
|
||||||
|
} else {
|
||||||
|
writePacket(
|
||||||
|
formatContext!!, byteArray, bufferInfo.presentationTimeUs - firstPts!!, realVideoTrackIndex!!,
|
||||||
|
bufferInfo.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME != 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTrack(format: MediaFormat): Int {
|
||||||
|
val mime = format.getString("mime")!!.split('/')
|
||||||
|
val bitrate = format.getInteger("bitrate")
|
||||||
|
return if (mime[0] == "audio") {
|
||||||
|
audioFrameSize = addAudioTrack(
|
||||||
|
formatContext!!,
|
||||||
|
bitrate,
|
||||||
|
format.getInteger("sample-rate"),
|
||||||
|
format.getInteger("channel-count")
|
||||||
|
)
|
||||||
|
AUDIO_TRACK_INDEX
|
||||||
|
} else {
|
||||||
|
realVideoTrackIndex = addVideoTrack(
|
||||||
|
formatContext!!,
|
||||||
|
bitrate,
|
||||||
|
format.getInteger("width"),
|
||||||
|
format.getInteger("height")
|
||||||
|
)
|
||||||
|
VIDEO_TRACK_INDEX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
writeHeaders(formatContext!!)
|
||||||
|
}
|
||||||
|
fun stop() {
|
||||||
|
writeTrailer(formatContext!!)
|
||||||
|
}
|
||||||
|
fun release() {
|
||||||
|
writer.close()
|
||||||
|
release(formatContext!!)
|
||||||
|
firstPts = null
|
||||||
|
audioPts = 0
|
||||||
|
formatContext = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writePacket(buff: ByteArray) {
|
||||||
|
writer.write(buff)
|
||||||
|
}
|
||||||
|
fun seek(offset: Long) {
|
||||||
|
writer.seek(offset)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package sushi.hardcore.droidfs.video_recording
|
||||||
|
|
||||||
|
interface SeekableWriter {
|
||||||
|
fun write(byteArray: ByteArray)
|
||||||
|
fun seek(offset: Long)
|
||||||
|
fun close()
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,212 @@
|
|||||||
|
package sushi.hardcore.droidfs.video_recording;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.camera.core.impl.Config;
|
||||||
|
import androidx.camera.core.impl.ImageFormatConstants;
|
||||||
|
import androidx.camera.core.impl.ImageOutputConfig;
|
||||||
|
import androidx.camera.core.impl.OptionsBundle;
|
||||||
|
import androidx.camera.core.impl.UseCaseConfig;
|
||||||
|
import androidx.camera.core.internal.ThreadConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config for a video capture use case.
|
||||||
|
*
|
||||||
|
* <p>In the earlier stage, the VideoCapture is deprioritized.
|
||||||
|
*/
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
public final class VideoCaptureConfig
|
||||||
|
implements UseCaseConfig<VideoCapture>,
|
||||||
|
ImageOutputConfig,
|
||||||
|
ThreadConfig {
|
||||||
|
|
||||||
|
// Option Declarations:
|
||||||
|
// *********************************************************************************************
|
||||||
|
|
||||||
|
public static final Option<Integer> OPTION_VIDEO_FRAME_RATE =
|
||||||
|
Option.create("camerax.core.videoCapture.recordingFrameRate", int.class);
|
||||||
|
public static final Option<Integer> OPTION_BIT_RATE =
|
||||||
|
Option.create("camerax.core.videoCapture.bitRate", int.class);
|
||||||
|
public static final Option<Integer> OPTION_INTRA_FRAME_INTERVAL =
|
||||||
|
Option.create("camerax.core.videoCapture.intraFrameInterval", int.class);
|
||||||
|
public static final Option<Integer> OPTION_AUDIO_BIT_RATE =
|
||||||
|
Option.create("camerax.core.videoCapture.audioBitRate", int.class);
|
||||||
|
public static final Option<Integer> OPTION_AUDIO_SAMPLE_RATE =
|
||||||
|
Option.create("camerax.core.videoCapture.audioSampleRate", int.class);
|
||||||
|
public static final Option<Integer> OPTION_AUDIO_CHANNEL_COUNT =
|
||||||
|
Option.create("camerax.core.videoCapture.audioChannelCount", int.class);
|
||||||
|
public static final Option<Integer> OPTION_AUDIO_MIN_BUFFER_SIZE =
|
||||||
|
Option.create("camerax.core.videoCapture.audioMinBufferSize", int.class);
|
||||||
|
|
||||||
|
// *********************************************************************************************
|
||||||
|
|
||||||
|
private final OptionsBundle mConfig;
|
||||||
|
|
||||||
|
public VideoCaptureConfig(@NonNull OptionsBundle config) {
|
||||||
|
mConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the recording frames per second.
|
||||||
|
*
|
||||||
|
* @param valueIfMissing The value to return if this configuration option has not been set.
|
||||||
|
* @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
|
||||||
|
* configuration.
|
||||||
|
*/
|
||||||
|
public int getVideoFrameRate(int valueIfMissing) {
|
||||||
|
return retrieveOption(OPTION_VIDEO_FRAME_RATE, valueIfMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the recording frames per second.
|
||||||
|
*
|
||||||
|
* @return The stored value, if it exists in this configuration.
|
||||||
|
* @throws IllegalArgumentException if the option does not exist in this configuration.
|
||||||
|
*/
|
||||||
|
public int getVideoFrameRate() {
|
||||||
|
return retrieveOption(OPTION_VIDEO_FRAME_RATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the encoding bit rate.
|
||||||
|
*
|
||||||
|
* @param valueIfMissing The value to return if this configuration option has not been set.
|
||||||
|
* @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
|
||||||
|
* configuration.
|
||||||
|
*/
|
||||||
|
public int getBitRate(int valueIfMissing) {
|
||||||
|
return retrieveOption(OPTION_BIT_RATE, valueIfMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the encoding bit rate.
|
||||||
|
*
|
||||||
|
* @return The stored value, if it exists in this configuration.
|
||||||
|
* @throws IllegalArgumentException if the option does not exist in this configuration.
|
||||||
|
*/
|
||||||
|
public int getBitRate() {
|
||||||
|
return retrieveOption(OPTION_BIT_RATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of seconds between each key frame.
|
||||||
|
*
|
||||||
|
* @param valueIfMissing The value to return if this configuration option has not been set.
|
||||||
|
* @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
|
||||||
|
* configuration.
|
||||||
|
*/
|
||||||
|
public int getIFrameInterval(int valueIfMissing) {
|
||||||
|
return retrieveOption(OPTION_INTRA_FRAME_INTERVAL, valueIfMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of seconds between each key frame.
|
||||||
|
*
|
||||||
|
* @return The stored value, if it exists in this configuration.
|
||||||
|
* @throws IllegalArgumentException if the option does not exist in this configuration.
|
||||||
|
*/
|
||||||
|
public int getIFrameInterval() {
|
||||||
|
return retrieveOption(OPTION_INTRA_FRAME_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the audio encoding bit rate.
|
||||||
|
*
|
||||||
|
* @param valueIfMissing The value to return if this configuration option has not been set.
|
||||||
|
* @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
|
||||||
|
* configuration.
|
||||||
|
*/
|
||||||
|
public int getAudioBitRate(int valueIfMissing) {
|
||||||
|
return retrieveOption(OPTION_AUDIO_BIT_RATE, valueIfMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the audio encoding bit rate.
|
||||||
|
*
|
||||||
|
* @return The stored value, if it exists in this configuration.
|
||||||
|
* @throws IllegalArgumentException if the option does not exist in this configuration.
|
||||||
|
*/
|
||||||
|
public int getAudioBitRate() {
|
||||||
|
return retrieveOption(OPTION_AUDIO_BIT_RATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the audio sample rate.
|
||||||
|
*
|
||||||
|
* @param valueIfMissing The value to return if this configuration option has not been set.
|
||||||
|
* @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
|
||||||
|
* configuration.
|
||||||
|
*/
|
||||||
|
public int getAudioSampleRate(int valueIfMissing) {
|
||||||
|
return retrieveOption(OPTION_AUDIO_SAMPLE_RATE, valueIfMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the audio sample rate.
|
||||||
|
*
|
||||||
|
* @return The stored value, if it exists in this configuration.
|
||||||
|
* @throws IllegalArgumentException if the option does not exist in this configuration.
|
||||||
|
*/
|
||||||
|
public int getAudioSampleRate() {
|
||||||
|
return retrieveOption(OPTION_AUDIO_SAMPLE_RATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the audio channel count.
|
||||||
|
*
|
||||||
|
* @param valueIfMissing The value to return if this configuration option has not been set.
|
||||||
|
* @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
|
||||||
|
* configuration.
|
||||||
|
*/
|
||||||
|
public int getAudioChannelCount(int valueIfMissing) {
|
||||||
|
return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT, valueIfMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the audio channel count.
|
||||||
|
*
|
||||||
|
* @return The stored value, if it exists in this configuration.
|
||||||
|
* @throws IllegalArgumentException if the option does not exist in this configuration.
|
||||||
|
*/
|
||||||
|
public int getAudioChannelCount() {
|
||||||
|
return retrieveOption(OPTION_AUDIO_CHANNEL_COUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the audio minimum buffer size, in bytes.
|
||||||
|
*
|
||||||
|
* @param valueIfMissing The value to return if this configuration option has not been set.
|
||||||
|
* @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
|
||||||
|
* configuration.
|
||||||
|
*/
|
||||||
|
public int getAudioMinBufferSize(int valueIfMissing) {
|
||||||
|
return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE, valueIfMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the audio minimum buffer size, in bytes.
|
||||||
|
*
|
||||||
|
* @return The stored value, if it exists in this configuration.
|
||||||
|
* @throws IllegalArgumentException if the option does not exist in this configuration.
|
||||||
|
*/
|
||||||
|
public int getAudioMinBufferSize() {
|
||||||
|
return retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the format of the image that is fed as input.
|
||||||
|
*
|
||||||
|
* <p>This should always be PRIVATE for VideoCapture.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getInputFormat() {
|
||||||
|
return ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Config getConfig() {
|
||||||
|
return mConfig;
|
||||||
|
}
|
||||||
|
}
|
125
app/src/main/native/libmux.c
Normal file
125
app/src/main/native/libmux.c
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
#include <libavformat/avformat.h>
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
#include <libavutil/channel_layout.h>
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
const size_t BUFF_SIZE = 4096;
|
||||||
|
|
||||||
|
struct Muxer {
|
||||||
|
JavaVM* jvm;
|
||||||
|
jobject thiz;
|
||||||
|
jmethodID write_packet_method_id;
|
||||||
|
jmethodID seek_method_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
int write_packet(void* opaque, uint8_t* buff, int buff_size) {
|
||||||
|
struct Muxer* muxer = opaque;
|
||||||
|
JNIEnv *env;
|
||||||
|
(*muxer->jvm)->GetEnv(muxer->jvm, (void **) &env, JNI_VERSION_1_6);
|
||||||
|
jbyteArray jarray = (*env)->NewByteArray(env, buff_size);
|
||||||
|
(*env)->SetByteArrayRegion(env, jarray, 0, buff_size, buff);
|
||||||
|
(*env)->CallVoidMethod(env, muxer->thiz, muxer->write_packet_method_id, jarray, buff_size);
|
||||||
|
return buff_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t seek(void* opaque, int64_t offset, int whence) {
|
||||||
|
struct Muxer* muxer = opaque;
|
||||||
|
JNIEnv *env;
|
||||||
|
(*muxer->jvm)->GetEnv(muxer->jvm, (void **) &env, JNI_VERSION_1_6);
|
||||||
|
(*env)->CallVoidMethod(env, muxer->thiz, muxer->seek_method_id, offset);
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
jlong Java_sushi_hardcore_droidfs_video_1recording_MediaMuxer_allocContext(JNIEnv *env, jobject thiz) {
|
||||||
|
const AVOutputFormat *format = av_guess_format("mp4", NULL, NULL);
|
||||||
|
struct Muxer* muxer = malloc(sizeof(struct Muxer));
|
||||||
|
(*env)->GetJavaVM(env, &muxer->jvm);
|
||||||
|
muxer->thiz = (*env)->NewGlobalRef(env, thiz);
|
||||||
|
jclass class = (*env)->GetObjectClass(env, thiz);
|
||||||
|
muxer->write_packet_method_id = (*env)->GetMethodID(env, class, "writePacket", "([B)V");
|
||||||
|
muxer->seek_method_id = (*env)->GetMethodID(env, class, "seek", "(J)V");
|
||||||
|
AVIOContext* avio_context = avio_alloc_context(av_malloc(BUFF_SIZE), BUFF_SIZE, 1, muxer, NULL, write_packet, seek);
|
||||||
|
AVFormatContext* fc = avformat_alloc_context();
|
||||||
|
fc->oformat = format;
|
||||||
|
fc->pb = avio_context;
|
||||||
|
return (jlong) fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_sushi_hardcore_droidfs_video_1recording_MediaMuxer_addAudioTrack(JNIEnv *env, jobject thiz, jlong format_context, jint bitrate, jint sample_rate,
|
||||||
|
jint channel_count) {
|
||||||
|
const AVCodec* encoder = avcodec_find_encoder(AV_CODEC_ID_AAC);
|
||||||
|
AVStream* stream = avformat_new_stream((AVFormatContext *) format_context, NULL);
|
||||||
|
AVCodecContext* codec_context = avcodec_alloc_context3(encoder);
|
||||||
|
codec_context->channels = channel_count;
|
||||||
|
codec_context->channel_layout = av_get_default_channel_layout(channel_count);
|
||||||
|
codec_context->sample_rate = sample_rate;
|
||||||
|
codec_context->sample_fmt = encoder->sample_fmts[0];
|
||||||
|
codec_context->bit_rate = bitrate;
|
||||||
|
codec_context->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
|
||||||
|
stream->time_base.den = sample_rate;
|
||||||
|
stream->time_base.num = 1;
|
||||||
|
codec_context->flags = AV_CODEC_FLAG_GLOBAL_HEADER;
|
||||||
|
avcodec_open2(codec_context, encoder, NULL);
|
||||||
|
avcodec_parameters_from_context(stream->codecpar, codec_context);
|
||||||
|
int frame_size = codec_context->frame_size;
|
||||||
|
avcodec_free_context(&codec_context);
|
||||||
|
return frame_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_sushi_hardcore_droidfs_video_1recording_MediaMuxer_addVideoTrack(JNIEnv *env, jobject thiz, jlong format_context, jint bitrate, jint width,
|
||||||
|
jint height) {
|
||||||
|
AVStream* stream = avformat_new_stream((AVFormatContext *) format_context, NULL);
|
||||||
|
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||||
|
stream->codecpar->codec_id = AV_CODEC_ID_H264;
|
||||||
|
stream->codecpar->bit_rate = bitrate;
|
||||||
|
stream->codecpar->width = width;
|
||||||
|
stream->codecpar->height = height;
|
||||||
|
return stream->index;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_sushi_hardcore_droidfs_video_1recording_MediaMuxer_writeHeaders(JNIEnv *env, jobject thiz, jlong format_context) {
|
||||||
|
return avformat_write_header((AVFormatContext *) format_context, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_sushi_hardcore_droidfs_video_1recording_MediaMuxer_writePacket(JNIEnv *env, jobject thiz, jlong format_context,
|
||||||
|
jbyteArray buffer, jlong pts, jint stream_index,
|
||||||
|
jboolean is_key_frame) {
|
||||||
|
AVPacket* packet = av_packet_alloc();
|
||||||
|
int size = (*env)->GetArrayLength(env, buffer);
|
||||||
|
av_new_packet(packet, size);
|
||||||
|
packet->pts = pts;
|
||||||
|
if (stream_index >= 0) { //video
|
||||||
|
packet->stream_index = stream_index;
|
||||||
|
AVRational r;
|
||||||
|
r.num = 1;
|
||||||
|
r.den = 1000000;
|
||||||
|
av_packet_rescale_ts(packet, r, ((AVFormatContext *)format_context)->streams[stream_index]->time_base);
|
||||||
|
}
|
||||||
|
unsigned char* buff = malloc(size);
|
||||||
|
(*env)->GetByteArrayRegion(env, buffer, 0, size, buff);
|
||||||
|
packet->data = buff;
|
||||||
|
if (is_key_frame) {
|
||||||
|
packet->flags = AV_PKT_FLAG_KEY;
|
||||||
|
}
|
||||||
|
av_write_frame((AVFormatContext *)format_context, packet);
|
||||||
|
free(buff);
|
||||||
|
av_packet_free(&packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_sushi_hardcore_droidfs_video_1recording_MediaMuxer_writeTrailer(JNIEnv *env, jobject thiz, jlong format_context) {
|
||||||
|
av_write_trailer((AVFormatContext *) format_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_sushi_hardcore_droidfs_video_1recording_MediaMuxer_release(JNIEnv *env, jobject thiz, jlong format_context) {
|
||||||
|
AVFormatContext* fc = (AVFormatContext *) format_context;
|
||||||
|
av_free(fc->pb->buffer);
|
||||||
|
free(fc->pb->opaque);
|
||||||
|
avio_context_free(&fc->pb);
|
||||||
|
avformat_free_context(fc);
|
||||||
|
}
|
5
app/src/main/res/drawable/icon_video.xml
Normal file
5
app/src/main/res/drawable/icon_video.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z"/>
|
||||||
|
</vector>
|
17
app/src/main/res/drawable/record_video_button.xml
Normal file
17
app/src/main/res/drawable/record_video_button.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="#ff0000" />
|
||||||
|
<size android:width="75dp" android:height="75dp" />
|
||||||
|
<stroke android:width="15dp" android:color="#444444"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="#ff0000"/>
|
||||||
|
<size android:width="75dp" android:height="75dp" />
|
||||||
|
<stroke android:width="8dp" android:color="#444444"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
16
app/src/main/res/drawable/stop_recording_video_button.xml
Normal file
16
app/src/main/res/drawable/stop_recording_video_button.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#ff0000"/>
|
||||||
|
<size android:width="5dp" android:height="5dp" />
|
||||||
|
<stroke android:color="#00000000" android:width="45dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<size android:width="75dp" android:height="75dp" />
|
||||||
|
<stroke android:width="8dp" android:color="#444444"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
@ -4,7 +4,8 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
tools:context=".CameraActivity">
|
tools:context=".CameraActivity"
|
||||||
|
android:background="#000000">
|
||||||
|
|
||||||
<androidx.camera.view.PreviewView
|
<androidx.camera.view.PreviewView
|
||||||
android:id="@+id/camera_preview"
|
android:id="@+id/camera_preview"
|
||||||
@ -43,15 +44,15 @@
|
|||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:src="@drawable/icon_timer_off"
|
android:src="@drawable/icon_timer_off"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/image_close"
|
app:layout_constraintEnd_toStartOf="@id/image_flash"
|
||||||
app:layout_constraintStart_toEndOf="@id/image_ratio"
|
app:layout_constraintStart_toEndOf="@id/image_ratio"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_close"
|
android:id="@+id/image_flash"
|
||||||
android:layout_width="30dp"
|
android:layout_width="30dp"
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:src="@drawable/icon_close"
|
android:src="@drawable/icon_flash_auto"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/image_timer"
|
app:layout_constraintStart_toEndOf="@id/image_timer"
|
||||||
@ -74,27 +75,41 @@
|
|||||||
android:layout_marginBottom="30dp">
|
android:layout_marginBottom="30dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_flash"
|
android:id="@+id/image_mode_switch"
|
||||||
android:layout_width="30dp"
|
android:layout_width="30dp"
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:src="@drawable/icon_flash_auto"
|
android:src="@drawable/icon_video"
|
||||||
android:layout_alignEnd="@id/take_photo_button"
|
android:layout_alignEnd="@id/layout_record_buttons"
|
||||||
android:layout_marginEnd="120dp"
|
android:layout_marginEnd="120dp"
|
||||||
android:layout_centerVertical="true"/>
|
android:layout_centerVertical="true"/>
|
||||||
|
|
||||||
<sushi.hardcore.droidfs.widgets.TakePhotoButton
|
<RelativeLayout
|
||||||
android:id="@+id/take_photo_button"
|
android:id="@+id/layout_record_buttons"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/take_photo_button"
|
android:layout_centerInParent="true">
|
||||||
android:layout_centerInParent="true"/>
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/record_video_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/record_video_button"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<sushi.hardcore.droidfs.widgets.TakePhotoButton
|
||||||
|
android:id="@+id/take_photo_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/take_photo_button"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_camera_switch"
|
android:id="@+id/image_camera_switch"
|
||||||
android:layout_width="30dp"
|
android:layout_width="30dp"
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:src="@drawable/icon_camera_front"
|
android:src="@drawable/icon_camera_front"
|
||||||
android:layout_alignStart="@id/take_photo_button"
|
android:layout_alignStart="@id/layout_record_buttons"
|
||||||
android:layout_marginStart="120dp"
|
android:layout_marginStart="120dp"
|
||||||
android:layout_centerVertical="true"/>
|
android:layout_centerVertical="true"/>
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@
|
|||||||
<string name="copy_failed">Falha ao copiar o %s .</string>
|
<string name="copy_failed">Falha ao copiar o %s .</string>
|
||||||
<string name="copy_success">Cópia feita!</string>
|
<string name="copy_success">Cópia feita!</string>
|
||||||
<string name="fab_dialog_title">Adicionar</string>
|
<string name="fab_dialog_title">Adicionar</string>
|
||||||
<string name="take_photo">Tirar foto</string>
|
<string name="camera">Tirar foto</string>
|
||||||
<string name="picture_save_success">Foto salva ao %s</string>
|
<string name="picture_save_success">Foto salva ao %s</string>
|
||||||
<string name="picture_save_failed">Falha ao salvar esta imagem.</string>
|
<string name="picture_save_failed">Falha ao salvar esta imagem.</string>
|
||||||
<string name="file_overwrite_question">%s já existe, você quer substitui-lo ?</string>
|
<string name="file_overwrite_question">%s já existe, você quer substitui-lo ?</string>
|
||||||
|
@ -141,7 +141,7 @@
|
|||||||
<string name="copy_failed">Ошибка при копировании %s.</string>
|
<string name="copy_failed">Ошибка при копировании %s.</string>
|
||||||
<string name="copy_success">Копирование выполнено!</string>
|
<string name="copy_success">Копирование выполнено!</string>
|
||||||
<string name="fab_dialog_title">Добавить</string>
|
<string name="fab_dialog_title">Добавить</string>
|
||||||
<string name="take_photo">Сфотографировать</string>
|
<string name="camera">Сфотографировать</string>
|
||||||
<string name="picture_save_success">Изображение сохранено в %s</string>
|
<string name="picture_save_success">Изображение сохранено в %s</string>
|
||||||
<string name="picture_save_failed">Невозможно сохранить изображение.</string>
|
<string name="picture_save_failed">Невозможно сохранить изображение.</string>
|
||||||
<string name="file_overwrite_question">%s уже существует, переписать его?</string>
|
<string name="file_overwrite_question">%s уже существует, переписать его?</string>
|
||||||
|
@ -146,9 +146,10 @@
|
|||||||
<string name="copy_failed">Copy of %s failed.</string>
|
<string name="copy_failed">Copy of %s failed.</string>
|
||||||
<string name="copy_success">Copy successful !</string>
|
<string name="copy_success">Copy successful !</string>
|
||||||
<string name="fab_dialog_title">Add</string>
|
<string name="fab_dialog_title">Add</string>
|
||||||
<string name="take_photo">Take photo</string>
|
<string name="camera">Camera</string>
|
||||||
<string name="picture_save_success">Picture saved to %s</string>
|
<string name="picture_save_success">Picture saved to %s</string>
|
||||||
<string name="picture_save_failed">Failed to save this picture.</string>
|
<string name="picture_save_failed">Failed to save this picture.</string>
|
||||||
|
<string name="video_save_success">Video saved to %s</string>
|
||||||
<string name="file_overwrite_question">%s already exists, do you want to overwrite it ?</string>
|
<string name="file_overwrite_question">%s already exists, do you want to overwrite it ?</string>
|
||||||
<string name="dir_overwrite_question">%s already exists, do you want to merge its content ?</string>
|
<string name="dir_overwrite_question">%s already exists, do you want to merge its content ?</string>
|
||||||
<string name="enter_new_name">Enter new name</string>
|
<string name="enter_new_name">Enter new name</string>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = "1.5.30"
|
ext.kotlin_version = "1.5.31"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
Loading…
Reference in New Issue
Block a user