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(
|
||||
gocryptfs
|
||||
@ -23,6 +23,56 @@ target_link_libraries(
|
||||
gocryptfs
|
||||
)
|
||||
|
||||
include_directories(
|
||||
${PROJECT_SOURCE_DIR}/libgocryptfs/build/${ANDROID_ABI}/
|
||||
add_library(
|
||||
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'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "31"
|
||||
ndkVersion "21.4.7075529"
|
||||
|
||||
compileOptions {
|
||||
@ -57,7 +57,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "androidx.core:core-ktx:1.6.0"
|
||||
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.preference:preference-ktx:1.1.1"
|
||||
@ -66,14 +66,16 @@ dependencies {
|
||||
implementation "com.github.bumptech.glide:glide:4.12.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-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-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-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.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.any"
|
||||
|
@ -1,6 +1,7 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
@ -20,26 +21,31 @@ import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.camera.camera2.interop.Camera2CameraInfo
|
||||
import androidx.camera.core.*
|
||||
//import androidx.camera.core.VideoCapture
|
||||
import androidx.camera.extensions.ExtensionMode
|
||||
import androidx.camera.extensions.ExtensionsManager
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import sushi.hardcore.droidfs.adapters.DialogSingleChoiceAdapter
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
|
||||
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 java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
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 fileNameRandomMax = 999999
|
||||
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 gocryptfsVolume: GocryptfsVolume
|
||||
private lateinit var outputDirectory: String
|
||||
private lateinit var fileName: String
|
||||
private var isFinishingIntentionally = false
|
||||
private var isAskingPermissions = false
|
||||
private var permissionsGranted = false
|
||||
private lateinit var executor: Executor
|
||||
private lateinit var cameraProvider: ProcessCameraProvider
|
||||
private lateinit var extensionsManager: ExtensionsManager
|
||||
private lateinit var cameraSelector: CameraSelector
|
||||
private val cameraPreview = Preview.Builder().build()
|
||||
private var imageCapture: ImageCapture? = null
|
||||
private var videoCapture: VideoCapture? = null
|
||||
private var camera: Camera? = null
|
||||
private var resolutions: List<Size>? = null
|
||||
private var currentResolutionIndex: Int = 0
|
||||
private var captureMode = ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
|
||||
private var isBackCamera = true
|
||||
private var isInVideoMode = false
|
||||
private var isRecording = false
|
||||
private lateinit var binding: ActivityCameraBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -88,6 +98,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED){
|
||||
permissionsGranted = true
|
||||
} else {
|
||||
isAskingPermissions = true
|
||||
requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
} else {
|
||||
@ -174,10 +185,6 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
dialog.show()
|
||||
}
|
||||
binding.imageClose.setOnClickListener {
|
||||
isFinishingIntentionally = true
|
||||
finish()
|
||||
}
|
||||
binding.imageFlash.setOnClickListener {
|
||||
binding.imageFlash.setImageResource(when (imageCapture?.flashMode) {
|
||||
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 {
|
||||
isBackCamera = if (isBackCamera) {
|
||||
binding.imageCameraSwitch.setImageResource(R.drawable.icon_camera_back)
|
||||
@ -205,7 +228,8 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
setupCamera()
|
||||
}
|
||||
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)
|
||||
|
||||
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) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
when (requestCode) {
|
||||
CAMERA_PERMISSION_REQUEST_CODE -> if (grantResults.size == 1) {
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
isAskingPermissions = false
|
||||
if (grantResults.size == 1) {
|
||||
when (requestCode) {
|
||||
CAMERA_PERMISSION_REQUEST_CODE -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
permissionsGranted = true
|
||||
setupCamera()
|
||||
} else {
|
||||
@ -249,6 +275,12 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
finish()
|
||||
}.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)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun setupCamera(resolution: Size? = null){
|
||||
if (permissionsGranted && ::extensionsManager.isInitialized && ::cameraProvider.isInitialized) {
|
||||
imageCapture = ImageCapture.Builder()
|
||||
@ -278,13 +311,19 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
}
|
||||
.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)) {
|
||||
cameraSelector = extensionsManager.getExtensionEnabledCameraSelector(cameraProvider, cameraSelector, ExtensionMode.HDR)
|
||||
}
|
||||
|
||||
cameraProvider.unbindAll()
|
||||
camera = cameraProvider.bindToLifecycle(this, cameraSelector, cameraPreview, imageCapture)
|
||||
camera = cameraProvider.bindToLifecycle(this, cameraSelector, cameraPreview, imageCapture, videoCapture)
|
||||
|
||||
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 outputBuff = ByteArrayOutputStream()
|
||||
val outputOptions = ImageCapture.OutputFileOptions.Builder(outputBuff).build()
|
||||
imageCapture.takePicture(outputOptions, executor, object : ImageCapture.OnImageSavedCallback {
|
||||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||||
binding.takePhotoButton.onPhotoTaken()
|
||||
if (gocryptfsVolume.importFile(ByteArrayInputStream(outputBuff.toByteArray()), PathUtils.pathJoin(outputDirectory, fileName))){
|
||||
Toast.makeText(applicationContext, getString(R.string.picture_save_success, fileName), Toast.LENGTH_SHORT).show()
|
||||
if (gocryptfsVolume.importFile(ByteArrayInputStream(outputBuff.toByteArray()), outputPath)){
|
||||
Toast.makeText(applicationContext, getString(R.string.picture_save_success, outputPath), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
ColoredAlertDialogBuilder(this@CameraActivity)
|
||||
.setTitle(R.string.error)
|
||||
@ -327,11 +366,17 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
})
|
||||
}
|
||||
|
||||
private fun onClickTakePhoto() {
|
||||
val baseName = "IMG_"+dateFormat.format(Date())+"_"
|
||||
private fun getOutputPath(isVideo: Boolean): String {
|
||||
val baseName = if (isVideo) {"VID"} else {"IMG"}+'_'+dateFormat.format(Date())+'_'
|
||||
var fileName: String
|
||||
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))
|
||||
return PathUtils.pathJoin(outputDirectory, fileName)
|
||||
}
|
||||
|
||||
private fun onClickTakePhoto() {
|
||||
val path = getOutputPath(false)
|
||||
if (timerDuration > 0){
|
||||
binding.textTimer.visibility = View.VISIBLE
|
||||
Thread{
|
||||
@ -340,12 +385,47 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
Thread.sleep(1000)
|
||||
}
|
||||
runOnUiThread {
|
||||
takePhoto()
|
||||
takePhoto(path)
|
||||
binding.textTimer.visibility = View.GONE
|
||||
}
|
||||
}.start()
|
||||
} 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() {
|
||||
super.onPause()
|
||||
sensorOrientationListener.remove(this)
|
||||
if (
|
||||
(ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) //not asking for permission
|
||||
&& !usf_keep_open
|
||||
){
|
||||
if (!isAskingPermissions && !usf_keep_open) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder),
|
||||
listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown),
|
||||
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)
|
||||
.setSingleChoiceItems(adapter, -1){ thisDialog, which ->
|
||||
@ -215,7 +215,7 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
"createFolder" -> {
|
||||
openDialogCreateFolder()
|
||||
}
|
||||
"takePhoto" -> {
|
||||
"camera" -> {
|
||||
val intent = Intent(this, CameraActivity::class.java)
|
||||
intent.putExtra("path", currentDirectoryPath)
|
||||
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_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".CameraActivity">
|
||||
tools:context=".CameraActivity"
|
||||
android:background="#000000">
|
||||
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/camera_preview"
|
||||
@ -43,15 +44,15 @@
|
||||
android:layout_height="30dp"
|
||||
android:src="@drawable/icon_timer_off"
|
||||
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_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_close"
|
||||
android:id="@+id/image_flash"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:src="@drawable/icon_close"
|
||||
android:src="@drawable/icon_flash_auto"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/image_timer"
|
||||
@ -74,27 +75,41 @@
|
||||
android:layout_marginBottom="30dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_flash"
|
||||
android:id="@+id/image_mode_switch"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:src="@drawable/icon_flash_auto"
|
||||
android:layout_alignEnd="@id/take_photo_button"
|
||||
android:src="@drawable/icon_video"
|
||||
android:layout_alignEnd="@id/layout_record_buttons"
|
||||
android:layout_marginEnd="120dp"
|
||||
android:layout_centerVertical="true"/>
|
||||
|
||||
<sushi.hardcore.droidfs.widgets.TakePhotoButton
|
||||
android:id="@+id/take_photo_button"
|
||||
<RelativeLayout
|
||||
android:id="@+id/layout_record_buttons"
|
||||
android:layout_width="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
|
||||
android:id="@+id/image_camera_switch"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
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_centerVertical="true"/>
|
||||
|
||||
|
@ -144,7 +144,7 @@
|
||||
<string name="copy_failed">Falha ao copiar o %s .</string>
|
||||
<string name="copy_success">Cópia feita!</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_failed">Falha ao salvar esta imagem.</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_success">Копирование выполнено!</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_failed">Невозможно сохранить изображение.</string>
|
||||
<string name="file_overwrite_question">%s уже существует, переписать его?</string>
|
||||
|
@ -146,9 +146,10 @@
|
||||
<string name="copy_failed">Copy of %s failed.</string>
|
||||
<string name="copy_success">Copy successful !</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_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="dir_overwrite_question">%s already exists, do you want to merge its content ?</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.
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.5.30"
|
||||
ext.kotlin_version = "1.5.31"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
Loading…
Reference in New Issue
Block a user