diff --git a/.gitignore b/.gitignore index 293d9ae..a722239 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ .externalNativeBuild .cxx local.properties +app/debug +app/alpha +app/beta +app/release diff --git a/app/build.gradle b/app/build.gradle index de58b67..91434fd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.application' + id "com.starter.easylauncher" version "4.0.0" } android { @@ -12,7 +13,7 @@ android { keyAlias digiviewKeyAlias } catch (ex) { - println("You should define mStoreFile, mStorePassword, mKeyPassword and mKeyAlias in ~/.gradle/gradle.properties.") + println("You should define mStoreFile, mStorePassword, mKeyPassword and mKeyAlias in ~/.gradle/gradle.properties : "+ ex.message) } } } @@ -65,14 +66,14 @@ android { dependencies { - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.3.0' 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 'com.google.android.exoplayer:exoplayer:2.14.1' implementation 'io.sentry:sentry-android:4.3.0' implementation 'androidx.preference:preference:1.1.1' - testImplementation 'junit:junit:4.+' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7df7ca4..fd85baf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,21 +12,20 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.Digiview" - > + android:theme="@style/Theme.Digiview"> + android:theme="@style/Theme.AppCompat.Dialog" /> + android:label="@string/title_activity_settings" /> + android:configChanges="orientation|keyboardHidden" + android:launchMode="singleTask" + android:screenOrientation="sensorLandscape"> @@ -38,14 +37,22 @@ - - - - + + + + new Extractor[] {new H264Extractor()}; - private static int MAX_SYNC_FRAME_SIZE = 131072; - - private long firstSampleTimestampUs; private static long sampleTime = 10000; // todo: try to lower this. it directly infer on speed and latency. this should be equal to 16666 to reach 60fps but works better with lower value private final H264Reader reader; private final ParsableByteArray sampleData; - + private long firstSampleTimestampUs; private boolean startedPacket; - public H264Extractor() { - this(0); - } - public H264Extractor(int mMaxSyncFrameSize, int mSampleTime) { this(0, mMaxSyncFrameSize, mSampleTime); } - public H264Extractor(long firstSampleTimestampUs) { - this(firstSampleTimestampUs, MAX_SYNC_FRAME_SIZE, (int) sampleTime); - } - public H264Extractor(long firstSampleTimestampUs, int mMaxSyncFrameSize, int mSampleTime) { MAX_SYNC_FRAME_SIZE = mMaxSyncFrameSize; sampleTime = mSampleTime; this.firstSampleTimestampUs = firstSampleTimestampUs; - reader = new H264Reader(new SeiReader(new ArrayList()),false,true); + reader = new H264Reader(new SeiReader(new ArrayList<>()), false, true); sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); } // Extractor implementation. @Override - public boolean sniff(ExtractorInput input) throws IOException { + @NonNullApi + public boolean sniff(ExtractorInput input) { return true; } @Override + @NonNullApi public void init(ExtractorOutput output) { reader.createTracks(output, new TsPayloadReader.TrackIdGenerator(0, 1)); output.endTracks(); @@ -78,6 +66,7 @@ public void release() { } @Override + @NonNullApi public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { int bytesRead = input.read(sampleData.getData(), 0, MAX_SYNC_FRAME_SIZE); if (bytesRead == C.RESULT_END_OF_INPUT) { diff --git a/app/src/main/java/com/fpvout/digiview/InputStreamBufferedDataSource.java b/app/src/main/java/com/fpvout/digiview/InputStreamBufferedDataSource.java index 7ba7af9..d6bc5b6 100644 --- a/app/src/main/java/com/fpvout/digiview/InputStreamBufferedDataSource.java +++ b/app/src/main/java/com/fpvout/digiview/InputStreamBufferedDataSource.java @@ -1,8 +1,9 @@ package com.fpvout.digiview; -import android.content.Context; import android.net.Uri; +import androidx.annotation.NonNull; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -19,10 +20,8 @@ public class InputStreamBufferedDataSource implements DataSource { private static final String ERROR_THREAD_NOT_INITIALIZED = "Read thread not initialized, call first 'startReadThread()'"; private static final long READ_TIMEOUT = 200; - private Context context; - private DataSpec dataSpec; + private final DataSpec dataSpec; private InputStream inputStream; - private long bytesRemaining; private boolean opened; private CircularByteBuffer readBuffer; @@ -30,20 +29,20 @@ public class InputStreamBufferedDataSource implements DataSource { private boolean working; - public InputStreamBufferedDataSource(Context context, DataSpec dataSpec, InputStream inputStream) { - this.context = context; + public InputStreamBufferedDataSource(DataSpec dataSpec, InputStream inputStream) { this.dataSpec = dataSpec; this.inputStream = inputStream; startReadThread(); } @Override - public void addTransferListener(TransferListener transferListener) { + public void addTransferListener(@NonNull TransferListener transferListener) { } @Override public long open(DataSpec dataSpec) throws IOException { + long bytesRemaining; try { long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) @@ -63,7 +62,7 @@ public long open(DataSpec dataSpec) throws IOException { } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException { if (readBuffer == null) throw new IOException(ERROR_THREAD_NOT_INITIALIZED); @@ -71,8 +70,6 @@ public int read(byte[] buffer, int offset, int readLength) throws IOException { int readBytes = 0; while (System.currentTimeMillis() < deadLine && readBytes <= 0) readBytes = readBuffer.read(buffer, offset, readLength); - if (readBytes <= 0) - return readBytes; return readBytes; } diff --git a/app/src/main/java/com/fpvout/digiview/InputStreamDataSource.java b/app/src/main/java/com/fpvout/digiview/InputStreamDataSource.java index 7b10606..fe1634c 100644 --- a/app/src/main/java/com/fpvout/digiview/InputStreamDataSource.java +++ b/app/src/main/java/com/fpvout/digiview/InputStreamDataSource.java @@ -1,8 +1,9 @@ package com.fpvout.digiview; -import android.content.Context; import android.net.Uri; +import androidx.annotation.NonNull; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -13,25 +14,23 @@ import java.io.InputStream; public class InputStreamDataSource implements DataSource { - private Context context; - private DataSpec dataSpec; + private final DataSpec dataSpec; private InputStream inputStream; - private long bytesRemaining; private boolean opened; - public InputStreamDataSource(Context context, DataSpec dataSpec, InputStream inputStream) { - this.context = context; + public InputStreamDataSource(DataSpec dataSpec, InputStream inputStream) { this.dataSpec = dataSpec; this.inputStream = inputStream; } @Override - public void addTransferListener(TransferListener transferListener) { + public void addTransferListener(@NonNull TransferListener transferListener) { } @Override public long open(DataSpec dataSpec) throws IOException { + long bytesRemaining; try { long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) @@ -51,7 +50,7 @@ public long open(DataSpec dataSpec) throws IOException { } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException { return inputStream.read(buffer, offset, readLength); } diff --git a/app/src/main/java/com/fpvout/digiview/MainActivity.java b/app/src/main/java/com/fpvout/digiview/MainActivity.java index 7f4c7a4..6643d65 100644 --- a/app/src/main/java/com/fpvout/digiview/MainActivity.java +++ b/app/src/main/java/com/fpvout/digiview/MainActivity.java @@ -3,7 +3,6 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.LayoutTransition; -import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -21,28 +20,25 @@ import android.view.ViewGroup; import android.view.WindowManager; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; import androidx.preference.PreferenceManager; -import java.util.HashMap; - import io.sentry.SentryLevel; import io.sentry.android.core.SentryAndroid; +import static com.fpvout.digiview.UsbMaskConnection.ACTION_USB_PERMISSION; import static com.fpvout.digiview.VideoReaderExoplayer.VideoZoomedIn; public class MainActivity extends AppCompatActivity implements UsbDeviceListener { - 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; - private static final int PRODUCT_ID = 31; - private int shortAnimationDuration; - private float buttonAlpha = 1; - private View settingsButton; - private View watermarkView; - private OverlayView overlayView; - PendingIntent permissionIntent; + private static final String ShowWatermark = "ShowWatermark"; UsbDeviceBroadcastReceiver usbDeviceBroadcastReceiver; UsbManager usbManager; UsbDevice usbDevice; @@ -50,10 +46,14 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener VideoReaderExoplayer mVideoReader; boolean usbConnected = false; SurfaceView fpvView; + private int shortAnimationDuration; + private float buttonAlpha = 1; + private View settingsButton; + private View watermarkView; + private OverlayView overlayView; private GestureDetector gestureDetector; private ScaleGestureDetector scaleGestureDetector; private SharedPreferences sharedPreferences; - private static final String ShowWatermark = "ShowWatermark"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -65,58 +65,48 @@ protected void onCreate(Bundle savedInstanceState) { checkDataCollectionAgreement(); // Hide top bar and status bar - View decorView = getWindow().getDecorView(); - decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.hide(); - } + setFullscreen(); // Prevent screen from sleeping getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); + // Register app for auto launch usbDeviceBroadcastReceiver = new UsbDeviceBroadcastReceiver(this); - IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); registerReceiver(usbDeviceBroadcastReceiver, filter); IntentFilter filterDetached = new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED); registerReceiver(usbDeviceBroadcastReceiver, filterDetached); - shortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); watermarkView = findViewById(R.id.watermarkView); overlayView = findViewById(R.id.overlayView); fpvView = findViewById(R.id.fpvView); - settingsButton = findViewById(R.id.settingsButton); - settingsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent = new Intent(v.getContext(), SettingsActivity.class); - v.getContext().startActivity(intent); - } - }); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + shortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); // Enable resizing animations ((ViewGroup) findViewById(R.id.mainLayout)).getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); setupGestureDetectors(); - mUsbMaskConnection = new UsbMaskConnection(); - Handler videoReaderEventListener = new Handler(this.getMainLooper(), msg -> onVideoReaderEvent((VideoReaderExoplayer.VideoReaderEventMessageCode) msg.obj)); + settingsButton.setOnClickListener(v -> { + Intent intent = new Intent(v.getContext(), SettingsActivity.class); + v.getContext().startActivity(intent); + }); + + mVideoReader = new VideoReaderExoplayer(fpvView, this); + mVideoReader.setVideoPlayingEventListener(this::hideOverlay); + mVideoReader.setVideoWaitingEventListener(() -> showOverlay(R.string.waiting_for_video, OverlayStatus.Connected)); - mVideoReader = new VideoReaderExoplayer(fpvView, this, videoReaderEventListener); + mUsbMaskConnection = new UsbMaskConnection(); if (!usbConnected) { - if (searchDevice()) { + usbDevice = UsbMaskConnection.searchDevice(usbManager, getApplicationContext()); + if (usbDevice != null) { + Log.i(TAG, "USB - usbDevice attached"); + showOverlay(R.string.usb_device_found, OverlayStatus.Connected); connect(); } else { showOverlay(R.string.waiting_for_usb_device, OverlayStatus.Disconnected); @@ -124,6 +114,20 @@ public void onClick(View v) { } } + private void setFullscreen() { + WindowInsetsControllerCompat insetsControllerCompat = new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView()); + insetsControllerCompat.hide(WindowInsetsCompat.Type.statusBars() + | WindowInsetsCompat.Type.navigationBars() + | WindowInsetsCompat.Type.captionBar() + | WindowInsetsCompat.Type.ime() + ); + insetsControllerCompat.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.hide(); + } + } + private void setupGestureDetectors() { gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { @Override @@ -183,18 +187,18 @@ private void updateVideoZoom() { private void cancelButtonAnimation() { Handler handler = settingsButton.getHandler(); if (handler != null) { - settingsButton.getHandler().removeCallbacksAndMessages(null); + handler.removeCallbacksAndMessages(null); } } - private void showSettingsButton() { - cancelButtonAnimation(); - - if (overlayView.getVisibility() == View.VISIBLE) { - buttonAlpha = 1; - settingsButton.setAlpha(1); - } - } +// private void showSettingsButton() { +// cancelButtonAnimation(); +// +// if (overlayView.getVisibility() == View.VISIBLE) { +// buttonAlpha = 1; +// settingsButton.setAlpha(1); +// } +// } private void toggleSettingsButton() { if (buttonAlpha == 1 && overlayView.getVisibility() == View.VISIBLE) return; @@ -223,17 +227,30 @@ private void autoHideSettingsButton() { if (overlayView.getVisibility() == View.VISIBLE) return; if (buttonAlpha == 0) return; - settingsButton.postDelayed(new Runnable() { - @Override - public void run() { - buttonAlpha = 0; - settingsButton.animate() - .alpha(0) - .setDuration(shortAnimationDuration); - } + settingsButton.postDelayed(() -> { + buttonAlpha = 0; + settingsButton.animate() + .alpha(0) + .setDuration(shortAnimationDuration); }, 3000); } + private void showOverlay(int textId, OverlayStatus connected) { + overlayView.show(textId, connected); + updateWatermark(); + autoHideSettingsButton(); + updateVideoZoom(); + + } + + private void hideOverlay() { + overlayView.hide(); + updateWatermark(); + autoHideSettingsButton(); + updateVideoZoom(); + } + + @Override public void usbDeviceApproved(UsbDevice device) { Log.i(TAG, "USB - usbDevice approved"); @@ -249,32 +266,10 @@ public void usbDeviceDetached() { disconnect(); } - private boolean searchDevice() { - HashMap deviceList = usbManager.getDeviceList(); - if (deviceList.size() <= 0) { - usbDevice = null; - return false; - } - - for (UsbDevice device : deviceList.values()) { - if (device.getVendorId() == VENDOR_ID && device.getProductId() == PRODUCT_ID) { - if (usbManager.hasPermission(device)) { - Log.i(TAG, "USB - usbDevice attached"); - showOverlay(R.string.usb_device_found, OverlayStatus.Connected); - usbDevice = device; - return true; - } - - usbManager.requestPermission(device, permissionIntent); - } - } - - return false; - } private void connect() { usbConnected = true; - mUsbMaskConnection.setUsbDevice(usbManager.openDevice(usbDevice), usbDevice); + mUsbMaskConnection.setUsbDevice(usbManager, usbDevice); mVideoReader.setUsbMaskConnection(mUsbMaskConnection); overlayView.hide(); mVideoReader.start(); @@ -288,24 +283,16 @@ public void onResume() { super.onResume(); Log.d(TAG, "APP - On Resume"); - View decorView = getWindow().getDecorView(); - decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.hide(); - } + setFullscreen(); 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); } } @@ -315,29 +302,6 @@ public void onResume() { updateVideoZoom(); } - private boolean onVideoReaderEvent(VideoReaderExoplayer.VideoReaderEventMessageCode m) { - if (VideoReaderExoplayer.VideoReaderEventMessageCode.WAITING_FOR_VIDEO.equals(m)) { - Log.d(TAG, "event: WAITING_FOR_VIDEO"); - showOverlay(R.string.waiting_for_video, OverlayStatus.Connected); - } else if (VideoReaderExoplayer.VideoReaderEventMessageCode.VIDEO_PLAYING.equals(m)) { - Log.d(TAG, "event: VIDEO_PLAYING"); - hideOverlay(); - } - return false; // false to continue listening - } - - private void showOverlay(int textId, OverlayStatus connected) { - overlayView.show(textId, connected); - updateWatermark(); - showSettingsButton(); - } - - private void hideOverlay() { - overlayView.hide(); - updateWatermark(); - showSettingsButton(); - autoHideSettingsButton(); - } private void disconnect() { mUsbMaskConnection.stop(); @@ -372,33 +336,26 @@ protected void onDestroy() { usbConnected = false; } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - boolean dataCollectionAccepted = preferences.getBoolean("dataCollectionAccepted", false); - - if (requestCode == 1) { // Data Collection agreement Activity - if (resultCode == RESULT_OK && dataCollectionAccepted) { - SentryAndroid.init(this, options -> options.setBeforeSend((event, hint) -> { - if (SentryLevel.DEBUG.equals(event.getLevel())) - return null; - else - return event; - })); - } - - } - } //onActivityResult - private void checkDataCollectionAgreement() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean dataCollectionAccepted = preferences.getBoolean("dataCollectionAccepted", false); boolean dataCollectionReplied = preferences.getBoolean("dataCollectionReplied", false); if (!dataCollectionReplied) { Intent intent = new Intent(this, DataCollectionAgreementPopupActivity.class); - startActivityForResult(intent, 1); + ActivityResultLauncher activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), (ActivityResultCallback) result -> { + if (result.getResultCode() == RESULT_OK) { + SentryAndroid.init(getApplicationContext(), options -> options.setBeforeSend((event, hint) -> { + if (SentryLevel.DEBUG.equals(event.getLevel())) + return null; + else + return event; + })); + } + setFullscreen(); + }); + activityResultLauncher.launch(intent); + + } else if (dataCollectionAccepted) { SentryAndroid.init(this, options -> options.setBeforeSend((event, hint) -> { if (SentryLevel.DEBUG.equals(event.getLevel())) 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 89e4965..9dc7067 100644 --- a/app/src/main/java/com/fpvout/digiview/PerformancePreset.java +++ b/app/src/main/java/com/fpvout/digiview/PerformancePreset.java @@ -1,19 +1,17 @@ package com.fpvout.digiview; -public class PerformancePreset { - int h264ReaderMaxSyncFrameSize = 131072; - int h264ReaderSampleTime = 10000; - int exoPlayerMinBufferMs = 500; - int exoPlayerMaxBufferMs = 2000; - int exoPlayerBufferForPlaybackMs = 17; - int exoPlayerBufferForPlaybackAfterRebufferMs = 17; - DataSourceType dataSourceType = DataSourceType.INPUT_STREAM; - - private PerformancePreset(){ +import androidx.annotation.NonNull; - } +public class PerformancePreset { + int h264ReaderMaxSyncFrameSize; + int h264ReaderSampleTime; + int exoPlayerMinBufferMs; + int exoPlayerMaxBufferMs; + int exoPlayerBufferForPlaybackMs; + int exoPlayerBufferForPlaybackAfterRebufferMs; + DataSourceType dataSourceType; - private PerformancePreset(int mH264ReaderMaxSyncFrameSize, int mH264ReaderSampleTime, int mExoPlayerMinBufferMs, int mExoPlayerMaxBufferMs, int mExoPlayerBufferForPlaybackMs, int mExoPlayerBufferForPlaybackAfterRebufferMs, DataSourceType mDataSourceType){ + private PerformancePreset(int mH264ReaderMaxSyncFrameSize, int mH264ReaderSampleTime, int mExoPlayerMinBufferMs, int mExoPlayerMaxBufferMs, int mExoPlayerBufferForPlaybackMs, int mExoPlayerBufferForPlaybackAfterRebufferMs, DataSourceType mDataSourceType) { h264ReaderMaxSyncFrameSize = mH264ReaderMaxSyncFrameSize; h264ReaderSampleTime = mH264ReaderSampleTime; exoPlayerMinBufferMs = mExoPlayerMinBufferMs; @@ -33,17 +31,14 @@ static PerformancePreset getPreset(PresetType p) { return new PerformancePreset(30720, 200, 32768, 65536, 0, 0, DataSourceType.BUFFERED_INPUT_STREAM); case LEGACY_BUFFERED: return new PerformancePreset(30720, 300, 32768, 65536, 34, 34, DataSourceType.BUFFERED_INPUT_STREAM); + case VIDEO_STREAM_SERVICE: + return new PerformancePreset(131072, 10000, 500, 2000, 17, 17, DataSourceType.VIDEO_STREAM_SERVICE); case DEFAULT: default: return new PerformancePreset(131072, 10000, 500, 2000, 17, 17, DataSourceType.INPUT_STREAM); } } - public enum DataSourceType { - INPUT_STREAM, - BUFFERED_INPUT_STREAM - } - static PerformancePreset getPreset(String p) { switch (p) { case "conservative": @@ -54,21 +49,16 @@ static PerformancePreset getPreset(String p) { return getPreset(PresetType.LEGACY); case "new_legacy": return getPreset(PresetType.LEGACY_BUFFERED); + case "video_stream_service": + return getPreset(PresetType.VIDEO_STREAM_SERVICE); case "default": default: return getPreset(PresetType.DEFAULT); } } - public enum PresetType { - DEFAULT, - CONSERVATIVE, - AGGRESSIVE, - LEGACY, - LEGACY_BUFFERED - } - @Override + @NonNull public String toString() { return "PerformancePreset{" + "h264ReaderMaxSyncFrameSize=" + h264ReaderMaxSyncFrameSize + @@ -80,4 +70,20 @@ public String toString() { ", dataSourceType=" + dataSourceType + '}'; } + + public enum PresetType { + DEFAULT, + CONSERVATIVE, + AGGRESSIVE, + LEGACY, + LEGACY_BUFFERED, + VIDEO_STREAM_SERVICE + } + + + public enum DataSourceType { + INPUT_STREAM, + BUFFERED_INPUT_STREAM, + VIDEO_STREAM_SERVICE + } } diff --git a/app/src/main/java/com/fpvout/digiview/UsbDeviceBroadcastReceiver.java b/app/src/main/java/com/fpvout/digiview/UsbDeviceBroadcastReceiver.java index db64b09..a2f75b9 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; } @@ -21,7 +22,7 @@ public void onReceive(Context context, Intent intent) { UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { - if(device != null){ + if (device != null) { listener.usbDeviceApproved(device); } } diff --git a/app/src/main/java/com/fpvout/digiview/UsbMaskConnection.java b/app/src/main/java/com/fpvout/digiview/UsbMaskConnection.java index 1d6d90e..57e6245 100644 --- a/app/src/main/java/com/fpvout/digiview/UsbMaskConnection.java +++ b/app/src/main/java/com/fpvout/digiview/UsbMaskConnection.java @@ -1,52 +1,69 @@ 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 java.io.IOException; +import java.util.HashMap; import usb.AndroidUSBInputStream; import usb.AndroidUSBOutputStream; public class UsbMaskConnection { - private final byte[] magicPacket = "RMVT".getBytes(); - private UsbDeviceConnection usbConnection; - private UsbDevice device; - private UsbInterface usbInterface; + 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; AndroidUSBInputStream mInputStream; AndroidUSBOutputStream mOutputStream; + private UsbDeviceConnection usbConnection; + private UsbInterface usbInterface; private boolean ready = false; + private VideoStreamService videoStreamService; public UsbMaskConnection() { } - public void setUsbDevice(UsbDeviceConnection c, UsbDevice d) { - usbConnection = c; - device = d; - usbInterface = device.getInterface(3); + public static UsbDevice searchDevice(UsbManager usbManager, Context c) { + PendingIntent permissionIntent = PendingIntent.getBroadcast(c, 0, new Intent(ACTION_USB_PERMISSION), 0); - usbConnection.claimInterface(usbInterface,true); + HashMap deviceList = usbManager.getDeviceList(); + if (deviceList.size() <= 0) { + return null; + } - mOutputStream = new AndroidUSBOutputStream(usbInterface.getEndpoint(0), usbConnection); - mInputStream = new AndroidUSBInputStream(usbInterface.getEndpoint(1), usbInterface.getEndpoint(0), usbConnection); - ready = true; + 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(){ - mOutputStream.write(magicPacket); + videoStreamService.start(); } public void stop() { ready = false; try { + if (videoStreamService != null) + videoStreamService.stop(); + if (mInputStream != null) mInputStream.close(); if (mOutputStream != null) mOutputStream.close(); - } catch (IOException e) { + } catch (IOException | InterruptedException e) { e.printStackTrace(); } @@ -59,4 +76,20 @@ public void stop() { public boolean isReady() { return ready; } + + public VideoStreamService getVideoStreamService() { + return videoStreamService; + } + + public void setUsbDevice(UsbManager usbManager, UsbDevice d) { + 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); + videoStreamService = new VideoStreamService(mInputStream, mOutputStream); + 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 23c795e..7146039 100644 --- a/app/src/main/java/com/fpvout/digiview/VideoReaderExoplayer.java +++ b/app/src/main/java/com/fpvout/digiview/VideoReaderExoplayer.java @@ -5,17 +5,16 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; -import android.os.Message; import android.util.Log; import android.view.SurfaceView; +import androidx.annotation.NonNull; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.preference.PreferenceManager; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; @@ -27,23 +26,24 @@ 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.VideoListener; +import com.google.android.exoplayer2.video.VideoSize; import usb.AndroidUSBInputStream; public class VideoReaderExoplayer { - private static final String TAG = "DIGIVIEW"; - private Handler videoReaderEventListener; - private SimpleExoPlayer mPlayer; static final String VideoPreset = "VideoPreset"; + static final String VideoZoomedIn = "VideoZoomedIn"; + private static final String TAG = "DIGIVIEW"; private final SurfaceView surfaceView; + private final Context context; + private final SharedPreferences sharedPreferences; + private SimpleExoPlayer mPlayer; 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 VideoPlayingListener videoPlayingListener = null; + private VideoWaitingListener videoWaitingListener = null; VideoReaderExoplayer(SurfaceView videoSurface, Context c) { surfaceView = videoSurface; @@ -51,9 +51,12 @@ public class VideoReaderExoplayer { sharedPreferences = PreferenceManager.getDefaultSharedPreferences(c); } - VideoReaderExoplayer(SurfaceView videoSurface, Context c, Handler v) { - this(videoSurface, c); - videoReaderEventListener = v; + public void setVideoPlayingEventListener(VideoPlayingListener listener) { + this.videoPlayingListener = listener; + } + + public void setVideoWaitingEventListener(VideoWaitingListener listener) { + this.videoWaitingListener = listener; } public void setUsbMaskConnection(UsbMaskConnection connection) { @@ -65,95 +68,93 @@ public void start() { zoomedIn = sharedPreferences.getBoolean(VideoZoomedIn, true); performancePreset = PerformancePreset.getPreset(sharedPreferences.getString(VideoPreset, "default")); - DefaultLoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(performancePreset.exoPlayerMinBufferMs, performancePreset.exoPlayerMaxBufferMs, performancePreset.exoPlayerBufferForPlaybackMs, performancePreset.exoPlayerBufferForPlaybackAfterRebufferMs).build(); - mPlayer = new SimpleExoPlayer.Builder(context).setLoadControl(loadControl).build(); - mPlayer.setVideoSurfaceView(surfaceView); - mPlayer.setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING); - mPlayer.setWakeMode(C.WAKE_MODE_LOCAL); - - DataSpec dataSpec = new DataSpec(Uri.EMPTY, 0, C.LENGTH_UNSET); - - Log.d(TAG, "preset: " + performancePreset); - - DataSource.Factory dataSourceFactory = () -> { - switch (performancePreset.dataSourceType){ - case INPUT_STREAM: - return (DataSource) new InputStreamDataSource(context, dataSpec, inputStream); - case BUFFERED_INPUT_STREAM: - default: - return (DataSource) new InputStreamBufferedDataSource(context, dataSpec, inputStream); - } - }; - - ExtractorsFactory extractorsFactory = () ->new Extractor[] {new H264Extractor(performancePreset.h264ReaderMaxSyncFrameSize, performancePreset.h264ReaderSampleTime)}; - MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory).createMediaSource(MediaItem.fromUri(Uri.EMPTY)); - mPlayer.setMediaSource(mediaSource); - - mPlayer.prepare(); - mPlayer.play(); - mPlayer.addListener(new ExoPlayer.EventListener() { - @Override - @NonNullApi - public void onPlayerError(ExoPlaybackException error) { - switch (error.type) { - case ExoPlaybackException.TYPE_SOURCE: - Log.e(TAG, "PLAYER_SOURCE - TYPE_SOURCE: " + error.getSourceException().getMessage()); - (new Handler(Looper.getMainLooper())).postDelayed(() -> restart(), 1000); - break; - case ExoPlaybackException.TYPE_REMOTE: - Log.e(TAG, "PLAYER_SOURCE - TYPE_REMOTE: " + error.getMessage()); - break; - case ExoPlaybackException.TYPE_RENDERER: - Log.e(TAG, "PLAYER_SOURCE - TYPE_RENDERER: " + error.getRendererException().getMessage()); - break; - case ExoPlaybackException.TYPE_UNEXPECTED: - Log.e(TAG, "PLAYER_SOURCE - TYPE_UNEXPECTED: " + error.getUnexpectedException().getMessage()); - break; - } + DefaultLoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(performancePreset.exoPlayerMinBufferMs, performancePreset.exoPlayerMaxBufferMs, performancePreset.exoPlayerBufferForPlaybackMs, performancePreset.exoPlayerBufferForPlaybackAfterRebufferMs).build(); + mPlayer = new SimpleExoPlayer.Builder(context).setLoadControl(loadControl).build(); + mPlayer.setVideoSurfaceView(surfaceView); + mPlayer.setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING); + mPlayer.setWakeMode(C.WAKE_MODE_LOCAL); + + DataSpec dataSpec = new DataSpec(Uri.EMPTY, 0, C.LENGTH_UNSET); + + Log.d(TAG, "preset: " + performancePreset); + + DataSource.Factory dataSourceFactory = () -> { + switch (performancePreset.dataSourceType) { + case INPUT_STREAM: + return (DataSource) new InputStreamDataSource(dataSpec, inputStream); + case BUFFERED_INPUT_STREAM: + return (DataSource) new InputStreamBufferedDataSource(dataSpec, inputStream); + case VIDEO_STREAM_SERVICE: + default: + VideoStreamServiceDataSource v = new VideoStreamServiceDataSource(dataSpec, mUsbMaskConnection.getVideoStreamService()); + mUsbMaskConnection.start(); + return v; + } + }; + + ExtractorsFactory extractorsFactory = () -> new Extractor[]{new H264Extractor(performancePreset.h264ReaderMaxSyncFrameSize, performancePreset.h264ReaderSampleTime)}; + MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory).createMediaSource(MediaItem.fromUri(Uri.EMPTY)); + mPlayer.setMediaSource(mediaSource); + + mPlayer.prepare(); + mPlayer.play(); + mPlayer.addListener(new Player.Listener() { + @Override + @NonNullApi + public void onPlayerError(ExoPlaybackException error) { + switch (error.type) { + case ExoPlaybackException.TYPE_SOURCE: + Log.e(TAG, "PLAYER_SOURCE - TYPE_SOURCE: " + error.getSourceException().getMessage()); + (new Handler(Looper.getMainLooper())).postDelayed(() -> restart(), 1000); + break; + case ExoPlaybackException.TYPE_REMOTE: + Log.e(TAG, "PLAYER_SOURCE - TYPE_REMOTE: " + error.getMessage()); + break; + case ExoPlaybackException.TYPE_RENDERER: + Log.e(TAG, "PLAYER_SOURCE - TYPE_RENDERER: " + error.getRendererException().getMessage()); + break; + case ExoPlaybackException.TYPE_UNEXPECTED: + Log.e(TAG, "PLAYER_SOURCE - TYPE_UNEXPECTED: " + error.getUnexpectedException().getMessage()); + break; } + } - @Override - public void onPlaybackStateChanged(@NonNullApi int state) { - switch (state) { - case Player.STATE_IDLE: - case Player.STATE_READY: - case Player.STATE_BUFFERING: - break; - case Player.STATE_ENDED: - Log.d(TAG, "PLAYER_STATE - ENDED"); - sendEvent(VideoReaderEventMessageCode.WAITING_FOR_VIDEO); // let MainActivity know so it can hide watermark/show settings button - (new Handler(Looper.getMainLooper())).postDelayed(() -> restart(), 1000); + @Override + public void onPlaybackStateChanged(int state) { + switch (state) { + case Player.STATE_IDLE: + case Player.STATE_READY: + case Player.STATE_BUFFERING: + break; + case Player.STATE_ENDED: + Log.d(TAG, "PLAYER_STATE - ENDED"); + if (videoWaitingListener != null) + videoWaitingListener.onVideoWaiting(); // let MainActivity know so it can hide watermark/show settings button + (new Handler(Looper.getMainLooper())).postDelayed(() -> restart(), 1000); break; } } }); - mPlayer.addVideoListener(new VideoListener() { - @Override - public void onRenderedFirstFrame() { - Log.d(TAG, "PLAYER_RENDER - FIRST FRAME"); - sendEvent(VideoReaderEventMessageCode.VIDEO_PLAYING); // let MainActivity know so it can hide watermark/show settings button - } + mPlayer.addVideoListener(new Player.Listener() { + @Override + public void onRenderedFirstFrame() { + Log.d(TAG, "PLAYER_RENDER - FIRST FRAME"); + if (videoPlayingListener != null) + videoPlayingListener.onVideoPlaying(); // let MainActivity know so it can hide watermark/show settings button + } - @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (!zoomedIn) { - ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) surfaceView.getLayoutParams(); - params.dimensionRatio = width + ":" + height; - surfaceView.setLayoutParams(params); - } + @Override + public void onVideoSizeChanged(@NonNull VideoSize videosize) { + if (!zoomedIn) { + ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) surfaceView.getLayoutParams(); + params.dimensionRatio = videosize.width + ":" + videosize.height; + surfaceView.setLayoutParams(params); } + } }); } - private void sendEvent(VideoReaderEventMessageCode eventCode) { - if (videoReaderEventListener != null) { // let MainActivity know so it can hide watermark/show settings button - Message videoReaderEventMessage = new Message(); - videoReaderEventMessage.obj = eventCode; - videoReaderEventListener.sendMessage(videoReaderEventMessage); - } - } - public void toggleZoom() { zoomedIn = !zoomedIn; @@ -163,12 +164,12 @@ public void toggleZoom() { ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) surfaceView.getLayoutParams(); - if (zoomedIn) { - params.dimensionRatio = ""; - } else { - if (mPlayer == null) return; - Format videoFormat = mPlayer.getVideoFormat(); - if (videoFormat == null) return; + if (zoomedIn) { + params.dimensionRatio = ""; + } else { + if (mPlayer == null) return; + Format videoFormat = mPlayer.getVideoFormat(); + if (videoFormat == null) return; params.dimensionRatio = videoFormat.width + ":" + videoFormat.height; } @@ -202,5 +203,11 @@ public void stop() { mPlayer.release(); } - public enum VideoReaderEventMessageCode {WAITING_FOR_VIDEO, VIDEO_PLAYING} + public interface VideoPlayingListener { + void onVideoPlaying(); + } + + public interface VideoWaitingListener { + void onVideoWaiting(); + } } diff --git a/app/src/main/java/com/fpvout/digiview/VideoStreamService.java b/app/src/main/java/com/fpvout/digiview/VideoStreamService.java new file mode 100644 index 0000000..49795a3 --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/VideoStreamService.java @@ -0,0 +1,82 @@ +package com.fpvout.digiview; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; + +/** + * Consume an inputStream and make it available to all the listeners + */ +public class VideoStreamService { + + private static final int READ_BUFFER_SIZE = 131072; + private static final int MAX_VIDEO_STREAM_LISTENER = 10; + private static final String TAG = "VideoStreamService"; + private final ArrayList videoStreamListeners; + InputStream videoStream; + private final byte[] magicPacket = "RMVT".getBytes(); + private boolean isRunning = false; + private Thread streamServiceThread; + OutputStream outStream; + + public VideoStreamService(InputStream inputStream, OutputStream outputStream) { + videoStream = inputStream; + outStream = outputStream; + videoStreamListeners = new ArrayList<>(); + } + + public void start() { + Log.d(TAG, "streamServiceThread started"); + if (!isRunning) { + isRunning = true; + streamServiceThread = new Thread(() -> { + while (isRunning) { + try { + outStream.write(magicPacket); + byte[] buffer = new byte[READ_BUFFER_SIZE]; + int bytesReceived = videoStream.read(buffer, 0, READ_BUFFER_SIZE); + if (bytesReceived >= 0) { + Log.d(TAG, "bytesReceived : " + bytesReceived); + for (VideoStreamListener v : videoStreamListeners) { + v.onVideoStreamData(buffer, bytesReceived); + } + } else { + outStream.write(magicPacket); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + streamServiceThread.start(); + } + + } + + public void stop() throws InterruptedException { + Log.d(TAG, "streamServiceThread stopped"); + isRunning = false; + streamServiceThread.join(); + } + + public void addVideoStreamListener(VideoStreamListener listener) { + if (videoStreamListeners.size() < MAX_VIDEO_STREAM_LISTENER) { + Log.d(TAG, "addVideoStreamListener"); + videoStreamListeners.add(listener); + } else { + Log.d(TAG, "addVideoStreamListener: Limit reached !"); + } + } + + public void removeVideoStreamListener(VideoStreamListener listener) { + Log.d(TAG, "removeVideoStreamListener"); + videoStreamListeners.remove(listener); + } + + public interface VideoStreamListener { + void onVideoStreamData(byte[] buffer, int bytesReceived); + } +} diff --git a/app/src/main/java/com/fpvout/digiview/VideoStreamServiceDataSource.java b/app/src/main/java/com/fpvout/digiview/VideoStreamServiceDataSource.java new file mode 100644 index 0000000..150026b --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/VideoStreamServiceDataSource.java @@ -0,0 +1,81 @@ +package com.fpvout.digiview; + +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; + +import usb.CircularByteBuffer; + +public class VideoStreamServiceDataSource implements DataSource { + private static final int READ_BUFFER_SIZE = 50 * 1024 * 1024; + private static final long READ_TIMEOUT = 200; + + private final DataSpec dataSpec; + private final CircularByteBuffer readBuffer; + private boolean opened; + + public VideoStreamServiceDataSource(DataSpec dataSpec) { + this.dataSpec = dataSpec; + readBuffer = new CircularByteBuffer(READ_BUFFER_SIZE); + } + + public VideoStreamServiceDataSource(DataSpec dataSpec, VideoStreamService v) { + this.dataSpec = dataSpec; + readBuffer = new CircularByteBuffer(READ_BUFFER_SIZE); + v.addVideoStreamListener(this::onVideoStreamData); + } + + @Override + public void addTransferListener(@NonNull TransferListener transferListener) { + + } + + @Override + public long open(DataSpec dataSpec) { + long bytesRemaining; + + if (dataSpec.length != C.LENGTH_UNSET) { + bytesRemaining = dataSpec.length; + } else { + bytesRemaining = C.LENGTH_UNSET; + } + + opened = true; + return bytesRemaining; + } + + @Override + public int read(@NonNull byte[] buffer, int offset, int readLength) { + long deadLine = System.currentTimeMillis() + READ_TIMEOUT; + int readBytes = 0; + while (System.currentTimeMillis() < deadLine && readBytes <= 0) + readBytes = readBuffer.read(buffer, offset, readLength); + return readBytes; + } + + @Override + public Uri getUri() { + return dataSpec.uri; + } + + @Override + public void close() { + if (opened) { + opened = false; + } + + } + + public void onVideoStreamData(byte[] buffer, int receivedBytes) { + if (receivedBytes > 0) { + readBuffer.write(buffer, 0, receivedBytes); + Log.d("onVideoStream", "onVideoStreamData: " + receivedBytes); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/usb/AndroidUSBInputStream.java b/app/src/main/java/usb/AndroidUSBInputStream.java index 84fdd24..571531d 100644 --- a/app/src/main/java/usb/AndroidUSBInputStream.java +++ b/app/src/main/java/usb/AndroidUSBInputStream.java @@ -1,54 +1,26 @@ -/* - * 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 java.io.IOException; -import java.io.InputStream; - import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; -import android.util.Log; -/** - * This class acts as a wrapper to read data from the USB Interface in Android - * behaving like an {@code InputputStream} class. - */ +import java.io.IOException; +import java.io.InputStream; + public class AndroidUSBInputStream extends InputStream { - private final String TAG = "USBInputStream"; - // Constants. - private static final int OFFSET = 0; private static final int READ_TIMEOUT = 100; - // Variables. - private UsbDeviceConnection usbConnection; - - private UsbEndpoint receiveEndPoint; + private final UsbDeviceConnection usbConnection; + private final UsbEndpoint receiveEndPoint; private final UsbEndpoint sendEndPoint; - private boolean working = false; - - /** * 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 */ @@ -59,17 +31,17 @@ public AndroidUSBInputStream( UsbEndpoint readEndpoint, UsbEndpoint sendEndpoint } @Override - public int read() throws IOException { + public int read() { byte[] buffer = new byte[131072]; - return read(buffer, 0, buffer.length); + return read(buffer, 0, buffer.length); } @Override - public int read(byte[] buffer, int offset, int length) throws IOException { + public int read(byte[] buffer, int offset, int length) { int receivedBytes = usbConnection.bulkTransfer(receiveEndPoint, buffer, buffer.length, READ_TIMEOUT); if (receivedBytes <= 0) { // send magic packet again; Would be great to handle this in UsbMaskConnection directly... - Log.d(TAG, "received buffer empty, sending magic packet again..."); + //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); } @@ -78,6 +50,8 @@ public int read(byte[] buffer, int offset, int length) throws IOException { @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 ba45a36..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; /** @@ -21,7 +6,7 @@ public class CircularByteBuffer { // Variables. - private byte[] buffer; + private final byte[] buffer; private int readIndex; private int writeIndex; @@ -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/values/arrays.xml b/app/src/main/res/values/arrays.xml index fd59273..68eefdd 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -6,6 +6,8 @@ @string/video_preset_aggressive @string/video_preset_legacy @string/video_preset_legacy_buffered + @string/video_preset_video_stream_service + @@ -14,5 +16,6 @@ aggressive legacy legacy_buffered + video_stream_service \ 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..2ee0237 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,5 +40,6 @@ Copyright Open-Source License MIT License + Video Stream Service \ No newline at end of file diff --git a/build.gradle b/build.gradle index 92f3684..8df7cb6 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,10 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath "com.android.tools.build:gradle:4.1.3" + classpath 'com.android.tools.build:gradle:4.2.1' // NOTE: Do not place your application dependencies here; they belong @@ -16,7 +16,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e3b2a6d..a143d9b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip