Skip to content

Initial implementation of a simple dvr #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ android {
defaultConfig {
applicationId "com.fpvout.digiview"
minSdkVersion 21
targetSdkVersion 30
targetSdkVersion 29
versionCode 1
versionName "1.0"

Expand All @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

<uses-feature android:name="android.hardware.usb.host" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<application
android:allowBackup="false"
Expand All @@ -12,6 +13,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Digiview"
android:requestLegacyExternalStorage="true"
>

<activity
Expand Down
52 changes: 51 additions & 1 deletion app/src/main/java/com/fpvout/digiview/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.fpvout.digiview;

import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.LayoutTransition;
import android.app.Application;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
Expand All @@ -19,12 +23,15 @@
import android.view.ViewGroup;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import java.util.HashMap;

public class MainActivity extends AppCompatActivity implements UsbDeviceListener {
private static AppCompatActivity instance;
private static final String ACTION_USB_PERMISSION = "com.fpvout.digiview.USB_PERMISSION";
private static final String TAG = "DIGIVIEW";
private static final int VENDOR_ID = 11427;
Expand All @@ -47,6 +54,7 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
instance = this;
Log.d(TAG, "APP - On Create");
setContentView(R.layout.activity_main);

Expand Down Expand Up @@ -81,7 +89,7 @@ protected void onCreate(Bundle savedInstanceState) {
fpvView = findViewById(R.id.fpvView);

// Enable resizing animations
((ViewGroup)findViewById(R.id.mainLayout)).getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
((ViewGroup) findViewById(R.id.mainLayout)).getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);

gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
Expand Down Expand Up @@ -115,13 +123,51 @@ public void onScaleEnd(ScaleGestureDetector detector) {
mUsbMaskConnection = new UsbMaskConnection();
mVideoReader = new VideoReaderExoplayer(fpvView, overlayView, this);

if(checkStoragePermission()){
// permission already granted
finishStartup();
}
}

private void finishStartup() {

if (!usbConnected) {
if (searchDevice()) {
connect();
} else {
overlayView.showOpaque(R.string.waiting_for_usb_device, OverlayStatus.Disconnected);
}
}

}

@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.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
Expand Down Expand Up @@ -244,4 +290,8 @@ protected void onDestroy() {
mVideoReader.stop();
usbConnected = false;
}

public static Context getContext() {
return instance.getApplicationContext();
}
}
117 changes: 117 additions & 0 deletions app/src/main/java/com/fpvout/digiview/Mp4Muxer.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
73 changes: 73 additions & 0 deletions app/src/main/java/com/fpvout/digiview/StreamDumper.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
12 changes: 10 additions & 2 deletions app/src/main/java/usb/AndroidUSBInputStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand All @@ -167,3 +174,4 @@ public void close() throws IOException {
super.close();
}
}