Browse Source

Video recording #1

video-recording
Hardcore Sushi 2 months ago
parent
commit
4912f003b0
Signed by: hardcoresushi GPG Key ID: 007F84120107191E
  1. 54
      app/CMakeLists.txt
  2. 6
      app/build.gradle
  3. 2
      app/ffmpeg/.gitignore
  4. 82
      app/ffmpeg/build.sh
  5. 1
      app/src/main/AndroidManifest.xml
  6. 68
      app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt
  7. 80
      app/src/main/java/sushi/hardcore/droidfs/Muxer.kt
  8. 2028
      app/src/main/java/sushi/hardcore/droidfs/VideoCapture.java
  9. 212
      app/src/main/java/sushi/hardcore/droidfs/VideoCaptureConfig.java
  10. 137
      app/src/main/native/libmux.c
  11. 5
      app/src/main/res/drawable/icon_video.xml
  12. 17
      app/src/main/res/drawable/record_video_button.xml
  13. 39
      app/src/main/res/layout/activity_camera.xml
  14. 2
      build.gradle

54
app/CMakeLists.txt

@ -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/${ANDROID_ABI}/libavformat/libavformat.a
)
add_library(
avcodec
STATIC
IMPORTED
)
set_target_properties(
avcodec
PROPERTIES IMPORTED_LOCATION
${PROJECT_SOURCE_DIR}/ffmpeg/${ANDROID_ABI}/libavcodec/libavcodec.a
)
add_library(
avutil
STATIC
IMPORTED
)
set_target_properties(
avutil
PROPERTIES IMPORTED_LOCATION
${PROJECT_SOURCE_DIR}/ffmpeg/${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/${ANDROID_ABI}
)

6
app/build.gradle

@ -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,10 +66,12 @@ 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"
implementation "androidx.concurrent:concurrent-futures:1.1.0"
def camerax_v1 = "1.1.0-alpha08"
implementation "androidx.camera:camera-camera2:$camerax_v1"
implementation "androidx.camera:camera-lifecycle:$camerax_v1"

2
app/ffmpeg/.gitignore

@ -0,0 +1,2 @@
ffmpeg
build

82
app/ffmpeg/build.sh

@ -0,0 +1,82 @@
#!/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-encoders \
--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

1
app/src/main/AndroidManifest.xml

@ -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"

68
app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt

@ -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,29 @@ 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.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)
@ -69,11 +73,14 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
private lateinit var extensionsManager: ExtensionsManager
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?) {
@ -174,10 +181,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 +197,21 @@ 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) {
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 +223,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.imageCameraSwitch)
sensorOrientationListener = SensorOrientationListener(this)
val scaleGestureDetector = ScaleGestureDetector(this, object : ScaleGestureDetector.SimpleOnScaleGestureListener(){
@ -232,6 +251,7 @@ 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) {
@ -266,6 +286,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 +299,15 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
}
.build()
videoCapture = VideoCapture.Builder().build()
var 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())
@ -349,6 +372,24 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
}
}
@SuppressLint("MissingPermission")
private fun onClickRecordVideo() {
isRecording = if (isRecording) {
videoCapture?.stopRecording()
false
} else {
videoCapture?.startRecording(FileOutputStream(filesDir.path+"/v.mp4"), executor, object : VideoCapture.OnVideoSavedCallback {
override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
}
override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
cause?.printStackTrace()
}
})
true
}
}
override fun onDestroy() {
super.onDestroy()
if (!isFinishingIntentionally) {
@ -368,9 +409,12 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
super.onPause()
sensorOrientationListener.remove(this)
if (
(ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) //not asking for permission
&& !usf_keep_open
){
(ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED) //not asking for permission
&& !usf_keep_open
) {
finish()
}
}

80
app/src/main/java/sushi/hardcore/droidfs/Muxer.kt

@ -0,0 +1,80 @@
package sushi.hardcore.droidfs
import android.media.MediaCodec
import android.media.MediaFormat
import java.io.FileOutputStream
import java.nio.ByteBuffer
class Muxer(val file: FileOutputStream) {
external fun allocContext(): Long
external fun addVideoTrack(formatContext: Long, codec: String, bitrate: Int, width: Int, height: Int, orientationHint: Int): Int
external fun addAudioTrack(formatContext: Long, codec: String, 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)
var formatContext: Long?
var firstPts: Long? = null
var orientationHint: Int = 0
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
}
writePacket(
formatContext!!, byteArray, bufferInfo.presentationTimeUs - firstPts!!, trackIndex,
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") {
addAudioTrack(
formatContext!!,
mime[1],
bitrate,
format.getInteger("sample-rate"),
format.getInteger("channel-count")
)
} else {
addVideoTrack(
formatContext!!,
mime[1],
bitrate,
format.getInteger("width"),
format.getInteger("height"),
orientationHint
)
}
}
fun start() {
writeHeaders(formatContext!!)
}
fun stop() {
writeTrailer(formatContext!!)
}
fun release() {
firstPts = null
release(formatContext!!)
formatContext = null
}
fun writePacket(buff: ByteArray) {
file.write(buff)
}
fun seek(offset: Long) {
file.channel.position(offset)
}
}

