diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..6bf4635
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..c393b28
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index db7ceb8..a3e7efc 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -70,6 +70,10 @@ dependencies {
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
+ implementation 'org.mp4parser:isoparser:1.9.39'
+ implementation 'org.mp4parser:muxer:1.9.39'
+ compile 'org.slf4j:slf4j-nop:1.7.25'
+ implementation 'org.jcodec:jcodec:0.2.5'
implementation 'io.sentry:sentry-android:4.3.0'
implementation 'androidx.preference:preference:1.1.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7df7ca4..515ff8d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,9 @@
package="com.fpvout.digiview">
+
+
+
diff --git a/app/src/main/java/com/fpvout/digiview/H264Extractor.java b/app/src/main/java/com/fpvout/digiview/H264Extractor.java
index 2131349..13fbbd2 100644
--- a/app/src/main/java/com/fpvout/digiview/H264Extractor.java
+++ b/app/src/main/java/com/fpvout/digiview/H264Extractor.java
@@ -3,6 +3,7 @@
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
diff --git a/app/src/main/java/com/fpvout/digiview/MainActivity.java b/app/src/main/java/com/fpvout/digiview/MainActivity.java
index 7e6bcfb..d45f336 100644
--- a/app/src/main/java/com/fpvout/digiview/MainActivity.java
+++ b/app/src/main/java/com/fpvout/digiview/MainActivity.java
@@ -1,5 +1,6 @@
package com.fpvout.digiview;
+import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.LayoutTransition;
@@ -9,10 +10,17 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
+import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
+import android.os.StrictMode;
+import android.preference.PreferenceManager;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
@@ -21,13 +29,21 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
+import android.widget.ImageButton;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.preference.PreferenceManager;
+import androidx.core.app.ActivityCompat;
+import com.fpvout.digiview.dvr.DVR;
+
+import java.io.File;
+import java.io.IOException;
import java.util.HashMap;
import io.sentry.SentryLevel;
@@ -43,6 +59,9 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener
private int shortAnimationDuration;
private float buttonAlpha = 1;
private View settingsButton;
+ private View recordButton;
+ private ImageButton thumbnail;
+ private RelativeLayout toolbar;
private View watermarkView;
private OverlayView overlayView;
PendingIntent permissionIntent;
@@ -53,10 +72,12 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener
VideoReaderExoplayer mVideoReader;
boolean usbConnected = false;
SurfaceView fpvView;
+ DVR dvr;
private GestureDetector gestureDetector;
private ScaleGestureDetector scaleGestureDetector;
private SharedPreferences sharedPreferences;
private static final String ShowWatermark = "ShowWatermark";
+ private boolean overlayIsShown = false;
ActivityResultLauncher launchDataCollectionActivity = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
@@ -64,7 +85,6 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener
boolean dataCollectionAccepted = preferences.getBoolean("dataCollectionAccepted", false);
if (result.getResultCode() == Activity.RESULT_OK && dataCollectionAccepted) {
- Log.d(TAG, "launchDataCollectionActivity: " + dataCollectionAccepted);
SentryAndroid.init(getApplicationContext(), options -> options.setBeforeSend((event, hint) -> {
if (SentryLevel.DEBUG.equals(event.getLevel()))
return null;
@@ -78,7 +98,7 @@ private void setupGestureDetectors() {
gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
- toggleSettingsButton();
+ toggleToolbar();
return super.onSingleTapConfirmed(e);
}
@@ -131,40 +151,44 @@ private void updateVideoZoom() {
}
private void cancelButtonAnimation() {
- Handler handler = settingsButton.getHandler();
+ Handler handler = toolbar.getHandler();
if (handler != null) {
- settingsButton.getHandler().removeCallbacksAndMessages(null);
+ toolbar.getHandler().removeCallbacksAndMessages(null);
}
}
- private void showSettingsButton() {
+ private void showToolbar() {
cancelButtonAnimation();
if (overlayView.getVisibility() == View.VISIBLE) {
buttonAlpha = 1;
- settingsButton.setAlpha(1);
+ toolbar.setAlpha(1);
}
}
- private void toggleSettingsButton() {
+ private void toggleToolbar() {
if (buttonAlpha == 1 && overlayView.getVisibility() == View.VISIBLE) return;
// cancel any pending delayed animations first
cancelButtonAnimation();
+ int translation = 0;
if (buttonAlpha == 1) {
buttonAlpha = 0;
+ translation = 60;
} else {
buttonAlpha = 1;
}
-
- settingsButton.animate()
+ updateDVRThumb();
+ toolbar.animate()
.alpha(buttonAlpha)
+ .translationX(translation)
.setDuration(shortAnimationDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- autoHideSettingsButton();
+ autoHideToolbar();
+ updateDVRThumb();
}
});
}
@@ -191,6 +215,37 @@ protected void onCreate(Bundle savedInstanceState) {
actionBar.hide();
}
+ thumbnail = findViewById(R.id.thumbnail);
+ thumbnail.setOnClickListener(view -> {
+ StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
+ StrictMode.setVmPolicy(builder.build());
+ Intent intent = new Intent();
+ intent.setAction(android.content.Intent.ACTION_VIEW);
+ if (dvr != null) {
+ intent.setDataAndType(Uri.withAppendedPath(Uri.fromFile(dvr.getDefaultFolder()), ""), "video/*");
+ } else {
+ intent.setType("image/*");
+ }
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ });
+ toolbar = findViewById(R.id.toolbar);
+
+ recordButton = findViewById(R.id.recordbt);
+ recordButton.setOnClickListener(view -> {
+ if (dvr != null) {
+ updateDVRThumb();
+ if (dvr.isRecording()) {
+ dvr.stop();
+ } else {
+ dvr.start();
+ }
+ } else {
+ Toast.makeText(this, this.getText(R.string.no_dvr_video), Toast.LENGTH_LONG).show();
+ }
+ });
+
+
// Prevent screen from sleeping
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -226,6 +281,12 @@ protected void onCreate(Bundle savedInstanceState) {
mVideoReader = new VideoReaderExoplayer(fpvView, this, videoReaderEventListener);
+ dvr = DVR.getInstance(this, true, new Handler(message -> {
+ updateDVRThumb();
+ return true;
+ }), mUsbMaskConnection);
+ updateDVRThumb();
+
if (!usbConnected) {
if (searchDevice()) {
connect();
@@ -250,6 +311,18 @@ public void usbDeviceDetached() {
disconnect();
}
+ private void updateDVRThumb() {
+ if (dvr != null) {
+ File file = new File(dvr.getLatestThumbFile());
+ if (file.exists()) {
+ Bitmap bmp = BitmapFactory.decodeFile(dvr.getLatestThumbFile());
+ thumbnail.setImageBitmap(bmp);
+ } else {
+ thumbnail.setImageBitmap(null);
+ }
+ }
+ }
+
private boolean searchDevice() {
HashMap deviceList = usbManager.getDeviceList();
if (deviceList.size() <= 0) {
@@ -275,12 +348,13 @@ private boolean searchDevice() {
private void connect() {
usbConnected = true;
- mUsbMaskConnection.setUsbDevice(usbManager.openDevice(usbDevice), usbDevice);
+ mUsbMaskConnection.setUsbDevice(usbManager, usbDevice, dvr);
mVideoReader.setUsbMaskConnection(mUsbMaskConnection);
overlayView.hide();
mVideoReader.start();
+ updateDVRThumb();
updateWatermark();
- autoHideSettingsButton();
+ autoHideToolbar();
showOverlay(R.string.waiting_for_video, OverlayStatus.Connected);
}
@@ -301,19 +375,66 @@ public void onResume() {
actionBar.hide();
}
+
+ toolbar.setAlpha(1);
+ autoHideToolbar();
+ updateWatermark();
+ updateVideoZoom();
+
+ if(checkStoragePermission()) {
+ finishStartup();
+ }
+ }
+
+ private void finishStartup(){
+ // Init DVR recorder
+ try {
+ dvr.init();
+ } catch (IOException e) {
+ Log.i(TAG, "DVR - init failed");
+ }
if (!usbConnected) {
- if (searchDevice()) {
+ usbDevice = UsbMaskConnection.searchDevice(usbManager, getApplicationContext());
+ if (usbDevice != null) {
Log.d(TAG, "APP - On Resume usbDevice device found");
+ showOverlay(R.string.usb_device_found, OverlayStatus.Connected);
connect();
} else {
- showOverlay(R.string.waiting_for_usb_device, OverlayStatus.Connected);
+ showOverlay(R.string.waiting_for_usb_device, OverlayStatus.Disconnected);
}
}
+ }
- settingsButton.setAlpha(1);
- autoHideSettingsButton();
- updateWatermark();
- updateVideoZoom();
+ private boolean checkStoragePermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED &&
+ checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED &&
+ checkSelfPermission(Manifest.permission.RECORD_AUDIO)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+
+ }else{
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA }, 1);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if(requestCode == 1){
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ finishStartup();
+ }
+ else {
+ overlayView.show( R.string.storage_rights_required, OverlayStatus.Error);
+ }
+ }
}
private boolean onVideoReaderEvent(VideoReaderExoplayer.VideoReaderEventMessageCode m) {
@@ -329,15 +450,19 @@ private boolean onVideoReaderEvent(VideoReaderExoplayer.VideoReaderEventMessageC
private void showOverlay(int textId, OverlayStatus connected) {
overlayView.show(textId, connected);
+ overlayIsShown = true;
+ toolbar.setTranslationX(0);
+ toolbar.setAlpha(1);
updateWatermark();
- showSettingsButton();
+ showToolbar();
}
private void hideOverlay() {
overlayView.hide();
+ overlayIsShown = false;
updateWatermark();
- showSettingsButton();
- autoHideSettingsButton();
+ showToolbar();
+ autoHideToolbar();
}
private void disconnect() {
@@ -373,14 +498,15 @@ protected void onDestroy() {
usbConnected = false;
}
- private void autoHideSettingsButton() {
+ private void autoHideToolbar() {
if (overlayView.getVisibility() == View.VISIBLE) return;
if (buttonAlpha == 0) return;
- settingsButton.postDelayed(() -> {
+ toolbar.postDelayed(() -> {
buttonAlpha = 0;
- settingsButton.animate()
+ toolbar.animate()
.alpha(0)
+ .translationX(60)
.setDuration(shortAnimationDuration);
}, 3000);
}
@@ -400,6 +526,7 @@ private void checkDataCollectionAgreement() {
return event;
}));
}
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fpvout/digiview/OverlayView.java b/app/src/main/java/com/fpvout/digiview/OverlayView.java
index b1327c8..8a6c736 100644
--- a/app/src/main/java/com/fpvout/digiview/OverlayView.java
+++ b/app/src/main/java/com/fpvout/digiview/OverlayView.java
@@ -38,6 +38,8 @@ private void showInfo(String text, OverlayStatus status){
int image = R.drawable.ic_goggles_white;
switch(status){
+ case Connected:
+ break;
case Disconnected:
image = R.drawable.ic_goggles_disconnected_white;
break;
diff --git a/app/src/main/java/com/fpvout/digiview/PerformancePreset.java b/app/src/main/java/com/fpvout/digiview/PerformancePreset.java
index 61e6bda..b8cd4a7 100644
--- a/app/src/main/java/com/fpvout/digiview/PerformancePreset.java
+++ b/app/src/main/java/com/fpvout/digiview/PerformancePreset.java
@@ -70,8 +70,8 @@ public enum PresetType {
LEGACY_BUFFERED
}
- @NonNull
@Override
+ @NonNull
public String toString() {
return "PerformancePreset{" +
"h264ReaderMaxSyncFrameSize=" + h264ReaderMaxSyncFrameSize +
diff --git a/app/src/main/java/com/fpvout/digiview/UsbDeviceBroadcastReceiver.java b/app/src/main/java/com/fpvout/digiview/UsbDeviceBroadcastReceiver.java
index db64b09..e2eebc9 100644
--- a/app/src/main/java/com/fpvout/digiview/UsbDeviceBroadcastReceiver.java
+++ b/app/src/main/java/com/fpvout/digiview/UsbDeviceBroadcastReceiver.java
@@ -6,11 +6,12 @@
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
+import static com.fpvout.digiview.UsbMaskConnection.ACTION_USB_PERMISSION;
+
public class UsbDeviceBroadcastReceiver extends BroadcastReceiver {
- private static final String ACTION_USB_PERMISSION = "com.fpvout.digiview.USB_PERMISSION";
private final UsbDeviceListener listener;
- public UsbDeviceBroadcastReceiver(UsbDeviceListener listener ){
+ public UsbDeviceBroadcastReceiver(UsbDeviceListener listener) {
this.listener = listener;
}
diff --git a/app/src/main/java/com/fpvout/digiview/UsbMaskConnection.java b/app/src/main/java/com/fpvout/digiview/UsbMaskConnection.java
index 1d6d90e..5985c1b 100644
--- a/app/src/main/java/com/fpvout/digiview/UsbMaskConnection.java
+++ b/app/src/main/java/com/fpvout/digiview/UsbMaskConnection.java
@@ -1,37 +1,59 @@
package com.fpvout.digiview;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+
+import com.fpvout.digiview.dvr.DVR;
import java.io.IOException;
+import java.util.HashMap;
import usb.AndroidUSBInputStream;
import usb.AndroidUSBOutputStream;
public class UsbMaskConnection {
+ public static final String ACTION_USB_PERMISSION = "com.fpvout.digiview.USB_PERMISSION";
+ private static final int VENDOR_ID = 11427;
+ private static final int PRODUCT_ID = 31;
private final byte[] magicPacket = "RMVT".getBytes();
private UsbDeviceConnection usbConnection;
- private UsbDevice device;
private UsbInterface usbInterface;
AndroidUSBInputStream mInputStream;
AndroidUSBOutputStream mOutputStream;
private boolean ready = false;
+ private DVR dvr;
public UsbMaskConnection() {
+
}
- public void setUsbDevice(UsbDeviceConnection c, UsbDevice d) {
- usbConnection = c;
- device = d;
- usbInterface = device.getInterface(3);
+ public AndroidUSBInputStream getInputStream(){
+ return mInputStream;
+ }
- usbConnection.claimInterface(usbInterface,true);
+ public static UsbDevice searchDevice(UsbManager usbManager, Context c) {
+ PendingIntent permissionIntent = PendingIntent.getBroadcast(c, 0, new Intent(ACTION_USB_PERMISSION), 0);
- mOutputStream = new AndroidUSBOutputStream(usbInterface.getEndpoint(0), usbConnection);
- mInputStream = new AndroidUSBInputStream(usbInterface.getEndpoint(1), usbInterface.getEndpoint(0), usbConnection);
- ready = true;
+ HashMap deviceList = usbManager.getDeviceList();
+ if (deviceList.size() <= 0) {
+ return null;
+ }
+
+ for (UsbDevice device : deviceList.values()) {
+ if (device.getVendorId() == VENDOR_ID && device.getProductId() == PRODUCT_ID) {
+ if (usbManager.hasPermission(device)) {
+ return device;
+ }
+ usbManager.requestPermission(device, permissionIntent);
+ }
+ }
+ return null;
}
public void start(){
@@ -59,4 +81,16 @@ public void stop() {
public boolean isReady() {
return ready;
}
+
+ public void setUsbDevice(UsbManager usbManager, UsbDevice d, DVR _dvr) {
+ dvr = _dvr;
+ usbConnection = usbManager.openDevice(d);
+ usbInterface = d.getInterface(3);
+
+ usbConnection.claimInterface(usbInterface, true);
+
+ mOutputStream = new AndroidUSBOutputStream(usbInterface.getEndpoint(0), usbConnection);
+ mInputStream = new AndroidUSBInputStream(usbInterface.getEndpoint(1), usbInterface.getEndpoint(0), usbConnection);
+ ready = true;
+ }
}
diff --git a/app/src/main/java/com/fpvout/digiview/VideoReaderExoplayer.java b/app/src/main/java/com/fpvout/digiview/VideoReaderExoplayer.java
index 85a89e5..7877af5 100644
--- a/app/src/main/java/com/fpvout/digiview/VideoReaderExoplayer.java
+++ b/app/src/main/java/com/fpvout/digiview/VideoReaderExoplayer.java
@@ -28,7 +28,6 @@
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
-import com.google.android.exoplayer2.util.NonNullApi;
import com.google.android.exoplayer2.video.VideoSize;
import usb.AndroidUSBInputStream;
@@ -40,12 +39,13 @@ public class VideoReaderExoplayer {
static final String VideoPreset = "VideoPreset";
private final SurfaceView surfaceView;
private AndroidUSBInputStream inputStream;
- private UsbMaskConnection mUsbMaskConnection;
+ private UsbMaskConnection mUsbMaskConnection;
private boolean zoomedIn;
private final Context context;
private PerformancePreset performancePreset = PerformancePreset.getPreset(PerformancePreset.PresetType.DEFAULT);
static final String VideoZoomedIn = "VideoZoomedIn";
private final SharedPreferences sharedPreferences;
+ private boolean streaming = false;
VideoReaderExoplayer(SurfaceView videoSurface, Context c) {
surfaceView = videoSurface;
@@ -95,7 +95,6 @@ public void start() {
mPlayer.play();
mPlayer.addListener(new Player.Listener() {
@Override
- @NonNullApi
public void onPlayerErrorChanged(@Nullable PlaybackException error) {
if (error == null) {
Log.e(TAG, "PLAYER_SOURCE - TYPE_UNEXPECTED: no message");
@@ -105,6 +104,7 @@ public void onPlayerErrorChanged(@Nullable PlaybackException error) {
Log.e(TAG, "onPlayerErrorChanged: " + e.type);
switch (e.type) {
case ExoPlaybackException.TYPE_SOURCE:
+ streaming = false;
Log.e(TAG, "PLAYER_SOURCE - TYPE_SOURCE: " + error.getMessage());
(new Handler(Looper.getMainLooper())).postDelayed(() -> restart(), 1000);
break;
@@ -121,7 +121,7 @@ public void onPlayerErrorChanged(@Nullable PlaybackException error) {
}
@Override
- public void onPlaybackStateChanged(@NonNullApi int state) {
+ public void onPlaybackStateChanged(@NonNull int state) {
switch (state) {
case Player.STATE_IDLE:
case Player.STATE_READY:
diff --git a/app/src/main/java/com/fpvout/digiview/dvr/DVR.java b/app/src/main/java/com/fpvout/digiview/dvr/DVR.java
new file mode 100644
index 0000000..7317d9d
--- /dev/null
+++ b/app/src/main/java/com/fpvout/digiview/dvr/DVR.java
@@ -0,0 +1,165 @@
+package com.fpvout.digiview.dvr;
+
+import android.app.Activity;
+import android.media.MediaRecorder;
+import android.os.Environment;
+import android.os.Handler;
+import android.util.Log;
+import android.widget.ImageButton;
+import android.widget.Toast;
+
+import com.fpvout.digiview.R;
+import com.fpvout.digiview.UsbMaskConnection;
+import com.fpvout.digiview.helpers.DataListener;
+import com.fpvout.digiview.helpers.Mp4Muxer;
+import com.fpvout.digiview.helpers.StreamDumper;
+import com.fpvout.digiview.helpers.ThreadPerTaskExecutor;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+
+public class DVR {
+ private final Activity activity;
+ private final boolean recordAmbientAudio;
+ private MediaRecorder recorder;
+ private boolean recording = false;
+ private static DVR instance;
+ private static final String DVR_LOG_TAG = "DVR";
+ private String defaultFolder = "";
+ private final StreamDumper streamDumper;
+ private String videoFile;
+ private String dvrFile;
+ private String fileName;
+ private final UsbMaskConnection connection;
+ private String ambientAudio;
+ private static Handler updateAfterRecord;
+ public static final String LATEST_THUMB_FILE = "latest.jpeg";
+
+ DVR(Activity activity, boolean recordAmbientAudio, Handler updateAfterRecord, UsbMaskConnection connection) {
+ this.activity = activity;
+ this.connection = connection;
+ this.recordAmbientAudio = recordAmbientAudio;
+ DVR.updateAfterRecord = updateAfterRecord;
+ defaultFolder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/" + this.activity.getApplicationInfo().loadLabel(this.activity.getPackageManager()).toString();
+ streamDumper = new StreamDumper(activity, defaultFolder);
+ }
+
+ public static DVR getInstance(Activity context, boolean recordAmbientAudio, Handler updateAfterRecord, UsbMaskConnection connection){
+ if (instance == null) {
+ instance = new DVR(context, recordAmbientAudio, updateAfterRecord, connection);
+ }
+ return instance;
+ }
+
+ public void init() throws IOException {
+ repairNotFinishedDVR();
+ recorder = new MediaRecorder();
+ recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+ recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
+ recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
+ }
+
+ private void repairNotFinishedDVR(){
+ File dvrFolder = new File(defaultFolder);
+ if (dvrFolder.exists()) {
+ File[] files = dvrFolder.listFiles();
+ for (int i = 0; i < files.length; ++i) {
+ File file = files[i];
+ if (file.getAbsolutePath().endsWith(".h264")) {
+ File ambientAudio = new File(file.getAbsolutePath().replace(".h264", ".aac"));
+ if (ambientAudio.exists()) {
+ File output = new File(file.getAbsolutePath().replace(".h264", ".mp4"));
+ new Mp4Muxer(activity, dvrFolder , file, ambientAudio,output, false).start();
+ }
+ }
+ }
+ }
+ }
+
+ public void start() {
+ if (connection.getInputStream() != null) {
+ Toast.makeText(activity, activity.getText(R.string.recording_started), Toast.LENGTH_LONG).show();
+ ((ImageButton) activity.findViewById(R.id.recordbt)).setImageResource(R.drawable.stop);
+
+ ThreadPerTaskExecutor executor = new ThreadPerTaskExecutor();
+ executor.execute(() -> {
+ connection.getInputStream().setInputStreamListener(new DataListener() {
+ @Override
+ public void calllback(byte[] buffer, int offset, int length) {
+ if (streamDumper != null) {
+ if (isRecording()) {
+ if (buffer != null) {
+ streamDumper.dump(buffer, offset, length);
+ }
+ }
+ }
+ }
+ });
+
+ fileName = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss")
+ .format(Calendar.getInstance().getTime());
+ ambientAudio = "/DigiView_" + fileName + ".aac";
+ videoFile = "/DigiView_" + fileName + ".h264";
+ dvrFile = "/DigiView_" + fileName + ".mp4";
+
+ Log.d(DVR_LOG_TAG, "creating folder for dvr saving ...");
+ File objFolder = new File(defaultFolder);
+ if (!objFolder.exists())
+ objFolder.mkdir();
+
+ Log.d(DVR_LOG_TAG, "start recording ...");
+ streamDumper.init(videoFile, ambientAudio, dvrFile);
+ if (recordAmbientAudio) {
+ Log.d(DVR_LOG_TAG, "starting ambient recording ...");
+ recorder.setOutputFile(defaultFolder + ambientAudio);
+ try {
+ recorder.prepare();
+ recorder.start(); // Ambient Audio Recording is now started
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ //start recording (input stream starts collecting data
+ this.recording = true;
+ });
+ } else {
+ Toast.makeText(activity, "Stream not ready", Toast.LENGTH_LONG).show();
+ }
+ }
+
+ public String getLatestThumbFile() {
+ return defaultFolder + "/" + LATEST_THUMB_FILE;
+ }
+
+ public boolean isRecording(){
+ return recording;
+ }
+
+ public void stop() {
+ Log.d(DVR_LOG_TAG, "stop recording ...");
+ this.recording = false;
+ Toast.makeText(activity, activity.getText(R.string.recording_stopped), Toast.LENGTH_LONG).show();
+ ((ImageButton) activity.findViewById(R.id.recordbt)).setImageResource(R.drawable.record);
+
+ ThreadPerTaskExecutor executor = new ThreadPerTaskExecutor();
+ executor.execute(() -> {
+ connection.getInputStream().setInputStreamListener(null); //remove listener from raw
+ streamDumper.stop(updateAfterRecord);
+ if (recordAmbientAudio) {
+ recorder.stop();
+ }
+
+ try {
+ init();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ public File getDefaultFolder() {
+ return new File(defaultFolder);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/fpvout/digiview/helpers/DataListener.java b/app/src/main/java/com/fpvout/digiview/helpers/DataListener.java
new file mode 100644
index 0000000..84f8336
--- /dev/null
+++ b/app/src/main/java/com/fpvout/digiview/helpers/DataListener.java
@@ -0,0 +1,5 @@
+package com.fpvout.digiview.helpers;
+
+public interface DataListener{
+ public void calllback(byte[] buffer, int offset, int length);
+}
diff --git a/app/src/main/java/com/fpvout/digiview/helpers/Mp4Muxer.java b/app/src/main/java/com/fpvout/digiview/helpers/Mp4Muxer.java
new file mode 100644
index 0000000..8b2f5c9
--- /dev/null
+++ b/app/src/main/java/com/fpvout/digiview/helpers/Mp4Muxer.java
@@ -0,0 +1,171 @@
+package com.fpvout.digiview.helpers;
+
+
+import org.jcodec.codecs.h264.BufferH264ES;
+import org.jcodec.codecs.h264.H264Decoder;
+import org.jcodec.common.Codec;
+import org.jcodec.common.MuxerTrack;
+import org.jcodec.common.VideoCodecMeta;
+import org.jcodec.common.io.NIOUtils;
+import org.jcodec.common.io.SeekableByteChannel;
+import org.jcodec.common.model.Packet;
+import org.jcodec.containers.mp4.muxer.MP4Muxer;
+import org.mp4parser.Container;
+import org.mp4parser.muxer.FileDataSourceImpl;
+import org.mp4parser.muxer.Movie;
+import org.mp4parser.muxer.builder.DefaultMp4Builder;
+import org.mp4parser.muxer.container.mp4.MovieCreator;
+import org.mp4parser.muxer.tracks.AACTrackImpl;
+import org.mp4parser.muxer.tracks.ClippedTrack;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.media.MediaScannerConnection;
+import android.media.ThumbnailUtils;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import com.fpvout.digiview.MainActivity;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+
+import static com.fpvout.digiview.dvr.DVR.LATEST_THUMB_FILE;
+
+public class Mp4Muxer extends Thread {
+
+ private static final int TIMESCALE = 60;
+ private static final long DURATION = 1;
+
+ private final File h264Dump;
+ private final File ambientAudioFile;
+ private final File videoFile;
+ private final File output;
+ private final Context context;
+ private final File dumpDir;
+ private boolean thumbnail = false;
+
+
+ SeekableByteChannel file;
+ MP4Muxer muxer;
+ BufferH264ES es;
+
+ public Mp4Muxer(Context context, File dumpDir , File h264Dump, File ambientAudio, File output, boolean thumbnail) {
+ this.context = context;
+ this.dumpDir = dumpDir;
+ this.h264Dump = h264Dump;
+ this.ambientAudioFile = ambientAudio;
+ this.videoFile = new File(output.getAbsolutePath() + ".tmp");
+ this.output = output;
+ this.thumbnail = thumbnail;
+ }
+
+ private void init() throws IOException {
+ file = NIOUtils.writableChannel(videoFile);
+ muxer = MP4Muxer.createMP4MuxerToChannel(file);
+
+ es = new BufferH264ES(NIOUtils.mapFile(h264Dump));
+ }
+
+
+ private MuxerTrack initVideoTrack(Packet frame){
+ VideoCodecMeta md = new H264Decoder().getCodecMeta(frame.getData());
+ return muxer.addVideoTrack(Codec.H264, md);
+ }
+
+ private Packet skipToFirstValidFrame(){
+ return nextValidFrame(null, null);
+ }
+
+ /**
+ * Seek next valid frame.
+ * For every invalid frame, insert placeholder frame into track
+ */
+ private Packet nextValidFrame(Packet placeholder, MuxerTrack track){
+ Packet frame = null;
+ // drop invalid frames
+ while (frame == null) {
+ try{
+ frame = es.nextFrame();
+ if(frame == null){
+ return null; // end of input
+ }
+ }catch (Exception ignore){
+ try {
+ if(track != null){
+ track.addFrame(placeholder);
+ }
+ } catch (IOException ignored) { }
+ // invalid frames can cause a variety of exceptions on read
+ // continue
+ }
+ }
+ return frame;
+ }
+
+ @Override
+ public void run() {
+ try{
+
+ init();
+
+ Packet frame = skipToFirstValidFrame();
+
+ MuxerTrack track = null;
+ //save first frame as img (thumb)
+
+ while (frame != null) {
+ if (track == null) {
+ track = initVideoTrack(frame);
+ }
+
+ frame.setTimescale(TIMESCALE);
+ frame.setDuration(DURATION);
+ track.addFrame(frame);
+
+ frame = nextValidFrame(frame, track);
+ }
+ muxer.finish();
+ file.close();
+
+ mergeAudioVideoFiles(videoFile, ambientAudioFile, output);
+
+ if (this.thumbnail) {
+ Bitmap thumb = ThumbnailUtils.createVideoThumbnail(output.getAbsolutePath(), MediaStore.Images.Thumbnails.MINI_KIND);
+ FileOutputStream thumbOutputStream = new FileOutputStream(new File(dumpDir.getAbsolutePath() + "/" + LATEST_THUMB_FILE));
+ thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOutputStream);
+ thumbOutputStream.flush();
+ thumbOutputStream.close();
+ }
+
+ // add mp4 to gallery
+ MediaScannerConnection.scanFile(context,
+ new String[]{output.toString()},
+ null, null);
+
+ // cleanup
+ h264Dump.delete();
+ videoFile.delete();
+ ambientAudioFile.delete();
+ } catch (IOException exception){
+ Log.e("DIGIVIEW", "MUXER: " + exception.getMessage());
+ }
+ }
+
+ public void mergeAudioVideoFiles(File _videoFile, File _ambientFile, File _output) throws IOException {
+ Movie movie = MovieCreator.build(_videoFile.getAbsolutePath());
+ AACTrackImpl aacTrack = new AACTrackImpl(new FileDataSourceImpl(_ambientFile));
+ ClippedTrack aacCroppedTrack = new ClippedTrack(aacTrack, 1, aacTrack.getSamples().size());
+ movie.addTrack(aacCroppedTrack);
+
+ Container mp4file = new DefaultMp4Builder().build(movie);
+
+ FileOutputStream fileOutputStream = new FileOutputStream(_output);
+ FileChannel fc = fileOutputStream.getChannel();
+ mp4file.writeContainer(fc);
+ fileOutputStream.close();
+ }
+}
diff --git a/app/src/main/java/com/fpvout/digiview/helpers/StreamDumper.java b/app/src/main/java/com/fpvout/digiview/helpers/StreamDumper.java
new file mode 100644
index 0000000..f015898
--- /dev/null
+++ b/app/src/main/java/com/fpvout/digiview/helpers/StreamDumper.java
@@ -0,0 +1,71 @@
+package com.fpvout.digiview.helpers;
+
+import android.content.Context;
+import android.os.Environment;
+import android.os.Handler;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+
+public class StreamDumper {
+
+ private FileOutputStream fos;
+ private boolean bytesWritten = false;
+
+ private final File dumpDir;
+ private File streamDump;
+ private File streamAmbient;
+ private File outFile;
+ private final Context context;
+
+ public StreamDumper(Context context, String defaultPath){
+ this.context = context;
+ dumpDir = new File(defaultPath);
+
+ dumpDir.mkdirs();
+ }
+
+ public void dump(byte[] buffer, int offset, int receivedBytes) {
+
+ try {
+ fos.write(buffer, offset, receivedBytes);
+ bytesWritten = true;
+ } catch (IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ public void init(String videoFileName, String ambientAudioFileName, String outFileFileName) {
+ try {
+ streamDump = new File(dumpDir, videoFileName);
+ streamAmbient = new File(dumpDir, ambientAudioFileName);
+ outFile = new File(dumpDir, outFileFileName);
+ fos = new FileOutputStream(streamDump);
+ bytesWritten = false;
+ } catch (IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ public void stop(Handler completeHandler) {
+ try {
+ if(fos != null){
+ fos.flush();
+ fos.close();
+
+ if(bytesWritten) {
+ new Mp4Muxer(this.context, dumpDir, streamDump, streamAmbient,outFile, true).start();
+ }
+ }
+ if(!bytesWritten){
+ streamDump.delete();
+ }
+ completeHandler.sendEmptyMessage(0);
+ } catch (IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+}
diff --git a/app/src/main/java/com/fpvout/digiview/helpers/ThreadPerTaskExecutor.java b/app/src/main/java/com/fpvout/digiview/helpers/ThreadPerTaskExecutor.java
new file mode 100644
index 0000000..9e86b2c
--- /dev/null
+++ b/app/src/main/java/com/fpvout/digiview/helpers/ThreadPerTaskExecutor.java
@@ -0,0 +1,9 @@
+package com.fpvout.digiview.helpers;
+
+import java.util.concurrent.Executor;
+
+public class ThreadPerTaskExecutor implements Executor {
+ public void execute(Runnable r) {
+ new Thread(r).start();
+ }
+}
diff --git a/app/src/main/java/usb/AndroidUSBInputStream.java b/app/src/main/java/usb/AndroidUSBInputStream.java
index 0bd94c7..077720a 100644
--- a/app/src/main/java/usb/AndroidUSBInputStream.java
+++ b/app/src/main/java/usb/AndroidUSBInputStream.java
@@ -18,9 +18,11 @@
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.util.Log;
+import com.fpvout.digiview.helpers.DataListener;
import java.io.IOException;
import java.io.InputStream;
+import java.util.Arrays;
/**
* This class acts as a wrapper to read data from the USB Interface in Android
@@ -40,15 +42,15 @@ public class AndroidUSBInputStream extends InputStream {
private final UsbEndpoint sendEndPoint;
private final boolean working = false;
-
+ private DataListener inputListener = null;
/**
* Class constructor. Instantiates a new {@code AndroidUSBInputStream}
* object with the given parameters.
*
* @param readEndpoint The USB end point to use to read data from.
- * @param connection The USB connection to use to read data from.
- *
+ * @param sendEndpoint The USB end point to use to sent data to.
+ * @param connection The USB connection to use to read data from.
* @see UsbDeviceConnection
* @see UsbEndpoint
*/
@@ -61,7 +63,7 @@ public AndroidUSBInputStream( UsbEndpoint readEndpoint, UsbEndpoint sendEndpoint
@Override
public int read() throws IOException {
byte[] buffer = new byte[131072];
- return read(buffer, 0, buffer.length);
+ return read(buffer, 0, buffer.length);
}
@Override
@@ -72,12 +74,23 @@ public int read(byte[] buffer, int offset, int length) throws IOException {
Log.d(TAG, "received buffer empty, sending magic packet again...");
usbConnection.bulkTransfer(sendEndPoint, "RMVT".getBytes(), "RMVT".getBytes().length, 2000);
receivedBytes = usbConnection.bulkTransfer(receiveEndPoint, buffer, buffer.length, READ_TIMEOUT);
+ } else {
+ if (inputListener != null) {
+ byte[] bufferCopy = Arrays.copyOf(buffer, buffer.length);
+ this.inputListener.calllback(bufferCopy, offset, bufferCopy.length);
+ }
}
return receivedBytes;
}
+ public void setInputStreamListener(DataListener inputListener) {
+ this.inputListener = inputListener;
+ }
+
@Override
- public void close() throws IOException {}
+ public void close() throws IOException {
+ super.close();
+ }
}
diff --git a/app/src/main/java/usb/AndroidUSBOutputStream.java b/app/src/main/java/usb/AndroidUSBOutputStream.java
index 3225bda..da7f042 100644
--- a/app/src/main/java/usb/AndroidUSBOutputStream.java
+++ b/app/src/main/java/usb/AndroidUSBOutputStream.java
@@ -1,18 +1,3 @@
-/*
- * Copyright 2019, Digi International Inc.
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
package usb;
import android.hardware.usb.UsbDeviceConnection;
@@ -20,7 +5,6 @@
import java.io.IOException;
import java.io.OutputStream;
-import java.util.concurrent.LinkedBlockingQueue;
/**
* This class acts as a wrapper to write data to the USB Interface in Android
@@ -31,15 +15,9 @@ public class AndroidUSBOutputStream extends OutputStream {
// Constants.
private static final int WRITE_TIMEOUT = 2000;
- // Variables.
private final UsbDeviceConnection usbConnection;
-
private final UsbEndpoint sendEndPoint;
- private LinkedBlockingQueue writeQueue;
-
- private final boolean streamOpen = true;
-
/**
* Class constructor. Instantiates a new {@code AndroidUSBOutputStream}
* object with the given parameters.
@@ -80,10 +58,8 @@ public void write(byte[] buffer) {
@Override
public void write(byte[] buffer, int offset, int count) {
usbConnection.bulkTransfer(sendEndPoint, buffer, count, WRITE_TIMEOUT);
-
}
-
@Override
public void close() throws IOException {
super.close();
diff --git a/app/src/main/java/usb/CircularByteBuffer.java b/app/src/main/java/usb/CircularByteBuffer.java
index 8540a57..90f9109 100644
--- a/app/src/main/java/usb/CircularByteBuffer.java
+++ b/app/src/main/java/usb/CircularByteBuffer.java
@@ -1,18 +1,3 @@
-/*
- * Copyright 2019, Digi International Inc.
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at http://mozilla.org/MPL/2.0/.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
package usb;
/**
@@ -58,7 +43,7 @@ public CircularByteBuffer(int size) {
* @throws NullPointerException if {@code data == null}.
*
* @see #read(byte[], int, int)
- * @see #skip(int)
+ // * @see #skip(int)
*/
public synchronized int write(byte[] data, int offset, int numBytes) {
if (data == null)
@@ -105,8 +90,8 @@ public synchronized int write(byte[] data, int offset, int numBytes) {
* @throws IllegalArgumentException if {@code offset < 0} or
* if {@code numBytes < 1}.
* @throws NullPointerException if {@code data == null}.
- *
- * @see #skip(int)
+ *
+ // * @see #skip(int)
* @see #write(byte[], int, int)
*/
public synchronized int read(byte[] data, int offset, int numBytes) {
@@ -135,54 +120,53 @@ public synchronized int read(byte[] data, int offset, int numBytes) {
} else {
System.arraycopy(buffer, getReadIndex(), data, offset, buffer.length - getReadIndex());
System.arraycopy(buffer, 0, data, offset + buffer.length - getReadIndex(), numBytes - (buffer.length - getReadIndex()));
- readIndex = numBytes-(buffer.length - getReadIndex());
+ readIndex = numBytes - (buffer.length - getReadIndex());
}
-
+
// If we have read all bytes, set the buffer as empty.
if (readIndex == writeIndex)
empty = true;
-
- return numBytes;
- }
- /**
- * Skips the given number of bytes from the circular byte buffer.
- *
- * @param numBytes Number of bytes to skip.
- * @return The number of bytes actually skipped.
- *
- * @throws IllegalArgumentException if {@code numBytes < 1}.
- *
- * @see #read(byte[], int, int)
- * @see #write(byte[], int, int)
- */
- public synchronized int skip(int numBytes) {
- if (numBytes < 1)
- throw new IllegalArgumentException("Number of bytes to skip must be greater than 0.");
-
- // If we are empty, return 0.
- if (empty)
- return 0;
-
- if (availableToRead() < numBytes)
- return skip(availableToRead());
- if (numBytes < buffer.length - getReadIndex())
- readIndex = getReadIndex() + numBytes;
- else
- readIndex = numBytes - (buffer.length - getReadIndex());
-
- // If we have skipped all bytes, set the buffer as empty.
- if (readIndex == writeIndex)
- empty = true;
-
return numBytes;
}
+// /**
+// * Skips the given number of bytes from the circular byte buffer.
+// *
+// * @param numBytes Number of bytes to skip.
+// * @return The number of bytes actually skipped.
+// *
+// * @throws IllegalArgumentException if {@code numBytes < 1}.
+// *
+// * @see #read(byte[], int, int)
+// * @see #write(byte[], int, int)
+// */
+// public synchronized int skip(int numBytes) {
+// if (numBytes < 1)
+// throw new IllegalArgumentException("Number of bytes to skip must be greater than 0.");
+//
+// // If we are empty, return 0.
+// if (empty)
+// return 0;
+//
+// if (availableToRead() < numBytes)
+// return skip(availableToRead());
+// if (numBytes < buffer.length - getReadIndex())
+// readIndex = getReadIndex() + numBytes;
+// else
+// readIndex = numBytes - (buffer.length - getReadIndex());
+//
+// // If we have skipped all bytes, set the buffer as empty.
+// if (readIndex == writeIndex)
+// empty = true;
+//
+// return numBytes;
+// }
+
/**
* Returns the available number of bytes to read from the byte buffer.
- *
+ *
* @return The number of bytes in the buffer available for reading.
- *
* @see #getCapacity()
* @see #read(byte[], int, int)
*/
@@ -212,22 +196,22 @@ private int getReadIndex() {
private int getWriteIndex() {
return writeIndex;
}
-
+
/**
* Returns the circular byte buffer capacity.
- *
+ *
* @return The circular byte buffer capacity.
*/
public int getCapacity() {
return buffer.length;
}
-
- /**
- * Clears the circular buffer.
- */
- public void clearBuffer() {
- empty = true;
- readIndex = 0;
- writeIndex = 0;
- }
+
+// /**
+// * Clears the circular buffer.
+// */
+// public void clearBuffer() {
+// empty = true;
+// readIndex = 0;
+// writeIndex = 0;
+// }
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-hdpi/rounded_corners.xml b/app/src/main/res/drawable-hdpi/rounded_corners.xml
new file mode 100644
index 0000000..9f9308b
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/rounded_corners.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/record.png b/app/src/main/res/drawable-v24/record.png
new file mode 100644
index 0000000..3868ddc
Binary files /dev/null and b/app/src/main/res/drawable-v24/record.png differ
diff --git a/app/src/main/res/drawable-v24/stop.png b/app/src/main/res/drawable-v24/stop.png
new file mode 100644
index 0000000..4e9a096
Binary files /dev/null and b/app/src/main/res/drawable-v24/stop.png differ
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 3d2145d..363ee98 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -40,17 +40,57 @@
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toTopOf="parent" >
+
-
+ app:layout_constraintTop_toTopOf="parent">
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index dba2270..cd4e64f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -40,5 +40,12 @@
Copyright
Open-Source License
MIT License
-
+ Connect
+ recording started
+ recording stopped
+ No stream! recording could not be started
+ dvr has been saved
+ merging audio and video of the dvr
+ Storage access is required.
+ repairing dvrs ...
\ No newline at end of file