diff --git a/app/build.gradle b/app/build.gradle
index 281b0dc..ed57f43 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -9,7 +9,7 @@ android {
defaultConfig {
applicationId "com.fpvout.digiview"
minSdkVersion 21
- targetSdkVersion 30
+ targetSdkVersion 29
versionCode 1
versionName "1.0"
@@ -35,6 +35,7 @@ dependencies {
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.exoplayer:exoplayer:2.13.3'
+ implementation 'org.jcodec:jcodec:0.2.5'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f5bb3de..0ddaf96 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+
+ finishStartup();
+ }
+ else {
+ overlayView.showOpaque("Storage access is required.", OverlayStatus.Error);
+ }
+ }
+ }
+
+ private boolean checkStoragePermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+
+ }else{
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
+ return false;
+ }
+ }
+ return true;
}
@Override
@@ -244,4 +290,8 @@ protected void onDestroy() {
mVideoReader.stop();
usbConnected = false;
}
+
+ public static Context getContext() {
+ return instance.getApplicationContext();
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fpvout/digiview/Mp4Muxer.java b/app/src/main/java/com/fpvout/digiview/Mp4Muxer.java
new file mode 100644
index 0000000..ab55b4e
--- /dev/null
+++ b/app/src/main/java/com/fpvout/digiview/Mp4Muxer.java
@@ -0,0 +1,117 @@
+package com.fpvout.digiview;
+
+import android.media.MediaScannerConnection;
+import android.util.Log;
+
+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 java.io.File;
+import java.io.IOException;
+
+public class Mp4Muxer extends Thread {
+
+ private static final int TIMESCALE = 60;
+ private static final long DURATION = 1;
+
+ private final File h264Dump;
+ private final File output;
+
+ SeekableByteChannel file;
+ MP4Muxer muxer;
+ BufferH264ES es;
+
+ public Mp4Muxer(File h264Dump, File output) {
+ this.h264Dump = h264Dump;
+ this.output = output;
+ }
+
+ private void init() throws IOException {
+ file = NIOUtils.writableChannel(output);
+ 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;
+ 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();
+
+ // cleanup
+ h264Dump.delete();
+
+ // add mp4 to gallery
+ MediaScannerConnection.scanFile(MainActivity.getContext(),
+ new String[]{output.toString()},
+ null, null);
+
+ } catch (IOException exception){
+ Log.e("DIGIVIEW", "MUXER: " + exception.getMessage());
+ }
+ }
+}
diff --git a/app/src/main/java/com/fpvout/digiview/StreamDumper.java b/app/src/main/java/com/fpvout/digiview/StreamDumper.java
new file mode 100644
index 0000000..8b06ec3
--- /dev/null
+++ b/app/src/main/java/com/fpvout/digiview/StreamDumper.java
@@ -0,0 +1,73 @@
+package com.fpvout.digiview;
+
+import android.os.Environment;
+
+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 String startTimestamp;
+
+ public boolean dumpStream = true;
+
+ public StreamDumper(){
+ dumpDir = new File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
+ "DigiView");
+
+ dumpDir.mkdirs();
+
+ init();
+ }
+
+ public void dump(byte[] buffer, int offset, int receivedBytes) {
+
+ try {
+ fos.write(buffer, offset, receivedBytes);
+ bytesWritten = true;
+ } catch (IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ private void init() {
+ try {
+ startTimestamp = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss")
+ .format(Calendar.getInstance().getTime());
+ streamDump = new File(dumpDir, "DigiView "+startTimestamp+".h264");
+ fos = new FileOutputStream(streamDump);
+ bytesWritten = false;
+ } catch (IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ public void stop() {
+ try {
+ if(fos != null){
+ fos.flush();
+ fos.close();
+
+ if(bytesWritten) {
+ File out = new File(dumpDir, "DigiView "+startTimestamp+".mp4");
+ new Mp4Muxer(streamDump, out).start();
+ }
+ }
+ if(!bytesWritten){
+ streamDump.delete();
+ }
+
+ } catch (IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+}
diff --git a/app/src/main/java/usb/AndroidUSBInputStream.java b/app/src/main/java/usb/AndroidUSBInputStream.java
index d57eeac..cbd0b62 100644
--- a/app/src/main/java/usb/AndroidUSBInputStream.java
+++ b/app/src/main/java/usb/AndroidUSBInputStream.java
@@ -21,6 +21,8 @@
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
+import com.fpvout.digiview.StreamDumper;
+
/**
* This class acts as a wrapper to read data from the USB Interface in Android
* behaving like an {@code InputputStream} class.
@@ -137,17 +139,22 @@ public void startReadThread() {
working = true;
readBuffer = new CircularByteBuffer(READ_BUFFER_SIZE);
receiveThread = new Thread() {
+ StreamDumper streamDumper;
@Override
public void run() {
+ streamDumper = new StreamDumper();
while (working) {
byte[] buffer = new byte[1024];
int receivedBytes = usbConnection.bulkTransfer(receiveEndPoint, buffer, buffer.length, READ_TIMEOUT) - OFFSET;
if (receivedBytes > 0) {
- byte[] data = new byte[receivedBytes];
- System.arraycopy(buffer, OFFSET, data, 0, receivedBytes);
readBuffer.write(buffer, OFFSET, receivedBytes);
+ if(streamDumper.dumpStream){
+ streamDumper.dump(buffer, OFFSET, receivedBytes);
+ }
}
}
+ streamDumper.stop();
+
}
};
receiveThread.start();
@@ -167,3 +174,4 @@ public void close() throws IOException {
super.close();
}
}
+