2028
app/src/main/java/sushi/hardcore/droidfs/VideoCapture.java

File diff suppressed because it is too large

212
app/src/main/java/sushi/hardcore/droidfs/VideoCaptureConfig.java

@ -0,0 +1,212 @@
package sushi.hardcore.droidfs;
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;
}
}

137
app/src/main/native/libmux.c

@ -0,0 +1,137 @@
#include <libavformat/avformat.h>
#include <jni.h>
#define FF_PROFILE_AAC_LOW 1
const size_t BUFF_SIZE = 4096;
struct AVDictionary {
int count;
AVDictionaryEntry *elems;
};
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_Muxer_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_Muxer_addAudioTrack(JNIEnv *env, jobject thiz, jlong format_context,
jstring jcodec,
jint bitrate, jint sample_rate,
jint channel_count) {
AVStream* stream = avformat_new_stream((AVFormatContext *) format_context, NULL);
stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
const char* codec = (*env)->GetStringUTFChars(env, jcodec, NULL);
if (strcmp(codec, "opus") == 0) {
stream->codecpar->codec_id = AV_CODEC_ID_OPUS;
} else if (strcmp(codec, "mp4a-latm") == 0) {
stream->codecpar->codec_id = AV_CODEC_ID_AAC;
stream->codecpar->profile = FF_PROFILE_AAC_LOW;
}
(*env)->ReleaseStringUTFChars(env, jcodec, codec);
stream->codecpar->bit_rate = bitrate;
stream->codecpar->sample_rate = sample_rate;
stream->codecpar->channels = channel_count;
return stream->index;
}
JNIEXPORT jint JNICALL
Java_sushi_hardcore_droidfs_Muxer_addVideoTrack(JNIEnv *env, jobject thiz, jlong format_context,
jstring jcodec, jint bitrate, jint width,
jint height, jint orientation_hint) {
AVStream* stream = avformat_new_stream((AVFormatContext *) format_context, NULL);
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
const char* codec = (*env)->GetStringUTFChars(env, jcodec, NULL);
if (strcmp(codec, "x-vnd.on2.vp8") == 0) {
stream->codecpar->codec_id = AV_CODEC_ID_VP8;
} else if (strcmp(codec, "avc") == 0) {
stream->codecpar->codec_id = AV_CODEC_ID_H264;
}
(*env)->ReleaseStringUTFChars(env, jcodec, codec);
AVDictionary* metadata = av_malloc(sizeof(struct AVDictionary));
metadata->count = 0;
metadata->elems = NULL;//av_malloc(sizeof(struct AVDictionaryEntry));
av_dict_set_int(&metadata, "rotate", orientation_hint, 0);
stream->metadata = metadata;
stream->codecpar->bit_rate = bitrate;
stream->codecpar->width = width;
stream->codecpar->height = height;
return stream->index;
}
JNIEXPORT jint JNICALL
Java_sushi_hardcore_droidfs_Muxer_writeHeaders(JNIEnv *env, jobject thiz, jlong format_context) {
return avformat_write_header((AVFormatContext *) format_context, NULL);
}
JNIEXPORT void JNICALL
Java_sushi_hardcore_droidfs_Muxer_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;
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_Muxer_writeTrailer(JNIEnv *env, jobject thiz, jlong format_context) {
av_write_trailer((AVFormatContext *) format_context);
}
JNIEXPORT void JNICALL
Java_sushi_hardcore_droidfs_Muxer_release(JNIEnv *env, jobject thiz, jlong format_context) {
AVFormatContext* fc = (AVFormatContext *) format_context;
free(fc->pb);
avio_context_free(&fc->pb);
avformat_free_context(fc);
}

5
app/src/main/res/drawable/icon_video.xml

@ -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

@ -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>

39
app/src/main/res/layout/activity_camera.xml

@ -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="75dp"
android:layout_height="75dp"
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"/>

2
build.gradle

@ -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…
Cancel
Save