From 69dc92b3ef51557175e51ae9227acd54c16d1140 Mon Sep 17 00:00:00 2001 From: Olivier Mouren Date: Sun, 30 May 2021 19:09:37 +0200 Subject: [PATCH 01/12] Stream full screen to RTMP --- app/build.gradle | 7 + app/src/main/AndroidManifest.xml | 37 +++-- .../com/fpvout/digiview/MainActivity.java | 100 +++++++++++-- .../com/fpvout/digiview/StreamingService.java | 132 ++++++++++++++++++ app/src/main/res/drawable/streaming_pixel.xml | 10 ++ .../main/res/drawable/streaming_pixel_off.png | Bin 0 -> 124 bytes .../main/res/drawable/streaming_pixel_on.png | Bin 0 -> 124 bytes app/src/main/res/layout/activity_main.xml | 24 ++++ app/src/main/res/values-de/strings.xml | 11 ++ app/src/main/res/values-es/strings.xml | 11 ++ app/src/main/res/values-fr/strings.xml | 11 ++ app/src/main/res/values-pt/strings.xml | 11 ++ app/src/main/res/values-zh/strings.xml | 11 ++ app/src/main/res/values/arrays.xml | 11 +- app/src/main/res/values/strings.xml | 11 ++ app/src/main/res/xml/root_preferences.xml | 50 +++++++ 16 files changed, 415 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/fpvout/digiview/StreamingService.java create mode 100644 app/src/main/res/drawable/streaming_pixel.xml create mode 100644 app/src/main/res/drawable/streaming_pixel_off.png create mode 100644 app/src/main/res/drawable/streaming_pixel_on.png diff --git a/app/build.gradle b/app/build.gradle index de58b67..9a46c28 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -61,6 +61,12 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + allprojects { + repositories { + maven { url 'https://jitpack.io' } + } + } } dependencies { @@ -71,6 +77,7 @@ dependencies { implementation 'com.google.android.exoplayer:exoplayer:2.13.3' implementation 'io.sentry:sentry-android:4.3.0' implementation 'androidx.preference:preference:1.1.1' + implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:2.0.2' 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 7df7ca4..1ecf9a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,7 +4,11 @@ package="com.fpvout.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 +41,26 @@ - - - - + + + + + + { + Intent intent = new Intent(v.getContext(), SettingsActivity.class); + v.getContext().startActivity(intent); + }); + + StreamingService.init(this); + liveButton = findViewById(R.id.liveButton); + liveButton.setOnClickListener(v -> { + if (!StreamingService.isStreaming()) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + startActivityForResult(StreamingService.sendIntent(), MEDIA_PROJECTION_PERMISSION); + } else { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, RECORD_AUDIO_PERMISSION); + } + } else { + stopService(new Intent(this, StreamingService.class)); + this.updateLiveButtonIcon(); } }); @@ -376,7 +403,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); boolean dataCollectionAccepted = preferences.getBoolean("dataCollectionAccepted", false); - if (requestCode == 1) { // Data Collection agreement Activity + if (requestCode == DATA_COLLECTION_AGREEMENT) { // Data Collection agreement Activity if (resultCode == RESULT_OK && dataCollectionAccepted) { SentryAndroid.init(this, options -> options.setBeforeSend((event, hint) -> { if (SentryLevel.DEBUG.equals(event.getLevel())) @@ -385,9 +412,23 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { return event; })); } + } + + if (data != null && (requestCode == MEDIA_PROJECTION_PERMISSION && resultCode == Activity.RESULT_OK)) { + StreamingService.setMediaProjectionData(resultCode, data); + Intent intent = new Intent(this, StreamingService.class); + startService(intent); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == RECORD_AUDIO_PERMISSION && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + startActivityForResult(StreamingService.sendIntent(), MEDIA_PROJECTION_PERMISSION); } - } //onActivityResult + } private void checkDataCollectionAgreement() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); @@ -395,7 +436,7 @@ private void checkDataCollectionAgreement() { boolean dataCollectionReplied = preferences.getBoolean("dataCollectionReplied", false); if (!dataCollectionReplied) { Intent intent = new Intent(this, DataCollectionAgreementPopupActivity.class); - startActivityForResult(intent, 1); + startActivityForResult(intent, DATA_COLLECTION_AGREEMENT); } else if (dataCollectionAccepted) { SentryAndroid.init(this, options -> options.setBeforeSend((event, hint) -> { if (SentryLevel.DEBUG.equals(event.getLevel())) @@ -404,7 +445,46 @@ private void checkDataCollectionAgreement() { return event; })); } + } + private void updateLiveButtonIcon() { + runOnUiThread(() -> { + if (StreamingService.isStreaming()) { + liveButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.exo_icon_stop, this.getTheme())); + } else { + liveButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.exo_icon_play, this.getTheme())); + } + }); + } + + @Override + public void onConnectionSuccessRtmp() { + this.updateLiveButtonIcon(); + } + + @Override + public void onConnectionFailedRtmp(String reason) { + stopService(new Intent(this, StreamingService.class)); + this.updateLiveButtonIcon(); } + @Override + public void onNewBitrateRtmp(long bitrate) { + + } + + @Override + public void onDisconnectRtmp() { + this.updateLiveButtonIcon(); + } + + @Override + public void onAuthErrorRtmp() { + this.updateLiveButtonIcon(); + } + + @Override + public void onAuthSuccessRtmp() { + this.updateLiveButtonIcon(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/fpvout/digiview/StreamingService.java b/app/src/main/java/com/fpvout/digiview/StreamingService.java new file mode 100644 index 0000000..567ba08 --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/StreamingService.java @@ -0,0 +1,132 @@ +package com.fpvout.digiview; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import androidx.preference.PreferenceManager; + +import com.pedro.rtplibrary.base.DisplayBase; +import com.pedro.rtplibrary.rtmp.RtmpDisplay; + +import net.ossrs.rtmp.ConnectCheckerRtmp; + +public class StreamingService extends Service { + private static final String TAG = "Streaming-Service"; + private static NotificationManager notificationManager; + private static SharedPreferences sharedPreferences; + private static final String channelId = "streamingServiceNotification"; + private static int notificationId = 12345; + private static Context appContext; + private static Intent mediaProjectionData; + private static int mediaProjectionResultCode; + private static DisplayBase rtmpDisplayBase; + private String endpoint; + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + Log.e(TAG, "Create"); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH); + notificationManager.createNotificationChannel(channel); + } + keepAliveTrick(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.e(TAG, "Start"); + endpoint = String.format("%s/%s", sharedPreferences.getString("RtmpUrl", null), sharedPreferences.getString("RtmpKey", null)); + + prepareStreaming(); + startStreaming(); + + return START_STICKY; + } + + private void prepareStreaming() { + stopStreaming(); + rtmpDisplayBase = new RtmpDisplay(appContext, true, (ConnectCheckerRtmp) appContext); + rtmpDisplayBase.setIntentResult(mediaProjectionResultCode, mediaProjectionData); + } + + private void startStreaming() { + if (!rtmpDisplayBase.isStreaming()) { + if (rtmpDisplayBase.prepareVideo( + Integer.parseInt(sharedPreferences.getString("OutputWidth", "1280")), + Integer.parseInt(sharedPreferences.getString("OutputHeight", "720")), + Integer.parseInt(sharedPreferences.getString("OutputFramerate", "60")), + Integer.parseInt(sharedPreferences.getString("OutputBitrate", "1200")) * 1024, + 0, + 320 + ) && rtmpDisplayBase.prepareAudio()) { + rtmpDisplayBase.startStream(endpoint); + } + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopStreaming(); + } + + private void keepAliveTrick() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { + Notification notification = new NotificationCompat.Builder(this, channelId) + .setOngoing(true) + .setContentTitle("") + .setContentText("").build(); + startForeground(1, notification); + } else { + startForeground(1, new Notification()); + } + } + + public static boolean isStreaming() { + return rtmpDisplayBase != null && rtmpDisplayBase.isStreaming(); + } + + public static void init(Context context) { + appContext = context; + if (rtmpDisplayBase == null) { + rtmpDisplayBase = new RtmpDisplay(context, true, (ConnectCheckerRtmp) context); + } + } + + public static void setMediaProjectionData(int resultCode, Intent data) { + mediaProjectionResultCode = resultCode; + mediaProjectionData = data; + } + + public static Intent sendIntent() { + if (rtmpDisplayBase != null) { + return rtmpDisplayBase.sendIntent(); + } + + return null; + } + + public static void stopStreaming() { + if (StreamingService.isStreaming()) { + rtmpDisplayBase.stopStream(); + } + } +} diff --git a/app/src/main/res/drawable/streaming_pixel.xml b/app/src/main/res/drawable/streaming_pixel.xml new file mode 100644 index 0000000..8a6bbae --- /dev/null +++ b/app/src/main/res/drawable/streaming_pixel.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/streaming_pixel_off.png b/app/src/main/res/drawable/streaming_pixel_off.png new file mode 100644 index 0000000000000000000000000000000000000000..ab3999581a6321e0a74b56f504ecf1944ae03fe1 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^j3CU&3?x-=hn)gaEa{HEjtmSN`?>!lvVtUwgWR1M z)}51i3FI&b_=LCu>E%_nw?J|wL4Lu^H{W;O0dlxJT^vI=t|un|*-Q+Kj}-S916d57 Lu6{1-oD!M!lvVtUwgWR1M z)}51i3FI&b_=LFr|Nnpa5$O{kxso8iVCI|eJMREFT%InDAsp9}6M$?c2F6E + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8b50f25..5852944 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -40,4 +40,15 @@ Copyright Open-Source Lizenz MIT Lizenz + Streaming + RTMP Url + RTMP Key + Record audio + Audio source + Default + Internal audio + Output Width + Output Height + Output Framerate + Output Bitrate \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c6ccf2e..c5bc988 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -39,4 +39,15 @@ Copyright Licencia Open-Source Licencia MIT + Streaming + RTMP Url + RTMP Key + Record audio + Audio source + Default + Internal audio + Output Width + Output Height + Output Framerate + Output Bitrate \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ccb60bf..2811748 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -37,4 +37,15 @@ Copyright License Open-Source License MIT + Streaming + RTMP Url + RTMP Key + Enregistrer le son + Source audio + Micro par défaut + Audio interne + Output Width + Output Height + Output Framerate + Output Bitrate \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b2b9365..6139451 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -39,5 +39,16 @@ Direitos Autorais Licença Open-Source Licença MIT + Streaming + RTMP Url + RTMP Key + Record audio + Audio source + Default + Internal audio + Output Width + Output Height + Output Framerate + Output Bitrate \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 3d53462..d99efc2 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -39,5 +39,16 @@ 版权所有 开源证书 MIT License + Streaming + RTMP Url + RTMP Key + Record audio + Audio source + Default + Internal audio + Output Width + Output Height + Output Framerate + Output Bitrate diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index fd59273..f8a13de 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1,5 +1,4 @@ - @string/video_preset_default @string/video_preset_conservative @@ -15,4 +14,14 @@ legacy legacy_buffered + + + @string/audio_source_default + @string/audio_source_internal + + + + default + internal + \ 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..7291c35 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,5 +40,16 @@ Copyright Open-Source License MIT License + Streaming + RTMP Url + RTMP Key + Record audio + Audio source + Default Mic + Internal audio + Output Width + Output Height + Output Framerate + Output Bitrate \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index aafa7b5..fd57ab9 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -47,6 +47,56 @@ + + + + + + + + + + + + + + + + + + + + Date: Sun, 30 May 2021 22:15:04 +0200 Subject: [PATCH 02/12] Better overlay toggle --- .../com/fpvout/digiview/MainActivity.java | 186 +++++++++--------- .../java/com/fpvout/digiview/OverlayView.java | 8 - app/src/main/res/layout/activity_main.xml | 18 +- 3 files changed, 99 insertions(+), 113 deletions(-) diff --git a/app/src/main/java/com/fpvout/digiview/MainActivity.java b/app/src/main/java/com/fpvout/digiview/MainActivity.java index bc83e39..9820986 100644 --- a/app/src/main/java/com/fpvout/digiview/MainActivity.java +++ b/app/src/main/java/com/fpvout/digiview/MainActivity.java @@ -25,6 +25,7 @@ import android.view.WindowManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; @@ -41,8 +42,6 @@ import io.sentry.SentryLevel; import io.sentry.android.core.SentryAndroid; -import static com.fpvout.digiview.VideoReaderExoplayer.VideoZoomedIn; - public class MainActivity extends AppCompatActivity implements UsbDeviceListener, ConnectCheckerRtmp, ActivityCompat.OnRequestPermissionsResultCallback { private static final String TAG = "DIGIVIEW"; private static final String ACTION_USB_PERMISSION = "com.fpvout.digiview.USB_PERMISSION"; @@ -69,6 +68,18 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener private ScaleGestureDetector scaleGestureDetector; private SharedPreferences sharedPreferences; private static final String ShowWatermark = "ShowWatermark"; + private Runnable hideSettingsButtonRunnable = new Runnable() { + @Override + public void run() { + toggleView(settingsButton, false); + } + }; + private Runnable hideLiveButtonRunnable = new Runnable() { + @Override + public void run() { + toggleView(liveButton, false); + } + }; @Override protected void onCreate(Bundle savedInstanceState) { @@ -151,11 +162,84 @@ protected void onCreate(Bundle savedInstanceState) { } } + private void toggleFullOverlay() { + if (overlayView.getAlpha() > 0.0f) return; + + if (sharedPreferences.getBoolean(ShowWatermark, true)) { + toggleView(watermarkView, 0.3f); + } + + settingsButton.removeCallbacks(hideSettingsButtonRunnable); + toggleView(settingsButton, new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + settingsButton.postDelayed(hideSettingsButtonRunnable, 3000); + } + }); + liveButton.removeCallbacks(hideLiveButtonRunnable); + toggleView(liveButton, new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + liveButton.postDelayed(hideLiveButtonRunnable, 3000); + } + }); + } + + private void hideFullOverlay() { + toggleView(watermarkView, sharedPreferences.getBoolean(ShowWatermark, true), 0.3f); + + toggleView(settingsButton, false); + toggleView(liveButton, false); + toggleView(overlayView, false); + } + + private void showFullOverlay() { + toggleView(watermarkView, false); + + settingsButton.removeCallbacks(hideSettingsButtonRunnable); + toggleView(settingsButton, true); + + liveButton.removeCallbacks(hideLiveButtonRunnable); + toggleView(liveButton, true); + } + + private void toggleView(View view, @Nullable AnimatorListenerAdapter animatorListener) { + toggleView(view, view.getAlpha() == 0.0f, 1.0f, animatorListener); + } + + private void toggleView(View view, float visibleAlpha) { + toggleView(view, view.getAlpha() == 0.0f, visibleAlpha); + } + + private void toggleView(View view, boolean visible) { + toggleView(view, visible, 1.0f, null); + } + + private void toggleView(View view, boolean visible, float visibleAlpha) { + toggleView(view, visible, visibleAlpha, null); + } + + private void toggleView(View view, boolean visible, float visibleAlpha, @Nullable AnimatorListenerAdapter animatorListener) { + if (!visible) { + view.animate().cancel(); + view.animate() + .alpha(0) + .setDuration(shortAnimationDuration) + .setListener(null); + } else { + view.animate().cancel(); + view.animate() + .alpha(visibleAlpha) + .setDuration(shortAnimationDuration) + .setListener(animatorListener); + } + } + private void setupGestureDetectors() { gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onSingleTapConfirmed(MotionEvent e) { - toggleSettingsButton(); + toggleFullOverlay(); return super.onSingleTapConfirmed(e); } @@ -186,81 +270,6 @@ public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } - private void updateWatermark() { - if (overlayView.getVisibility() == View.VISIBLE) { - watermarkView.setAlpha(0); - return; - } - - if (sharedPreferences.getBoolean(ShowWatermark, true)) { - watermarkView.setAlpha(0.3F); - } else { - watermarkView.setAlpha(0F); - } - } - - private void updateVideoZoom() { - if (sharedPreferences.getBoolean(VideoZoomedIn, true)) { - mVideoReader.zoomIn(); - } else { - mVideoReader.zoomOut(); - } - } - - private void cancelButtonAnimation() { - Handler handler = settingsButton.getHandler(); - if (handler != null) { - settingsButton.getHandler().removeCallbacksAndMessages(null); - } - } - - 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; - - // cancel any pending delayed animations first - cancelButtonAnimation(); - - if (buttonAlpha == 1) { - buttonAlpha = 0; - } else { - buttonAlpha = 1; - } - - settingsButton.animate() - .alpha(buttonAlpha) - .setDuration(shortAnimationDuration) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - autoHideSettingsButton(); - } - }); - } - - 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); - } - }, 3000); - } - @Override public void usbDeviceApproved(UsbDevice device) { Log.i(TAG, "USB - usbDevice approved"); @@ -303,10 +312,7 @@ private void connect() { usbConnected = true; mUsbMaskConnection.setUsbDevice(usbManager.openDevice(usbDevice), usbDevice); mVideoReader.setUsbMaskConnection(mUsbMaskConnection); - overlayView.hide(); mVideoReader.start(); - updateWatermark(); - autoHideSettingsButton(); showOverlay(R.string.waiting_for_video, OverlayStatus.Connected); } @@ -335,11 +341,6 @@ public void onResume() { showOverlay(R.string.waiting_for_usb_device, OverlayStatus.Connected); } } - - settingsButton.setAlpha(1); - autoHideSettingsButton(); - updateWatermark(); - updateVideoZoom(); } private boolean onVideoReaderEvent(VideoReaderExoplayer.VideoReaderEventMessageCode m) { @@ -348,22 +349,15 @@ private boolean onVideoReaderEvent(VideoReaderExoplayer.VideoReaderEventMessageC showOverlay(R.string.waiting_for_video, OverlayStatus.Connected); } else if (VideoReaderExoplayer.VideoReaderEventMessageCode.VIDEO_PLAYING.equals(m)) { Log.d(TAG, "event: VIDEO_PLAYING"); - hideOverlay(); + hideFullOverlay(); } return false; // false to continue listening } private void showOverlay(int textId, OverlayStatus connected) { + toggleView(overlayView, true); + showFullOverlay(); overlayView.show(textId, connected); - updateWatermark(); - showSettingsButton(); - } - - private void hideOverlay() { - overlayView.hide(); - updateWatermark(); - showSettingsButton(); - autoHideSettingsButton(); } @Override diff --git a/app/src/main/java/com/fpvout/digiview/OverlayView.java b/app/src/main/java/com/fpvout/digiview/OverlayView.java index b1327c8..8bbc442 100644 --- a/app/src/main/java/com/fpvout/digiview/OverlayView.java +++ b/app/src/main/java/com/fpvout/digiview/OverlayView.java @@ -2,7 +2,6 @@ import android.content.Context; import android.util.AttributeSet; -import android.view.View; import android.widget.ImageView; import android.widget.TextView; @@ -22,18 +21,11 @@ public OverlayView(Context context, AttributeSet attrs) { imageView = findViewById(R.id.backdrop_image); } - public void hide(){ - setVisibility(View.GONE); - } - public void show(int textResourceId, OverlayStatus status){ showInfo(getContext().getString(textResourceId), status); } private void showInfo(String text, OverlayStatus status){ - - setVisibility(View.VISIBLE); - textView.setText(text); int image = R.drawable.ic_goggles_white; diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 55aca97..e48e6c4 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -19,6 +19,15 @@ app:layout_constraintTop_toTopOf="parent"> + + - - Date: Tue, 1 Jun 2021 18:54:06 +0200 Subject: [PATCH 03/12] Add internal audio streaming + Mute audio button --- .../com/fpvout/digiview/MainActivity.java | 15 +++++++ .../com/fpvout/digiview/SettingsActivity.java | 18 +++++++++ .../com/fpvout/digiview/StreamingService.java | 39 +++++++++++++++++-- .../drawable/ic_microphone_slash_solid.xml | 9 +++++ .../main/res/drawable/ic_microphone_solid.xml | 12 ++++++ app/src/main/res/layout/activity_main.xml | 12 ++++++ app/src/main/res/values-de/strings.xml | 4 ++ app/src/main/res/values-es/strings.xml | 4 ++ app/src/main/res/values-fr/strings.xml | 6 ++- app/src/main/res/values-pt/strings.xml | 4 ++ app/src/main/res/values-zh/strings.xml | 4 ++ app/src/main/res/values/arrays.xml | 12 ++++-- app/src/main/res/values/strings.xml | 6 ++- app/src/main/res/xml/root_preferences.xml | 2 +- 14 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 app/src/main/res/drawable/ic_microphone_slash_solid.xml create mode 100644 app/src/main/res/drawable/ic_microphone_solid.xml diff --git a/app/src/main/java/com/fpvout/digiview/MainActivity.java b/app/src/main/java/com/fpvout/digiview/MainActivity.java index 9820986..0637cba 100644 --- a/app/src/main/java/com/fpvout/digiview/MainActivity.java +++ b/app/src/main/java/com/fpvout/digiview/MainActivity.java @@ -54,6 +54,7 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener private float buttonAlpha = 1; private View settingsButton; private FloatingActionButton liveButton; + private FloatingActionButton muteButton; private View watermarkView; private OverlayView overlayView; PendingIntent permissionIntent; @@ -127,6 +128,12 @@ protected void onCreate(Bundle savedInstanceState) { }); StreamingService.init(this); + muteButton = findViewById(R.id.muteButton); + muteButton.setOnClickListener(v -> { + StreamingService.toggleMute(); + updateLiveButtonIcon(); + }); + liveButton = findViewById(R.id.liveButton); liveButton.setOnClickListener(v -> { if (!StreamingService.isStreaming()) { @@ -444,8 +451,16 @@ private void checkDataCollectionAgreement() { private void updateLiveButtonIcon() { runOnUiThread(() -> { if (StreamingService.isStreaming()) { + if (StreamingService.isMuted()) { + muteButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_microphone_slash_solid, this.getTheme())); + } else { + muteButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_microphone_solid, this.getTheme())); + } + + toggleView(muteButton, true); liveButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.exo_icon_stop, this.getTheme())); } else { + toggleView(muteButton, false); liveButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.exo_icon_play, this.getTheme())); } }); diff --git a/app/src/main/java/com/fpvout/digiview/SettingsActivity.java b/app/src/main/java/com/fpvout/digiview/SettingsActivity.java index 1b552a8..1cf20f9 100644 --- a/app/src/main/java/com/fpvout/digiview/SettingsActivity.java +++ b/app/src/main/java/com/fpvout/digiview/SettingsActivity.java @@ -1,13 +1,18 @@ package com.fpvout.digiview; +import android.os.Build; import android.os.Bundle; import android.view.MenuItem; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.ListPreference; import androidx.preference.PreferenceFragmentCompat; +import java.util.ArrayList; +import java.util.Arrays; + public class SettingsActivity extends AppCompatActivity { @Override @@ -41,6 +46,19 @@ public static class SettingsFragment extends PreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.root_preferences, rootKey); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ListPreference audioSourcePreference = findPreference("AudioSource"); + + ArrayList entries = new ArrayList<>(Arrays.asList(audioSourcePreference.getEntries())); + ArrayList entryValues = new ArrayList<>(Arrays.asList(audioSourcePreference.getEntryValues())); + + entries.add(getString(R.string.audio_source_internal)); + entryValues.add("internal"); + + audioSourcePreference.setEntries(entries.toArray(new CharSequence[0])); + audioSourcePreference.setEntryValues(entryValues.toArray(new CharSequence[0])); + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/fpvout/digiview/StreamingService.java b/app/src/main/java/com/fpvout/digiview/StreamingService.java index 567ba08..693a237 100644 --- a/app/src/main/java/com/fpvout/digiview/StreamingService.java +++ b/app/src/main/java/com/fpvout/digiview/StreamingService.java @@ -9,6 +9,7 @@ import android.content.SharedPreferences; import android.os.Build; import android.os.IBinder; +import android.util.DisplayMetrics; import android.util.Log; import androidx.annotation.Nullable; @@ -30,6 +31,7 @@ public class StreamingService extends Service { private static Intent mediaProjectionData; private static int mediaProjectionResultCode; private static DisplayBase rtmpDisplayBase; + private static int dpi; private String endpoint; @Nullable @Override @@ -75,9 +77,24 @@ private void startStreaming() { Integer.parseInt(sharedPreferences.getString("OutputFramerate", "60")), Integer.parseInt(sharedPreferences.getString("OutputBitrate", "1200")) * 1024, 0, - 320 - ) && rtmpDisplayBase.prepareAudio()) { - rtmpDisplayBase.startStream(endpoint); + dpi + )) { + boolean audioInitialized; + if (sharedPreferences.getString("AudioSource", "0").equals("internal") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + audioInitialized = rtmpDisplayBase.prepareInternalAudio(64 * 1024, 32000, true, false, false); + } else { + audioInitialized = rtmpDisplayBase.prepareAudio(Integer.parseInt(sharedPreferences.getString("AudioSource", "0")), 64 * 1024, 32000, false, false, false); + } + + if (audioInitialized) { + if (!sharedPreferences.getBoolean("RecordAudio", true)) { + rtmpDisplayBase.disableAudio(); + } else { + rtmpDisplayBase.enableAudio(); + } + + rtmpDisplayBase.startStream(endpoint); + } } } } @@ -104,8 +121,24 @@ public static boolean isStreaming() { return rtmpDisplayBase != null && rtmpDisplayBase.isStreaming(); } + public static boolean isMuted() { + return rtmpDisplayBase != null && rtmpDisplayBase.isAudioMuted(); + } + + public static void toggleMute() { + if (isStreaming()) { + if (rtmpDisplayBase.isAudioMuted()) { + rtmpDisplayBase.enableAudio(); + } else { + rtmpDisplayBase.disableAudio(); + } + } + } + public static void init(Context context) { appContext = context; + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + dpi = dm.densityDpi; if (rtmpDisplayBase == null) { rtmpDisplayBase = new RtmpDisplay(context, true, (ConnectCheckerRtmp) context); } diff --git a/app/src/main/res/drawable/ic_microphone_slash_solid.xml b/app/src/main/res/drawable/ic_microphone_slash_solid.xml new file mode 100644 index 0000000..f3a98bc --- /dev/null +++ b/app/src/main/res/drawable/ic_microphone_slash_solid.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_microphone_solid.xml b/app/src/main/res/drawable/ic_microphone_solid.xml new file mode 100644 index 0000000..933783d --- /dev/null +++ b/app/src/main/res/drawable/ic_microphone_solid.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index e48e6c4..a3e0aad 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -64,6 +64,18 @@ app:layout_constraintEnd_toEndOf="parent" app:srcCompat="@drawable/exo_icon_play" /> + + Record audio Audio source Default + Mic + Camera + Communication + Performance Internal audio Output Width Output Height diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c5bc988..664a06f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -45,6 +45,10 @@ Record audio Audio source Default + Mic + Camera + Communication + Performance Internal audio Output Width Output Height diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2811748..fee4825 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -42,7 +42,11 @@ RTMP Key Enregistrer le son Source audio - Micro par défaut + Par défaut + Mic + Camera + Communication + Performance Audio interne Output Width Output Height diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 6139451..c03acd2 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -45,6 +45,10 @@ Record audio Audio source Default + Mic + Camera + Communication + Performance Internal audio Output Width Output Height diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index d99efc2..cb07c59 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -45,6 +45,10 @@ Record audio Audio source Default + Mic + Camera + Communication + Performance Internal audio Output Width Output Height diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index f8a13de..c91eeee 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -17,11 +17,17 @@ @string/audio_source_default - @string/audio_source_internal + @string/audio_source_mic + @string/audio_source_cam + @string/audio_source_communication + @string/audio_source_performance - default - internal + 0 + 1 + 5 + 7 + 10 \ 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 7291c35..143f343 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,7 +45,11 @@ RTMP Key Record audio Audio source - Default Mic + Default + Mic + Camera + Communication + Performance Internal audio Output Width Output Height diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index fd57ab9..b46ee31 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -88,7 +88,7 @@ app:defaultValue="true" /> Date: Tue, 1 Jun 2021 19:43:00 +0200 Subject: [PATCH 04/12] Move connect checker rtmp to local var + handle mute button hiding --- .../com/fpvout/digiview/MainActivity.java | 89 ++++++++++--------- .../com/fpvout/digiview/StreamingService.java | 12 ++- app/src/main/res/layout/activity_main.xml | 44 +++++---- 3 files changed, 82 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/fpvout/digiview/MainActivity.java b/app/src/main/java/com/fpvout/digiview/MainActivity.java index 0637cba..dde58ce 100644 --- a/app/src/main/java/com/fpvout/digiview/MainActivity.java +++ b/app/src/main/java/com/fpvout/digiview/MainActivity.java @@ -28,6 +28,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; @@ -42,7 +43,7 @@ import io.sentry.SentryLevel; import io.sentry.android.core.SentryAndroid; -public class MainActivity extends AppCompatActivity implements UsbDeviceListener, ConnectCheckerRtmp, ActivityCompat.OnRequestPermissionsResultCallback { +public class MainActivity extends AppCompatActivity implements UsbDeviceListener, ActivityCompat.OnRequestPermissionsResultCallback { private static final String TAG = "DIGIVIEW"; private static final String ACTION_USB_PERMISSION = "com.fpvout.digiview.USB_PERMISSION"; private static final int DATA_COLLECTION_AGREEMENT = 1; @@ -53,6 +54,7 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener private int shortAnimationDuration; private float buttonAlpha = 1; private View settingsButton; + private ConstraintLayout liveButtons; private FloatingActionButton liveButton; private FloatingActionButton muteButton; private View watermarkView; @@ -69,16 +71,48 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener private ScaleGestureDetector scaleGestureDetector; private SharedPreferences sharedPreferences; private static final String ShowWatermark = "ShowWatermark"; - private Runnable hideSettingsButtonRunnable = new Runnable() { + private final Runnable hideSettingsButtonRunnable = new Runnable() { @Override public void run() { toggleView(settingsButton, false); } }; - private Runnable hideLiveButtonRunnable = new Runnable() { + private final Runnable hideLiveButtonRunnable = new Runnable() { @Override public void run() { - toggleView(liveButton, false); + toggleView(liveButtons, false); + } + }; + private final ConnectCheckerRtmp connectChecker = new ConnectCheckerRtmp() { + @Override + public void onConnectionSuccessRtmp() { + updateLiveButtonIcon(); + } + + @Override + public void onConnectionFailedRtmp(String reason) { + stopService(new Intent(getApplicationContext(), StreamingService.class)); + updateLiveButtonIcon(); + } + + @Override + public void onNewBitrateRtmp(long bitrate) { + + } + + @Override + public void onDisconnectRtmp() { + updateLiveButtonIcon(); + } + + @Override + public void onAuthErrorRtmp() { + updateLiveButtonIcon(); + } + + @Override + public void onAuthSuccessRtmp() { + updateLiveButtonIcon(); } }; @@ -127,7 +161,9 @@ protected void onCreate(Bundle savedInstanceState) { v.getContext().startActivity(intent); }); - StreamingService.init(this); + liveButtons = findViewById(R.id.liveButtons); + StreamingService.init(this, connectChecker); + muteButton = findViewById(R.id.muteButton); muteButton.setOnClickListener(v -> { StreamingService.toggleMute(); @@ -183,11 +219,11 @@ public void onAnimationEnd(Animator animation) { settingsButton.postDelayed(hideSettingsButtonRunnable, 3000); } }); - liveButton.removeCallbacks(hideLiveButtonRunnable); - toggleView(liveButton, new AnimatorListenerAdapter() { + liveButtons.removeCallbacks(hideLiveButtonRunnable); + toggleView(liveButtons, new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - liveButton.postDelayed(hideLiveButtonRunnable, 3000); + liveButtons.postDelayed(hideLiveButtonRunnable, 3000); } }); } @@ -196,7 +232,7 @@ private void hideFullOverlay() { toggleView(watermarkView, sharedPreferences.getBoolean(ShowWatermark, true), 0.3f); toggleView(settingsButton, false); - toggleView(liveButton, false); + toggleView(liveButtons, false); toggleView(overlayView, false); } @@ -206,8 +242,8 @@ private void showFullOverlay() { settingsButton.removeCallbacks(hideSettingsButtonRunnable); toggleView(settingsButton, true); - liveButton.removeCallbacks(hideLiveButtonRunnable); - toggleView(liveButton, true); + liveButtons.removeCallbacks(hideLiveButtonRunnable); + toggleView(liveButtons, true); } private void toggleView(View view, @Nullable AnimatorListenerAdapter animatorListener) { @@ -465,35 +501,4 @@ private void updateLiveButtonIcon() { } }); } - - @Override - public void onConnectionSuccessRtmp() { - this.updateLiveButtonIcon(); - } - - @Override - public void onConnectionFailedRtmp(String reason) { - stopService(new Intent(this, StreamingService.class)); - this.updateLiveButtonIcon(); - } - - @Override - public void onNewBitrateRtmp(long bitrate) { - - } - - @Override - public void onDisconnectRtmp() { - this.updateLiveButtonIcon(); - } - - @Override - public void onAuthErrorRtmp() { - this.updateLiveButtonIcon(); - } - - @Override - public void onAuthSuccessRtmp() { - this.updateLiveButtonIcon(); - } } \ No newline at end of file diff --git a/app/src/main/java/com/fpvout/digiview/StreamingService.java b/app/src/main/java/com/fpvout/digiview/StreamingService.java index 693a237..5f0be3f 100644 --- a/app/src/main/java/com/fpvout/digiview/StreamingService.java +++ b/app/src/main/java/com/fpvout/digiview/StreamingService.java @@ -26,8 +26,8 @@ public class StreamingService extends Service { private static NotificationManager notificationManager; private static SharedPreferences sharedPreferences; private static final String channelId = "streamingServiceNotification"; - private static int notificationId = 12345; private static Context appContext; + private static ConnectCheckerRtmp connectChecker; private static Intent mediaProjectionData; private static int mediaProjectionResultCode; private static DisplayBase rtmpDisplayBase; @@ -65,7 +65,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { private void prepareStreaming() { stopStreaming(); - rtmpDisplayBase = new RtmpDisplay(appContext, true, (ConnectCheckerRtmp) appContext); + rtmpDisplayBase = new RtmpDisplay(appContext, true, connectChecker); rtmpDisplayBase.setIntentResult(mediaProjectionResultCode, mediaProjectionData); } @@ -79,6 +79,7 @@ private void startStreaming() { 0, dpi )) { + Log.i(TAG, String.valueOf(Integer.parseInt(sharedPreferences.getString("OutputFramerate", "60")))); boolean audioInitialized; if (sharedPreferences.getString("AudioSource", "0").equals("internal") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { audioInitialized = rtmpDisplayBase.prepareInternalAudio(64 * 1024, 32000, true, false, false); @@ -135,12 +136,15 @@ public static void toggleMute() { } } - public static void init(Context context) { + public static void init(Context context, ConnectCheckerRtmp connectCheckerRtmp) { appContext = context; + connectChecker = connectCheckerRtmp; + DisplayMetrics dm = context.getResources().getDisplayMetrics(); dpi = dm.densityDpi; + if (rtmpDisplayBase == null) { - rtmpDisplayBase = new RtmpDisplay(context, true, (ConnectCheckerRtmp) context); + rtmpDisplayBase = new RtmpDisplay(appContext, true, connectChecker); } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a3e0aad..81a6d64 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -53,28 +53,38 @@ app:layout_constraintEnd_toEndOf="parent" app:srcCompat="@drawable/exo_ic_settings" /> - + android:clipChildren="false" + android:clipToPadding="false"> - + + + + Date: Tue, 1 Jun 2021 21:21:04 +0200 Subject: [PATCH 05/12] Add bitrate stat, add RTMP errors in Toast --- .../com/fpvout/digiview/MainActivity.java | 27 +++++++++++++++---- .../com/fpvout/digiview/StreamingService.java | 6 ++--- app/src/main/res/layout/activity_main.xml | 12 +++++++++ app/src/main/res/values-de/strings.xml | 3 +++ app/src/main/res/values-es/strings.xml | 3 +++ app/src/main/res/values-fr/strings.xml | 3 +++ app/src/main/res/values-pt/strings.xml | 3 +++ app/src/main/res/values-zh/strings.xml | 3 +++ app/src/main/res/values/strings.xml | 3 +++ 9 files changed, 54 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/fpvout/digiview/MainActivity.java b/app/src/main/java/com/fpvout/digiview/MainActivity.java index dde58ce..4566bb9 100644 --- a/app/src/main/java/com/fpvout/digiview/MainActivity.java +++ b/app/src/main/java/com/fpvout/digiview/MainActivity.java @@ -23,6 +23,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -52,11 +54,11 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener 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 ConstraintLayout liveButtons; private FloatingActionButton liveButton; private FloatingActionButton muteButton; + private TextView bitrateTextview; private View watermarkView; private OverlayView overlayView; PendingIntent permissionIntent; @@ -93,11 +95,16 @@ public void onConnectionSuccessRtmp() { public void onConnectionFailedRtmp(String reason) { stopService(new Intent(getApplicationContext(), StreamingService.class)); updateLiveButtonIcon(); + runOnUiThread(() -> { + Toast.makeText(getApplicationContext(), getString(R.string.rtmp_connection_failed), Toast.LENGTH_SHORT).show(); + }); } @Override public void onNewBitrateRtmp(long bitrate) { - + runOnUiThread(() -> { + bitrateTextview.setText(String.format("%s kbps", bitrate / 1024)); + }); } @Override @@ -108,6 +115,9 @@ public void onDisconnectRtmp() { @Override public void onAuthErrorRtmp() { updateLiveButtonIcon(); + runOnUiThread(() -> { + Toast.makeText(getApplicationContext(), getString(R.string.rtmp_auth_error), Toast.LENGTH_SHORT).show(); + }); } @Override @@ -162,6 +172,7 @@ protected void onCreate(Bundle savedInstanceState) { }); liveButtons = findViewById(R.id.liveButtons); + bitrateTextview = findViewById(R.id.bitrateText); StreamingService.init(this, connectChecker); muteButton = findViewById(R.id.muteButton); @@ -173,10 +184,16 @@ protected void onCreate(Bundle savedInstanceState) { liveButton = findViewById(R.id.liveButton); liveButton.setOnClickListener(v -> { if (!StreamingService.isStreaming()) { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { - startActivityForResult(StreamingService.sendIntent(), MEDIA_PROJECTION_PERMISSION); + if (sharedPreferences.getString("RtmpUrl", "").isEmpty() || sharedPreferences.getString("RtmpKey", "").isEmpty()) { + Toast.makeText(this, getString(R.string.rtmp_settings_empty), Toast.LENGTH_LONG).show(); + Intent intent = new Intent(v.getContext(), SettingsActivity.class); + v.getContext().startActivity(intent); } else { - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, RECORD_AUDIO_PERMISSION); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + startActivityForResult(StreamingService.sendIntent(), MEDIA_PROJECTION_PERMISSION); + } else { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, RECORD_AUDIO_PERMISSION); + } } } else { stopService(new Intent(this, StreamingService.class)); diff --git a/app/src/main/java/com/fpvout/digiview/StreamingService.java b/app/src/main/java/com/fpvout/digiview/StreamingService.java index 5f0be3f..4c723df 100644 --- a/app/src/main/java/com/fpvout/digiview/StreamingService.java +++ b/app/src/main/java/com/fpvout/digiview/StreamingService.java @@ -16,7 +16,6 @@ import androidx.core.app.NotificationCompat; import androidx.preference.PreferenceManager; -import com.pedro.rtplibrary.base.DisplayBase; import com.pedro.rtplibrary.rtmp.RtmpDisplay; import net.ossrs.rtmp.ConnectCheckerRtmp; @@ -30,7 +29,7 @@ public class StreamingService extends Service { private static ConnectCheckerRtmp connectChecker; private static Intent mediaProjectionData; private static int mediaProjectionResultCode; - private static DisplayBase rtmpDisplayBase; + private static RtmpDisplay rtmpDisplayBase; private static int dpi; private String endpoint; @Nullable @@ -55,8 +54,7 @@ public void onCreate() { @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.e(TAG, "Start"); - endpoint = String.format("%s/%s", sharedPreferences.getString("RtmpUrl", null), sharedPreferences.getString("RtmpKey", null)); - + endpoint = String.format("%s/%s", sharedPreferences.getString("RtmpUrl", ""), sharedPreferences.getString("RtmpKey", "")); prepareStreaming(); startStreaming(); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 81a6d64..0b6f000 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -84,6 +84,18 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/liveButton" app:srcCompat="@drawable/ic_microphone_solid"/> + + Output Height Output Framerate Output Bitrate + Failed to connect to RTMP server + RTMP authentication failed + Please, check your streaming RTMP URL and Key \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 664a06f..11bfad1 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -54,4 +54,7 @@ Output Height Output Framerate Output Bitrate + Failed to connect to RTMP server + RTMP authentication failed + Please, check your streaming RTMP URL and Key \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fee4825..99eeab9 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -52,4 +52,7 @@ Output Height Output Framerate Output Bitrate + Failed to connect to RTMP server + RTMP authentication failed + Please, check your streaming RTMP URL and Key \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index c03acd2..08ff2a3 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -54,5 +54,8 @@ Output Height Output Framerate Output Bitrate + Failed to connect to RTMP server + RTMP authentication failed + Please, check your streaming RTMP URL and Key \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index cb07c59..2f8deee 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -54,5 +54,8 @@ Output Height Output Framerate Output Bitrate + Failed to connect to RTMP server + RTMP authentication failed + Please, check your streaming RTMP URL and Key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 143f343..b8729c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,5 +55,8 @@ Output Height Output Framerate Output Bitrate + Failed to connect to RTMP server + RTMP authentication failed + Please, check your streaming RTMP URL and Key \ No newline at end of file From 2692be01c974474800ceaf84b53a1ea0264ed4d0 Mon Sep 17 00:00:00 2001 From: Olivier Mouren Date: Tue, 1 Jun 2021 21:30:08 +0200 Subject: [PATCH 06/12] Handle RTMP basic auth --- .../main/java/com/fpvout/digiview/StreamingService.java | 6 ++++++ app/src/main/res/values-de/strings.xml | 2 ++ app/src/main/res/values-es/strings.xml | 2 ++ app/src/main/res/values-fr/strings.xml | 2 ++ app/src/main/res/values-pt/strings.xml | 2 ++ app/src/main/res/values-zh/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/root_preferences.xml | 9 +++++++++ 8 files changed, 27 insertions(+) diff --git a/app/src/main/java/com/fpvout/digiview/StreamingService.java b/app/src/main/java/com/fpvout/digiview/StreamingService.java index 4c723df..c969039 100644 --- a/app/src/main/java/com/fpvout/digiview/StreamingService.java +++ b/app/src/main/java/com/fpvout/digiview/StreamingService.java @@ -64,6 +64,9 @@ public int onStartCommand(Intent intent, int flags, int startId) { private void prepareStreaming() { stopStreaming(); rtmpDisplayBase = new RtmpDisplay(appContext, true, connectChecker); + if (!sharedPreferences.getString("RtmpUsername", "").isEmpty() && !sharedPreferences.getString("RtmpPassword", "").isEmpty()) { + rtmpDisplayBase.setAuthorization(sharedPreferences.getString("RtmpUsername", ""), sharedPreferences.getString("RtmpPassword", "")); + } rtmpDisplayBase.setIntentResult(mediaProjectionResultCode, mediaProjectionData); } @@ -143,6 +146,9 @@ public static void init(Context context, ConnectCheckerRtmp connectCheckerRtmp) if (rtmpDisplayBase == null) { rtmpDisplayBase = new RtmpDisplay(appContext, true, connectChecker); + if (!sharedPreferences.getString("RtmpUsername", "").isEmpty() && !sharedPreferences.getString("RtmpPassword", "").isEmpty()) { + rtmpDisplayBase.setAuthorization(sharedPreferences.getString("RtmpUsername", ""), sharedPreferences.getString("RtmpPassword", "")); + } } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 140f818..583fa19 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -43,6 +43,8 @@ Streaming RTMP Url RTMP Key + RTMP Username + RTMP Password Record audio Audio source Default diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 11bfad1..05ef891 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -42,6 +42,8 @@ Streaming RTMP Url RTMP Key + RTMP Username + RTMP Password Record audio Audio source Default diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 99eeab9..82c3e4a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -40,6 +40,8 @@ Streaming RTMP Url RTMP Key + RTMP Username + RTMP Password Enregistrer le son Source audio Par défaut diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 08ff2a3..5f54249 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -42,6 +42,8 @@ Streaming RTMP Url RTMP Key + RTMP Username + RTMP Password Record audio Audio source Default diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2f8deee..08ebbfc 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -42,6 +42,8 @@ Streaming RTMP Url RTMP Key + RTMP Username + RTMP Password Record audio Audio source Default diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b8729c7..6cecd38 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,6 +43,8 @@ Streaming RTMP Url RTMP Key + RTMP Username + RTMP Password Record audio Audio source Default diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index b46ee31..ce90623 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -58,6 +58,15 @@ app:key="RtmpKey" app:title="@string/rtmp_key" /> + + + + Date: Tue, 1 Jun 2021 21:39:01 +0200 Subject: [PATCH 07/12] Handle RTMP basic auth --- app/src/main/java/com/fpvout/digiview/MainActivity.java | 3 +++ app/src/main/java/com/fpvout/digiview/StreamingService.java | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/fpvout/digiview/MainActivity.java b/app/src/main/java/com/fpvout/digiview/MainActivity.java index 4566bb9..762edf9 100644 --- a/app/src/main/java/com/fpvout/digiview/MainActivity.java +++ b/app/src/main/java/com/fpvout/digiview/MainActivity.java @@ -110,6 +110,9 @@ public void onNewBitrateRtmp(long bitrate) { @Override public void onDisconnectRtmp() { updateLiveButtonIcon(); + runOnUiThread(() -> { + bitrateTextview.setText(""); + }); } @Override diff --git a/app/src/main/java/com/fpvout/digiview/StreamingService.java b/app/src/main/java/com/fpvout/digiview/StreamingService.java index c969039..58c4bfc 100644 --- a/app/src/main/java/com/fpvout/digiview/StreamingService.java +++ b/app/src/main/java/com/fpvout/digiview/StreamingService.java @@ -41,7 +41,7 @@ public IBinder onBind(Intent intent) { @Override public void onCreate() { super.onCreate(); - Log.e(TAG, "Create"); + Log.d(TAG, "Create"); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -53,7 +53,7 @@ public void onCreate() { @Override public int onStartCommand(Intent intent, int flags, int startId) { - Log.e(TAG, "Start"); + Log.d(TAG, "Start"); endpoint = String.format("%s/%s", sharedPreferences.getString("RtmpUrl", ""), sharedPreferences.getString("RtmpKey", "")); prepareStreaming(); startStreaming(); @@ -80,7 +80,6 @@ private void startStreaming() { 0, dpi )) { - Log.i(TAG, String.valueOf(Integer.parseInt(sharedPreferences.getString("OutputFramerate", "60")))); boolean audioInitialized; if (sharedPreferences.getString("AudioSource", "0").equals("internal") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { audioInitialized = rtmpDisplayBase.prepareInternalAudio(64 * 1024, 32000, true, false, false); @@ -145,6 +144,7 @@ public static void init(Context context, ConnectCheckerRtmp connectCheckerRtmp) dpi = dm.densityDpi; if (rtmpDisplayBase == null) { + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext); rtmpDisplayBase = new RtmpDisplay(appContext, true, connectChecker); if (!sharedPreferences.getString("RtmpUsername", "").isEmpty() && !sharedPreferences.getString("RtmpPassword", "").isEmpty()) { rtmpDisplayBase.setAuthorization(sharedPreferences.getString("RtmpUsername", ""), sharedPreferences.getString("RtmpPassword", "")); From 75fa77fca4342044fd9d9c5c7309d4ca40113872 Mon Sep 17 00:00:00 2001 From: Olivier Mouren Date: Tue, 1 Jun 2021 21:51:35 +0200 Subject: [PATCH 08/12] New live icon --- .../main/java/com/fpvout/digiview/MainActivity.java | 2 +- app/src/main/res/drawable/ic_live_icon.xml | 12 ++++++++++++ app/src/main/res/layout/activity_main.xml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_live_icon.xml diff --git a/app/src/main/java/com/fpvout/digiview/MainActivity.java b/app/src/main/java/com/fpvout/digiview/MainActivity.java index 762edf9..0729c56 100644 --- a/app/src/main/java/com/fpvout/digiview/MainActivity.java +++ b/app/src/main/java/com/fpvout/digiview/MainActivity.java @@ -517,7 +517,7 @@ private void updateLiveButtonIcon() { liveButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.exo_icon_stop, this.getTheme())); } else { toggleView(muteButton, false); - liveButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.exo_icon_play, this.getTheme())); + liveButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_live_icon, this.getTheme())); } }); } diff --git a/app/src/main/res/drawable/ic_live_icon.xml b/app/src/main/res/drawable/ic_live_icon.xml new file mode 100644 index 0000000..6d63b47 --- /dev/null +++ b/app/src/main/res/drawable/ic_live_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 0b6f000..c1444e3 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -71,7 +71,7 @@ android:layout_marginEnd="25dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/liveButton" - app:srcCompat="@drawable/exo_icon_play" /> + app:srcCompat="@drawable/ic_live_icon" /> Date: Wed, 2 Jun 2021 22:07:48 +0200 Subject: [PATCH 09/12] New stream preferences --- app/src/main/AndroidManifest.xml | 2 +- .../com/fpvout/digiview/MainActivity.java | 3 +- .../com/fpvout/digiview/SettingsActivity.java | 11 +- .../streaming/StreamAudioBitrate.java | 9 ++ .../streaming/StreamAudioSampleRate.java | 28 ++++ .../digiview/streaming/StreamAudioSource.java | 26 ++++ .../digiview/streaming/StreamBitrate.java | 9 ++ .../digiview/streaming/StreamFramerate.java | 9 ++ .../digiview/streaming/StreamResolution.java | 37 +++++ .../{ => streaming}/StreamingService.java | 43 ++++-- app/src/main/res/values-de/strings.xml | 36 ++--- app/src/main/res/values-es/strings.xml | 36 ++--- app/src/main/res/values-fr/strings.xml | 36 ++--- app/src/main/res/values-pt/strings.xml | 36 ++--- app/src/main/res/values-zh/strings.xml | 36 ++--- app/src/main/res/values/arrays.xml | 136 ++++++++++++++++-- app/src/main/res/values/strings.xml | 36 ++--- app/src/main/res/xml/root_preferences.xml | 89 +++++++----- 18 files changed, 456 insertions(+), 162 deletions(-) create mode 100644 app/src/main/java/com/fpvout/digiview/streaming/StreamAudioBitrate.java create mode 100644 app/src/main/java/com/fpvout/digiview/streaming/StreamAudioSampleRate.java create mode 100644 app/src/main/java/com/fpvout/digiview/streaming/StreamAudioSource.java create mode 100644 app/src/main/java/com/fpvout/digiview/streaming/StreamBitrate.java create mode 100644 app/src/main/java/com/fpvout/digiview/streaming/StreamFramerate.java create mode 100644 app/src/main/java/com/fpvout/digiview/streaming/StreamResolution.java rename app/src/main/java/com/fpvout/digiview/{ => streaming}/StreamingService.java (68%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1ecf9a3..23ea73b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,7 +51,7 @@ { if (!StreamingService.isStreaming()) { - if (sharedPreferences.getString("RtmpUrl", "").isEmpty() || sharedPreferences.getString("RtmpKey", "").isEmpty()) { + if (sharedPreferences.getString("StreamRtmpUrl", "").isEmpty() || sharedPreferences.getString("StreamRtmpKey", "").isEmpty()) { Toast.makeText(this, getString(R.string.rtmp_settings_empty), Toast.LENGTH_LONG).show(); Intent intent = new Intent(v.getContext(), SettingsActivity.class); v.getContext().startActivity(intent); diff --git a/app/src/main/java/com/fpvout/digiview/SettingsActivity.java b/app/src/main/java/com/fpvout/digiview/SettingsActivity.java index 1cf20f9..6c70174 100644 --- a/app/src/main/java/com/fpvout/digiview/SettingsActivity.java +++ b/app/src/main/java/com/fpvout/digiview/SettingsActivity.java @@ -10,6 +10,8 @@ import androidx.preference.ListPreference; import androidx.preference.PreferenceFragmentCompat; +import com.fpvout.digiview.streaming.StreamAudioSource; + import java.util.ArrayList; import java.util.Arrays; @@ -48,13 +50,16 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.root_preferences, rootKey); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ListPreference audioSourcePreference = findPreference("AudioSource"); + ListPreference audioSourcePreference = findPreference("StreamAudioSource"); ArrayList entries = new ArrayList<>(Arrays.asList(audioSourcePreference.getEntries())); ArrayList entryValues = new ArrayList<>(Arrays.asList(audioSourcePreference.getEntryValues())); - entries.add(getString(R.string.audio_source_internal)); - entryValues.add("internal"); + entries.add(getString(R.string.stream_audio_source_performance)); + entryValues.add(StreamAudioSource.PERFORMANCE); + + entries.add(getString(R.string.stream_audio_source_internal)); + entryValues.add(StreamAudioSource.INTERNAL); audioSourcePreference.setEntries(entries.toArray(new CharSequence[0])); audioSourcePreference.setEntryValues(entryValues.toArray(new CharSequence[0])); diff --git a/app/src/main/java/com/fpvout/digiview/streaming/StreamAudioBitrate.java b/app/src/main/java/com/fpvout/digiview/streaming/StreamAudioBitrate.java new file mode 100644 index 0000000..e120e1a --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/streaming/StreamAudioBitrate.java @@ -0,0 +1,9 @@ +package com.fpvout.digiview.streaming; + +public class StreamAudioBitrate { + public static final String DEFAULT = "128"; + + public static int getBitrate(String value) { + return Integer.parseInt(value) * 1024; + } +} diff --git a/app/src/main/java/com/fpvout/digiview/streaming/StreamAudioSampleRate.java b/app/src/main/java/com/fpvout/digiview/streaming/StreamAudioSampleRate.java new file mode 100644 index 0000000..5404322 --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/streaming/StreamAudioSampleRate.java @@ -0,0 +1,28 @@ +package com.fpvout.digiview.streaming; + +public class StreamAudioSampleRate { + public static final String DEFAULT = "44100hz"; + + public static int getSampleRate(String value) { + switch (value) { + case "8khz": + return 8000; + case "11025hz": + return 11025; + case "16khz": + return 16000; + case "22050hz": + return 22050; + case "32000hz": + return 32000; + case DEFAULT: + return 44100; + case "48khz": + return 48000; + case "96khz": + return 96000; + } + + return -1; + } +} diff --git a/app/src/main/java/com/fpvout/digiview/streaming/StreamAudioSource.java b/app/src/main/java/com/fpvout/digiview/streaming/StreamAudioSource.java new file mode 100644 index 0000000..21e7ab1 --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/streaming/StreamAudioSource.java @@ -0,0 +1,26 @@ +package com.fpvout.digiview.streaming; + +import android.media.MediaRecorder; + +public class StreamAudioSource { + public static final String DEFAULT = "default"; + public static final String INTERNAL = "internal"; + public static final String PERFORMANCE = "performance"; + + public static int getAudioSource(String value) { + switch (value) { + case DEFAULT: + return MediaRecorder.AudioSource.DEFAULT; + case "mic": + return MediaRecorder.AudioSource.MIC; + case "cam": + return MediaRecorder.AudioSource.CAMCORDER; + case "communication": + return MediaRecorder.AudioSource.VOICE_COMMUNICATION; + case PERFORMANCE: + return MediaRecorder.AudioSource.VOICE_PERFORMANCE; + } + + return -1; + } +} diff --git a/app/src/main/java/com/fpvout/digiview/streaming/StreamBitrate.java b/app/src/main/java/com/fpvout/digiview/streaming/StreamBitrate.java new file mode 100644 index 0000000..eae699e --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/streaming/StreamBitrate.java @@ -0,0 +1,9 @@ +package com.fpvout.digiview.streaming; + +public class StreamBitrate { + public static final String DEFAULT = "2500"; + + public static int getBitrate(String value) { + return Integer.parseInt(value) * 1024; + } +} diff --git a/app/src/main/java/com/fpvout/digiview/streaming/StreamFramerate.java b/app/src/main/java/com/fpvout/digiview/streaming/StreamFramerate.java new file mode 100644 index 0000000..3087404 --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/streaming/StreamFramerate.java @@ -0,0 +1,9 @@ +package com.fpvout.digiview.streaming; + +public class StreamFramerate { + public static final String DEFAULT = "60"; + + public static int getFramerate(String value) { + return Integer.parseInt(value); + } +} diff --git a/app/src/main/java/com/fpvout/digiview/streaming/StreamResolution.java b/app/src/main/java/com/fpvout/digiview/streaming/StreamResolution.java new file mode 100644 index 0000000..fb74a43 --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/streaming/StreamResolution.java @@ -0,0 +1,37 @@ +package com.fpvout.digiview.streaming; + +public class StreamResolution { + public static final String DEFAULT = "720p"; + private final int width; + private final int height; + + private StreamResolution(int width, int height) { + this.width = width; + this.height = height; + } + + public static StreamResolution getResolution(String value) { + switch (value) { + case "240p": + return new StreamResolution(426, 240); + case "360p": + return new StreamResolution(640, 360); + case "480p": + return new StreamResolution(854, 480); + case DEFAULT: + return new StreamResolution(1280, 720); + case "1080p": + return new StreamResolution(1920, 1080); + } + + return null; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } +} diff --git a/app/src/main/java/com/fpvout/digiview/StreamingService.java b/app/src/main/java/com/fpvout/digiview/streaming/StreamingService.java similarity index 68% rename from app/src/main/java/com/fpvout/digiview/StreamingService.java rename to app/src/main/java/com/fpvout/digiview/streaming/StreamingService.java index 58c4bfc..2f819d6 100644 --- a/app/src/main/java/com/fpvout/digiview/StreamingService.java +++ b/app/src/main/java/com/fpvout/digiview/streaming/StreamingService.java @@ -1,4 +1,4 @@ -package com.fpvout.digiview; +package com.fpvout.digiview.streaming; import android.app.Notification; import android.app.NotificationChannel; @@ -32,6 +32,7 @@ public class StreamingService extends Service { private static RtmpDisplay rtmpDisplayBase; private static int dpi; private String endpoint; + @Nullable @Override public IBinder onBind(Intent intent) { @@ -54,7 +55,7 @@ public void onCreate() { @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "Start"); - endpoint = String.format("%s/%s", sharedPreferences.getString("RtmpUrl", ""), sharedPreferences.getString("RtmpKey", "")); + endpoint = String.format("%s/%s", sharedPreferences.getString("StreamRtmpUrl", ""), sharedPreferences.getString("StreamRtmpKey", "")); prepareStreaming(); startStreaming(); @@ -64,31 +65,45 @@ public int onStartCommand(Intent intent, int flags, int startId) { private void prepareStreaming() { stopStreaming(); rtmpDisplayBase = new RtmpDisplay(appContext, true, connectChecker); - if (!sharedPreferences.getString("RtmpUsername", "").isEmpty() && !sharedPreferences.getString("RtmpPassword", "").isEmpty()) { - rtmpDisplayBase.setAuthorization(sharedPreferences.getString("RtmpUsername", ""), sharedPreferences.getString("RtmpPassword", "")); + if (!sharedPreferences.getString("StreamRtmpUsername", "").isEmpty() && !sharedPreferences.getString("StreamRtmpPassword", "").isEmpty()) { + rtmpDisplayBase.setAuthorization(sharedPreferences.getString("StreamRtmpUsername", ""), sharedPreferences.getString("StreamRtmpPassword", "")); } rtmpDisplayBase.setIntentResult(mediaProjectionResultCode, mediaProjectionData); } private void startStreaming() { if (!rtmpDisplayBase.isStreaming()) { + StreamResolution streamResolution = StreamResolution.getResolution(sharedPreferences.getString("StreamResolution", StreamResolution.DEFAULT)); if (rtmpDisplayBase.prepareVideo( - Integer.parseInt(sharedPreferences.getString("OutputWidth", "1280")), - Integer.parseInt(sharedPreferences.getString("OutputHeight", "720")), - Integer.parseInt(sharedPreferences.getString("OutputFramerate", "60")), - Integer.parseInt(sharedPreferences.getString("OutputBitrate", "1200")) * 1024, + streamResolution.getWidth(), + streamResolution.getHeight(), + StreamFramerate.getFramerate(sharedPreferences.getString("StreamFramerate", StreamFramerate.DEFAULT)), + StreamBitrate.getBitrate(sharedPreferences.getString("StreamBitrate", StreamBitrate.DEFAULT)), 0, dpi )) { boolean audioInitialized; - if (sharedPreferences.getString("AudioSource", "0").equals("internal") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - audioInitialized = rtmpDisplayBase.prepareInternalAudio(64 * 1024, 32000, true, false, false); + if (sharedPreferences.getString("StreamAudioSource", StreamAudioSource.DEFAULT).equals(StreamAudioSource.INTERNAL) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + audioInitialized = rtmpDisplayBase.prepareInternalAudio( + StreamAudioBitrate.getBitrate(sharedPreferences.getString("StreamAudioBitrate", StreamAudioBitrate.DEFAULT)), + StreamAudioSampleRate.getSampleRate(sharedPreferences.getString("StreamAudioSampleRate", StreamAudioSampleRate.DEFAULT)), + sharedPreferences.getBoolean("StreamAudioStereo", true), + false, + false + ); } else { - audioInitialized = rtmpDisplayBase.prepareAudio(Integer.parseInt(sharedPreferences.getString("AudioSource", "0")), 64 * 1024, 32000, false, false, false); + audioInitialized = rtmpDisplayBase.prepareAudio( + StreamAudioSource.getAudioSource(sharedPreferences.getString("StreamAudioSource", StreamAudioSource.DEFAULT)), + StreamAudioBitrate.getBitrate(sharedPreferences.getString("StreamAudioBitrate", StreamAudioBitrate.DEFAULT)), + StreamAudioSampleRate.getSampleRate(sharedPreferences.getString("StreamAudioSampleRate", StreamAudioSampleRate.DEFAULT)), + sharedPreferences.getBoolean("StreamAudioStereo", true), + false, + false + ); } if (audioInitialized) { - if (!sharedPreferences.getBoolean("RecordAudio", true)) { + if (!sharedPreferences.getBoolean("StreamRecordAudio", true)) { rtmpDisplayBase.disableAudio(); } else { rtmpDisplayBase.enableAudio(); @@ -146,8 +161,8 @@ public static void init(Context context, ConnectCheckerRtmp connectCheckerRtmp) if (rtmpDisplayBase == null) { sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext); rtmpDisplayBase = new RtmpDisplay(appContext, true, connectChecker); - if (!sharedPreferences.getString("RtmpUsername", "").isEmpty() && !sharedPreferences.getString("RtmpPassword", "").isEmpty()) { - rtmpDisplayBase.setAuthorization(sharedPreferences.getString("RtmpUsername", ""), sharedPreferences.getString("RtmpPassword", "")); + if (!sharedPreferences.getString("StreamRtmpUsername", "").isEmpty() && !sharedPreferences.getString("StreamRtmpPassword", "").isEmpty()) { + rtmpDisplayBase.setAuthorization(sharedPreferences.getString("StreamRtmpUsername", ""), sharedPreferences.getString("StreamRtmpPassword", "")); } } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 583fa19..3a8f651 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -41,23 +41,27 @@ Open-Source Lizenz MIT Lizenz Streaming - RTMP Url - RTMP Key - RTMP Username - RTMP Password - Record audio - Audio source - Default - Mic - Camera - Communication - Performance - Internal audio - Output Width - Output Height - Output Framerate - Output Bitrate + RTMP Url + RTMP Key + RTMP Username + RTMP Password + Record audio + Audio source + Default + Mic + Camera + Communication + Performance + Internal audio + Output Resolution + Output Framerate + Output Max Bitrate Failed to connect to RTMP server RTMP authentication failed Please, check your streaming RTMP URL and Key + Audio recording can be muted during live + Stereo audio + Only if your device or the audio source supports it + Audio sample rate + Audio bitrate \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 05ef891..37c61f1 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -40,23 +40,27 @@ Licencia Open-Source Licencia MIT Streaming - RTMP Url - RTMP Key - RTMP Username - RTMP Password - Record audio - Audio source - Default - Mic - Camera - Communication - Performance - Internal audio - Output Width - Output Height - Output Framerate - Output Bitrate + RTMP Url + RTMP Key + RTMP Username + RTMP Password + Record audio + Audio source + Default + Mic + Camera + Communication + Performance + Internal audio + Output Resolution + Output Framerate + Output Max Bitrate Failed to connect to RTMP server RTMP authentication failed Please, check your streaming RTMP URL and Key + Audio recording can be muted during live + Stereo audio + Only if your device or the audio source supports it + Audio sample rate + Audio bitrate \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 82c3e4a..a909ceb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -38,23 +38,27 @@ License Open-Source License MIT Streaming - RTMP Url - RTMP Key - RTMP Username - RTMP Password - Enregistrer le son - Source audio - Par défaut - Mic - Camera - Communication - Performance - Audio interne - Output Width - Output Height - Output Framerate - Output Bitrate + RTMP Url + RTMP Key + RTMP Username + RTMP Password + Enregistrer le son + Source audio + Par défaut + Mic + Camera + Communication + Performance + Audio interne + Output Resolution + Output Framerate + Output Max Bitrate Failed to connect to RTMP server RTMP authentication failed Please, check your streaming RTMP URL and Key + Audio recording can be muted during live + Stereo audio + Only if your device or the audio source supports it + Audio sample rate + Audio bitrate \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 5f54249..7972209 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -40,24 +40,28 @@ Licença Open-Source Licença MIT Streaming - RTMP Url - RTMP Key - RTMP Username - RTMP Password - Record audio - Audio source - Default - Mic - Camera - Communication - Performance - Internal audio - Output Width - Output Height - Output Framerate - Output Bitrate + RTMP Url + RTMP Key + RTMP Username + RTMP Password + Record audio + Audio source + Default + Mic + Camera + Communication + Performance + Internal audio + Output Resolution + Output Framerate + Output Max Bitrate Failed to connect to RTMP server RTMP authentication failed Please, check your streaming RTMP URL and Key + Audio recording can be muted during live + Stereo audio + Only if your device or the audio source supports it + Audio sample rate + Audio bitrate \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 08ebbfc..304fb8b 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -40,24 +40,28 @@ 开源证书 MIT License Streaming - RTMP Url - RTMP Key - RTMP Username - RTMP Password - Record audio - Audio source - Default - Mic - Camera - Communication - Performance - Internal audio - Output Width - Output Height - Output Framerate - Output Bitrate + RTMP Url + RTMP Key + RTMP Username + RTMP Password + Record audio + Audio source + Default + Mic + Camera + Communication + Performance + Internal audio + Output Resolution + Output Framerate + Output Max Bitrate Failed to connect to RTMP server RTMP authentication failed Please, check your streaming RTMP URL and Key + Audio recording can be muted during live + Stereo audio + Only if your device or the audio source supports it + Audio sample rate + Audio bitrate diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index c91eeee..734d921 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -15,19 +15,127 @@ legacy_buffered - - @string/audio_source_default - @string/audio_source_mic - @string/audio_source_cam - @string/audio_source_communication - @string/audio_source_performance - - - - 0 - 1 - 5 - 7 - 10 + + @string/stream_audio_source_default + @string/stream_audio_source_mic + @string/stream_audio_source_cam + @string/stream_audio_source_communication + + + + default + mic + cam + communication + + + + 240p + 360p + 480p + 720p (default) + 1080p + + + + 240p + 360p + 480p + 720p + 1080p + + + + 15 fps + 24 fps + 25 fps + 30 fps + 60 fps (default) + + + + 15 + 24 + 25 + 30 + 60 + + + + 300 kbps + 500 kbps + 700 kbps + 1000 kbps + 1500 kbps + 2000 kbps + 2500 kbps (default) + 3500 kbps + 4000 kbps + 5000 kbps + 6000 kbps + 7000 kbps + 8000 kbps + 10000 kbps + 15000 kbps + 20000 kbps + 25000 kbps + + + + 300 + 500 + 700 + 1000 + 1500 + 2000 + 2500 + 3500 + 4000 + 5000 + 6000 + 7000 + 8000 + 10000 + 15000 + 20000 + 25000 + + + + 8 KHz + 11025 Hz + 16 KHz + 22050 Hz + 32000 Hz + 44100 Hz (default) + 48 KHz + 96 KHz + + + + 8khz + 11025hz + 16khz + 22050hz + 32000hz + 44100hz + 48khz + 96khz + + + + 64 kbps + 96 kbps + 128 kbps (default) + 192 kbps + 320 kbps + + + + 64 + 96 + 128 + 192 + 320 \ 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 6cecd38..9949102 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,24 +41,28 @@ Open-Source License MIT License Streaming - RTMP Url - RTMP Key - RTMP Username - RTMP Password - Record audio - Audio source - Default - Mic - Camera - Communication - Performance - Internal audio - Output Width - Output Height - Output Framerate - Output Bitrate + RTMP Url + RTMP Key + RTMP Username + RTMP Password + Record audio + Audio source + Default + Mic + Camera + Communication + Performance + Internal audio + Output Resolution + Output Framerate + Output Max Bitrate Failed to connect to RTMP server RTMP authentication failed Please, check your streaming RTMP URL and Key + Audio recording can be muted during live + Stereo audio + Only if your device or the audio source supports it + Audio sample rate + Audio bitrate \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index ce90623..20df726 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -50,58 +50,81 @@ + app:key="StreamRtmpKey" + app:title="@string/stream_rtmp_key" /> + app:key="StreamRtmpPassword" + app:title="@string/stream_rtmp_password" /> - - - - - + app:key="StreamRecordAudio" + app:defaultValue="true" + app:title="@string/stream_record_audio" + app:summary="@string/stream_record_audio_summary" /> + + + + + + From 7e355346a6d8225e7d293a4905362b3bd6584581 Mon Sep 17 00:00:00 2001 From: Olivier Mouren Date: Thu, 3 Jun 2021 22:00:45 +0200 Subject: [PATCH 10/12] Use FAB animation instead of manual fade animation --- .../com/fpvout/digiview/MainActivity.java | 93 ++++++++++--------- app/src/main/res/layout/activity_main.xml | 5 +- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/fpvout/digiview/MainActivity.java b/app/src/main/java/com/fpvout/digiview/MainActivity.java index 5209562..ba8da5b 100644 --- a/app/src/main/java/com/fpvout/digiview/MainActivity.java +++ b/app/src/main/java/com/fpvout/digiview/MainActivity.java @@ -23,6 +23,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.animation.Animation; import android.widget.TextView; import android.widget.Toast; @@ -55,8 +56,7 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener private static final int VENDOR_ID = 11427; private static final int PRODUCT_ID = 31; private int shortAnimationDuration; - private View settingsButton; - private ConstraintLayout liveButtons; + private FloatingActionButton settingsButton; private FloatingActionButton liveButton; private FloatingActionButton muteButton; private TextView bitrateTextview; @@ -74,16 +74,13 @@ public class MainActivity extends AppCompatActivity implements UsbDeviceListener private ScaleGestureDetector scaleGestureDetector; private SharedPreferences sharedPreferences; private static final String ShowWatermark = "ShowWatermark"; - private final Runnable hideSettingsButtonRunnable = new Runnable() { + private final Runnable hideButtonsRunnable = new Runnable() { @Override public void run() { toggleView(settingsButton, false); - } - }; - private final Runnable hideLiveButtonRunnable = new Runnable() { - @Override - public void run() { - toggleView(liveButtons, false); + toggleView(liveButton, false); + toggleView(muteButton, false); + toggleView(bitrateTextview, false); } }; private final ConnectCheckerRtmp connectChecker = new ConnectCheckerRtmp() { @@ -104,6 +101,7 @@ public void onConnectionFailedRtmp(String reason) { @Override public void onNewBitrateRtmp(long bitrate) { runOnUiThread(() -> { + bitrateTextview.setVisibility(View.VISIBLE); bitrateTextview.setText(String.format("%s kbps", bitrate / 1024)); }); } @@ -113,6 +111,7 @@ public void onDisconnectRtmp() { updateLiveButtonIcon(); runOnUiThread(() -> { bitrateTextview.setText(""); + bitrateTextview.setVisibility(View.GONE); }); } @@ -175,7 +174,6 @@ protected void onCreate(Bundle savedInstanceState) { v.getContext().startActivity(intent); }); - liveButtons = findViewById(R.id.liveButtons); bitrateTextview = findViewById(R.id.bitrateText); StreamingService.init(this, connectChecker); @@ -229,73 +227,80 @@ protected void onCreate(Bundle savedInstanceState) { private void toggleFullOverlay() { if (overlayView.getAlpha() > 0.0f) return; - if (sharedPreferences.getBoolean(ShowWatermark, true)) { - toggleView(watermarkView, 0.3f); + toggleView(settingsButton, hideButtonsRunnable); + toggleView(liveButton, hideButtonsRunnable); + if (StreamingService.isStreaming()) { + toggleView(bitrateTextview, hideButtonsRunnable); + toggleView(muteButton, hideButtonsRunnable); } - - settingsButton.removeCallbacks(hideSettingsButtonRunnable); - toggleView(settingsButton, new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - settingsButton.postDelayed(hideSettingsButtonRunnable, 3000); - } - }); - liveButtons.removeCallbacks(hideLiveButtonRunnable); - toggleView(liveButtons, new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - liveButtons.postDelayed(hideLiveButtonRunnable, 3000); - } - }); } private void hideFullOverlay() { toggleView(watermarkView, sharedPreferences.getBoolean(ShowWatermark, true), 0.3f); toggleView(settingsButton, false); - toggleView(liveButtons, false); + toggleView(liveButton, false); + toggleView(muteButton, false); + toggleView(bitrateTextview, false); toggleView(overlayView, false); } private void showFullOverlay() { toggleView(watermarkView, false); - settingsButton.removeCallbacks(hideSettingsButtonRunnable); toggleView(settingsButton, true); - - liveButtons.removeCallbacks(hideLiveButtonRunnable); - toggleView(liveButtons, true); + toggleView(liveButton, true); + if (StreamingService.isStreaming()) { + toggleView(muteButton, true); + toggleView(bitrateTextview, true); + } } - private void toggleView(View view, @Nullable AnimatorListenerAdapter animatorListener) { - toggleView(view, view.getAlpha() == 0.0f, 1.0f, animatorListener); + private void toggleView(View view, @Nullable Runnable runnable) { + toggleView(view, view.getAlpha() == 0.0f, 1.0f, runnable); } - private void toggleView(View view, float visibleAlpha) { - toggleView(view, view.getAlpha() == 0.0f, visibleAlpha); + private void toggleView(FloatingActionButton view, @Nullable Runnable runnable) { + toggleView(view, view.getVisibility() != View.VISIBLE, runnable); } private void toggleView(View view, boolean visible) { toggleView(view, visible, 1.0f, null); } + private void toggleView(FloatingActionButton view, boolean visible) { + toggleView(view, visible, null); + } + private void toggleView(View view, boolean visible, float visibleAlpha) { toggleView(view, visible, visibleAlpha, null); } - private void toggleView(View view, boolean visible, float visibleAlpha, @Nullable AnimatorListenerAdapter animatorListener) { + private void toggleView(View view, boolean visible, float visibleAlpha, @Nullable Runnable runnable) { if (!visible) { + view.removeCallbacks(runnable); view.animate().cancel(); view.animate() - .alpha(0) - .setDuration(shortAnimationDuration) - .setListener(null); + .alpha(0) + .setDuration(shortAnimationDuration) + .setListener(null); } else { + view.removeCallbacks(runnable); view.animate().cancel(); view.animate() - .alpha(visibleAlpha) - .setDuration(shortAnimationDuration) - .setListener(animatorListener); + .alpha(visibleAlpha) + .setDuration(shortAnimationDuration); + view.postDelayed(runnable, 3000); + } + } + + private void toggleView(FloatingActionButton view, boolean visible, @Nullable Runnable runnable) { + if (!visible) { + view.hide(); + } else { + view.removeCallbacks(runnable); + view.show(); + view.postDelayed(runnable, 3000); } } @@ -514,7 +519,7 @@ private void updateLiveButtonIcon() { muteButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_microphone_solid, this.getTheme())); } - toggleView(muteButton, true); + toggleView(muteButton, true, overlayView.getAlpha() > 0.0f ? null : hideButtonsRunnable); liveButton.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.exo_icon_stop, this.getTheme())); } else { toggleView(muteButton, false); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c1444e3..5064b3c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -75,9 +75,9 @@ + + Date: Sun, 6 Jun 2021 16:01:33 +0200 Subject: [PATCH 11/12] Add stream rotate to force portrait mode --- .../digiview/streaming/CustomDisplayBase.java | 642 ++++++++++++++++++ .../digiview/streaming/CustomRtmpDisplay.java | 159 +++++ .../digiview/streaming/StreamingService.java | 10 +- app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values-es/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-pt/strings.xml | 2 + app/src/main/res/values-zh/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/root_preferences.xml | 6 + 10 files changed, 823 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/fpvout/digiview/streaming/CustomDisplayBase.java create mode 100644 app/src/main/java/com/fpvout/digiview/streaming/CustomRtmpDisplay.java diff --git a/app/src/main/java/com/fpvout/digiview/streaming/CustomDisplayBase.java b/app/src/main/java/com/fpvout/digiview/streaming/CustomDisplayBase.java new file mode 100644 index 0000000..91fcae5 --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/streaming/CustomDisplayBase.java @@ -0,0 +1,642 @@ +package com.fpvout.digiview.streaming; + +import android.content.Context; +import android.content.Intent; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.AudioAttributes; +import android.media.AudioPlaybackCaptureConfiguration; +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.media.MediaRecorder; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.os.Build; +import android.view.Surface; +import android.view.SurfaceView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.pedro.encoder.Frame; +import com.pedro.encoder.audio.AudioEncoder; +import com.pedro.encoder.audio.GetAacData; +import com.pedro.encoder.input.audio.CustomAudioEffect; +import com.pedro.encoder.input.audio.GetMicrophoneData; +import com.pedro.encoder.input.audio.MicrophoneManager; +import com.pedro.encoder.input.audio.MicrophoneManagerManual; +import com.pedro.encoder.input.audio.MicrophoneMode; +import com.pedro.encoder.utils.CodecUtil; +import com.pedro.encoder.video.FormatVideoEncoder; +import com.pedro.encoder.video.GetVideoData; +import com.pedro.encoder.video.VideoEncoder; +import com.pedro.rtplibrary.util.FpsListener; +import com.pedro.rtplibrary.util.RecordController; +import com.pedro.rtplibrary.view.GlInterface; +import com.pedro.rtplibrary.view.OffScreenGlThread; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static android.content.Context.MEDIA_PROJECTION_SERVICE; + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public abstract class CustomDisplayBase implements GetAacData, GetVideoData, GetMicrophoneData { + + private OffScreenGlThread glInterface; + protected Context context; + private MediaProjection mediaProjection; + private MediaProjectionManager mediaProjectionManager; + protected VideoEncoder videoEncoder; + private MicrophoneManager microphoneManager; + private AudioEncoder audioEncoder; + private boolean streaming = false; + protected SurfaceView surfaceView; + private boolean videoEnabled = true; + private int dpi = 320; + private VirtualDisplay virtualDisplay; + private int resultCode = -1; + private Intent data; + protected RecordController recordController; + private FpsListener fpsListener = new FpsListener(); + private boolean audioInitialized = false; + + public CustomDisplayBase(Context context, boolean useOpengl) { + this.context = context; + if (useOpengl) { + glInterface = new OffScreenGlThread(context); + glInterface.init(); + } + mediaProjectionManager = + ((MediaProjectionManager) context.getSystemService(MEDIA_PROJECTION_SERVICE)); + this.surfaceView = null; + videoEncoder = new VideoEncoder(this); + audioEncoder = new AudioEncoder(this); + //Necessary use same thread to read input buffer and encode it with internal audio or audio is choppy. + setMicrophoneMode(MicrophoneMode.SYNC); + recordController = new RecordController(); + } + + /** + * Must be called before prepareAudio. + * + * @param microphoneMode mode to work accord to audioEncoder. By default SYNC: + * SYNC using same thread. This mode could solve choppy audio or audio frame discarded. + * ASYNC using other thread. + */ + public void setMicrophoneMode(MicrophoneMode microphoneMode) { + switch (microphoneMode) { + case SYNC: + microphoneManager = new MicrophoneManagerManual(); + audioEncoder = new AudioEncoder(this); + audioEncoder.setGetFrame(((MicrophoneManagerManual) microphoneManager).getGetFrame()); + break; + case ASYNC: + microphoneManager = new MicrophoneManager(this); + audioEncoder = new AudioEncoder(this); + break; + } + } + + /** + * Set an audio effect modifying microphone's PCM buffer. + */ + public void setCustomAudioEffect(CustomAudioEffect customAudioEffect) { + microphoneManager.setCustomAudioEffect(customAudioEffect); + } + + /** + * @param callback get fps while record or stream + */ + public void setFpsListener(FpsListener.Callback callback) { + fpsListener.setCallback(callback); + } + + /** + * Basic auth developed to work with Wowza. No tested with other server + * + * @param user auth. + * @param password auth. + */ + public abstract void setAuthorization(String user, String password); + + /** + * Call this method before use @startStream. If not you will do a stream without video. + * + * @param width resolution in px. + * @param height resolution in px. + * @param fps frames per second of the stream. + * @param bitrate H264 in bps. + * @param rotation could be 90, 180, 270 or 0 (Normally 0 if you are streaming in landscape or 90 + * if you are streaming in Portrait). This only affect to stream result. This work rotating with + * encoder. + * NOTE: Rotation with encoder is silence ignored in some devices. + * @param dpi of your screen device. + * @return true if success, false if you get a error (Normally because the encoder selected + * doesn't support any configuration seated or your device hasn't a H264 encoder). + */ + public boolean prepareVideo(int width, int height, int fps, int bitrate, int rotation, int dpi, + int avcProfile, int avcProfileLevel, int iFrameInterval) { + this.dpi = dpi; + boolean result = + videoEncoder.prepareVideoEncoder(width, height, fps, bitrate, rotation, iFrameInterval, + FormatVideoEncoder.SURFACE, avcProfile, avcProfileLevel); + if (glInterface != null) { + if (rotation == 90 || rotation == 270) { + glInterface.setEncoderSize(videoEncoder.getHeight(), videoEncoder.getWidth()); + } else { + glInterface.setEncoderSize(videoEncoder.getWidth(), videoEncoder.getHeight()); + } + } + return result; + } + + public boolean prepareVideo(int width, int height, int fps, int bitrate, int rotation, int dpi) { + return prepareVideo(width, height, fps, bitrate, rotation, dpi, -1, -1, 2); + } + + public boolean prepareVideo(int width, int height, int bitrate) { + return prepareVideo(width, height, 30, bitrate, 0, 320); + } + + protected abstract void prepareAudioRtp(boolean isStereo, int sampleRate); + + /** + * Call this method before use @startStream. If not you will do a stream without audio. + * + * @param bitrate AAC in kb. + * @param sampleRate of audio in hz. Can be 8000, 16000, 22500, 32000, 44100. + * @param isStereo true if you want Stereo audio (2 audio channels), false if you want Mono audio + * (1 audio channel). + * @param echoCanceler true enable echo canceler, false disable. + * @param noiseSuppressor true enable noise suppressor, false disable. + * @return true if success, false if you get a error (Normally because the encoder selected + * doesn't support any configuration seated or your device hasn't a AAC encoder). + */ + public boolean prepareAudio(int audioSource, int bitrate, int sampleRate, boolean isStereo, boolean echoCanceler, + boolean noiseSuppressor) { + if (!microphoneManager.createMicrophone(audioSource, sampleRate, isStereo, echoCanceler, noiseSuppressor)) { + return false; + } + prepareAudioRtp(isStereo, sampleRate); + audioInitialized = audioEncoder.prepareAudioEncoder(bitrate, sampleRate, isStereo, + microphoneManager.getMaxInputSize()); + return audioInitialized; + } + + public boolean prepareAudio(int bitrate, int sampleRate, boolean isStereo, boolean echoCanceler, + boolean noiseSuppressor) { + return prepareAudio(MediaRecorder.AudioSource.DEFAULT, bitrate, sampleRate, isStereo, echoCanceler, + noiseSuppressor); + } + + public boolean prepareAudio(int bitrate, int sampleRate, boolean isStereo) { + return prepareAudio(bitrate, sampleRate, isStereo, false, false); + } + + /** + * Call this method before use @startStream for streaming internal audio only. + * + * @param bitrate AAC in kb. + * @param sampleRate of audio in hz. Can be 8000, 16000, 22500, 32000, 44100. + * @param isStereo true if you want Stereo audio (2 audio channels), false if you want Mono audio + * (1 audio channel). + * @see AudioPlaybackCaptureConfiguration.Builder#Builder(MediaProjection) + */ + @RequiresApi(api = Build.VERSION_CODES.Q) + public boolean prepareInternalAudio(int bitrate, int sampleRate, boolean isStereo, + boolean echoCanceler, boolean noiseSuppressor) { + if (mediaProjection == null) { + mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data); + } + + AudioPlaybackCaptureConfiguration config = + new AudioPlaybackCaptureConfiguration.Builder(mediaProjection).addMatchingUsage( + AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(AudioAttributes.USAGE_GAME) + .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) + .build(); + if (!microphoneManager.createInternalMicrophone(config, sampleRate, isStereo, echoCanceler, + noiseSuppressor)) { + return false; + } + prepareAudioRtp(isStereo, sampleRate); + audioInitialized = audioEncoder.prepareAudioEncoder(bitrate, sampleRate, isStereo, + microphoneManager.getMaxInputSize()); + return audioInitialized; + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + public boolean prepareInternalAudio(int bitrate, int sampleRate, boolean isStereo) { + return prepareInternalAudio(bitrate, sampleRate, isStereo, false, false); + } + + /** + * Same to call: + * rotation = 0; + * if (Portrait) rotation = 90; + * prepareVideo(640, 480, 30, 1200 * 1024, true, 0); + * + * @return true if success, false if you get a error (Normally because the encoder selected + * doesn't support any configuration seated or your device hasn't a H264 encoder). + */ + public boolean prepareVideo() { + return prepareVideo(640, 480, 30, 1200 * 1024, 0, 320); + } + + /** + * Same to call: + * prepareAudio(64 * 1024, 32000, true, false, false); + * + * @return true if success, false if you get a error (Normally because the encoder selected + * doesn't support any configuration seated or your device hasn't a AAC encoder). + */ + public boolean prepareAudio() { + return prepareAudio(64 * 1024, 32000, true, false, false); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + public boolean prepareInternalAudio() { + return prepareInternalAudio(64 * 1024, 32000, true); + } + + /** + * @param forceVideo force type codec used. FIRST_COMPATIBLE_FOUND, SOFTWARE, HARDWARE + * @param forceAudio force type codec used. FIRST_COMPATIBLE_FOUND, SOFTWARE, HARDWARE + */ + public void setForce(CodecUtil.Force forceVideo, CodecUtil.Force forceAudio) { + videoEncoder.setForce(forceVideo); + audioEncoder.setForce(forceAudio); + } + + /** + * Starts recording an MP4 video. Needs to be called while streaming. + * + * @param path Where file will be saved. + * @throws IOException If initialized before a stream. + */ + public void startRecord(@NonNull String path, @Nullable RecordController.Listener listener) + throws IOException { + recordController.startRecord(path, listener); + if (!streaming) { + startEncoders(resultCode, data); + } else if (videoEncoder.isRunning()) { + resetVideoEncoder(); + } + } + + public void startRecord(@NonNull final String path) throws IOException { + startRecord(path, null); + } + + /** + * Starts recording an MP4 video. Needs to be called while streaming. + * + * @param fd Where the file will be saved. + * @throws IOException If initialized before a stream. + */ + @RequiresApi(api = Build.VERSION_CODES.O) + public void startRecord(@NonNull final FileDescriptor fd, + @Nullable RecordController.Listener listener) throws IOException { + recordController.startRecord(fd, listener); + if (!streaming) { + startEncoders(resultCode, data); + } else if (videoEncoder.isRunning()) { + resetVideoEncoder(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public void startRecord(@NonNull final FileDescriptor fd) throws IOException { + startRecord(fd, null); + } + + /** + * Stop record MP4 video started with @startRecord. If you don't call it file will be unreadable. + */ + public void stopRecord() { + recordController.stopRecord(); + if (!streaming) stopStream(); + } + + protected abstract void startStreamRtp(String url); + + /** + * Create Intent used to init screen capture with startActivityForResult. + * + * @return intent to startActivityForResult. + */ + public Intent sendIntent() { + return mediaProjectionManager.createScreenCaptureIntent(); + } + + public void setIntentResult(int resultCode, Intent data) { + this.resultCode = resultCode; + this.data = data; + } + + /** + * Need be called after @prepareVideo or/and @prepareAudio. + * + * @param url of the stream like: + * protocol://ip:port/application/streamName + * + * RTSP: rtsp://192.168.1.1:1935/live/pedroSG94 + * RTSPS: rtsps://192.168.1.1:1935/live/pedroSG94 + * RTMP: rtmp://192.168.1.1:1935/live/pedroSG94 + * RTMPS: rtmps://192.168.1.1:1935/live/pedroSG94 + */ + public void startStream(String url) { + streaming = true; + if (!recordController.isRunning()) { + startEncoders(resultCode, data); + } else { + resetVideoEncoder(); + } + startStreamRtp(url); + } + + private void startEncoders(int resultCode, Intent data) { + if (data == null) { + throw new RuntimeException("You need send intent data before startRecord or startStream"); + } + videoEncoder.start(); + if (audioInitialized) audioEncoder.start(); + if (glInterface != null) { + glInterface.init(); + glInterface.setFps(videoEncoder.getFps()); + glInterface.start(); + glInterface.addMediaCodecSurface(videoEncoder.getInputSurface()); + } + Surface surface = + (glInterface != null) ? glInterface.getSurface() : videoEncoder.getInputSurface(); + if (mediaProjection == null) { + mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data); + } + int flags = 0; + flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR + | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION + | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; + + if (glInterface != null && videoEncoder.getRotation() == 90 + || videoEncoder.getRotation() == 270) { + // Use VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT to force stream capture rotate 1 << 7 + flags |= 1 << 7; + + virtualDisplay = + mediaProjection.createVirtualDisplay("Stream Display", videoEncoder.getHeight(), + videoEncoder.getWidth(), dpi, flags, surface, null, null); + } else { + virtualDisplay = + mediaProjection.createVirtualDisplay("Stream Display", videoEncoder.getWidth(), + videoEncoder.getHeight(), dpi, flags, surface, null, null); + } + if (audioInitialized) microphoneManager.start(); + } + + private void resetVideoEncoder() { + virtualDisplay.setSurface(null); + if (glInterface != null) { + glInterface.removeMediaCodecSurface(); + } + videoEncoder.forceKeyFrame(); + if (glInterface != null) { + glInterface.addMediaCodecSurface(videoEncoder.getInputSurface()); + } + virtualDisplay.setSurface( + glInterface != null ? glInterface.getSurface() : videoEncoder.getInputSurface()); + } + + protected abstract void stopStreamRtp(); + + /** + * Stop stream started with @startStream. + */ + public void stopStream() { + if (streaming) { + streaming = false; + stopStreamRtp(); + } + if (!recordController.isRecording()) { + if (audioInitialized) microphoneManager.stop(); + if (mediaProjection != null) { + mediaProjection.stop(); + } + if (glInterface != null) { + glInterface.removeMediaCodecSurface(); + glInterface.stop(); + } + videoEncoder.stop(); + audioEncoder.stop(); + data = null; + recordController.resetFormats(); + } + } + + public boolean reTry(long delay, String reason) { + boolean result = shouldRetry(reason); + if (result) { + reTry(delay); + } + return result; + } + + /** + * Replace with reTry(long delay, String reason); + */ + @Deprecated + public void reTry(long delay) { + resetVideoEncoder(); + reConnect(delay); + } + + /** + * Replace with reTry(long delay, String reason); + */ + @Deprecated + public abstract boolean shouldRetry(String reason); + + public abstract void setReTries(int reTries); + + protected abstract void reConnect(long delay); + + //cache control + public abstract boolean hasCongestion(); + + public abstract void resizeCache(int newSize) throws RuntimeException; + + public abstract int getCacheSize(); + + public abstract long getSentAudioFrames(); + + public abstract long getSentVideoFrames(); + + public abstract long getDroppedAudioFrames(); + + public abstract long getDroppedVideoFrames(); + + public abstract void resetSentAudioFrames(); + + public abstract void resetSentVideoFrames(); + + public abstract void resetDroppedAudioFrames(); + + public abstract void resetDroppedVideoFrames(); + + public GlInterface getGlInterface() { + if (glInterface != null) { + return glInterface; + } else { + throw new RuntimeException("You can't do it. You are not using Opengl"); + } + } + + /** + * Mute microphone, can be called before, while and after stream. + */ + public void disableAudio() { + if (audioInitialized) { + microphoneManager.mute(); + } + } + + /** + * Enable a muted microphone, can be called before, while and after stream. + */ + public void enableAudio() { + if (audioInitialized) { + microphoneManager.unMute(); + } + } + + /** + * Get mute state of microphone. + * + * @return true if muted, false if enabled + */ + public boolean isAudioMuted() { + return microphoneManager.isMuted(); + } + + /** + * Get video camera state + * + * @return true if disabled, false if enabled + */ + public boolean isVideoEnabled() { + return videoEnabled; + } + + public int getBitrate() { + return videoEncoder.getBitRate(); + } + + public int getResolutionValue() { + return videoEncoder.getWidth() * videoEncoder.getHeight(); + } + + public int getStreamWidth() { + return videoEncoder.getWidth(); + } + + public int getStreamHeight() { + return videoEncoder.getHeight(); + } + + /** + * Set video bitrate of H264 in bits per second while stream. + * + * @param bitrate H264 in bits per second. + */ + public void setVideoBitrateOnFly(int bitrate) { + videoEncoder.setVideoBitrateOnFly(bitrate); + } + + /** + * Set limit FPS while stream. This will be override when you call to prepareVideo method. + * This could produce a change in iFrameInterval. + * + * @param fps frames per second + */ + public void setLimitFPSOnFly(int fps) { + videoEncoder.setFps(fps); + } + + /** + * Get stream state. + * + * @return true if streaming, false if not streaming. + */ + public boolean isStreaming() { + return streaming; + } + + /** + * Get record state. + * + * @return true if recording, false if not recoding. + */ + public boolean isRecording() { + return recordController.isRunning(); + } + + public void pauseRecord() { + recordController.pauseRecord(); + } + + public void resumeRecord() { + recordController.resumeRecord(); + } + + public RecordController.Status getRecordStatus() { + return recordController.getStatus(); + } + + protected abstract void getAacDataRtp(ByteBuffer aacBuffer, MediaCodec.BufferInfo info); + + @Override + public void getAacData(ByteBuffer aacBuffer, MediaCodec.BufferInfo info) { + recordController.recordAudio(aacBuffer, info); + if (streaming) getAacDataRtp(aacBuffer, info); + } + + protected abstract void onSpsPpsVpsRtp(ByteBuffer sps, ByteBuffer pps, ByteBuffer vps); + + @Override + public void onSpsPps(ByteBuffer sps, ByteBuffer pps) { + if (streaming) onSpsPpsVpsRtp(sps, pps, null); + } + + @Override + public void onSpsPpsVps(ByteBuffer sps, ByteBuffer pps, ByteBuffer vps) { + if (streaming) onSpsPpsVpsRtp(sps, pps, vps); + } + + protected abstract void getH264DataRtp(ByteBuffer h264Buffer, MediaCodec.BufferInfo info); + + @Override + public void getVideoData(ByteBuffer h264Buffer, MediaCodec.BufferInfo info) { + fpsListener.calculateFps(); + recordController.recordVideo(h264Buffer, info); + if (streaming) getH264DataRtp(h264Buffer, info); + } + + @Override + public void inputPCMData(Frame frame) { + audioEncoder.inputPCMData(frame); + } + + @Override + public void onVideoFormat(MediaFormat mediaFormat) { + recordController.setVideoFormat(mediaFormat); + } + + @Override + public void onAudioFormat(MediaFormat mediaFormat) { + recordController.setAudioFormat(mediaFormat); + } + + public abstract void setLogs(boolean enable); +} + diff --git a/app/src/main/java/com/fpvout/digiview/streaming/CustomRtmpDisplay.java b/app/src/main/java/com/fpvout/digiview/streaming/CustomRtmpDisplay.java new file mode 100644 index 0000000..eae4258 --- /dev/null +++ b/app/src/main/java/com/fpvout/digiview/streaming/CustomRtmpDisplay.java @@ -0,0 +1,159 @@ +package com.fpvout.digiview.streaming; + +import android.content.Context; +import android.media.MediaCodec; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import net.ossrs.rtmp.ConnectCheckerRtmp; +import net.ossrs.rtmp.SrsFlvMuxer; + +import java.nio.ByteBuffer; + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class CustomRtmpDisplay extends CustomDisplayBase { + + private SrsFlvMuxer srsFlvMuxer; + + public CustomRtmpDisplay(Context context, boolean useOpengl, ConnectCheckerRtmp connectChecker) { + super(context, useOpengl); + srsFlvMuxer = new SrsFlvMuxer(connectChecker); + } + + /** + * H264 profile. + * + * @param profileIop Could be ProfileIop.BASELINE or ProfileIop.CONSTRAINED + */ + public void setProfileIop(byte profileIop) { + srsFlvMuxer.setProfileIop(profileIop); + } + + @Override + public void resizeCache(int newSize) throws RuntimeException { + srsFlvMuxer.resizeFlvTagCache(newSize); + } + + @Override + public int getCacheSize() { + return srsFlvMuxer.getFlvTagCacheSize(); + } + + @Override + public long getSentAudioFrames() { + return srsFlvMuxer.getSentAudioFrames(); + } + + @Override + public long getSentVideoFrames() { + return srsFlvMuxer.getSentVideoFrames(); + } + + @Override + public long getDroppedAudioFrames() { + return srsFlvMuxer.getDroppedAudioFrames(); + } + + @Override + public long getDroppedVideoFrames() { + return srsFlvMuxer.getDroppedVideoFrames(); + } + + @Override + public void resetSentAudioFrames() { + srsFlvMuxer.resetSentAudioFrames(); + } + + @Override + public void resetSentVideoFrames() { + srsFlvMuxer.resetSentVideoFrames(); + } + + @Override + public void resetDroppedAudioFrames() { + srsFlvMuxer.resetDroppedAudioFrames(); + } + + @Override + public void resetDroppedVideoFrames() { + srsFlvMuxer.resetDroppedVideoFrames(); + } + + @Override + public void setAuthorization(String user, String password) { + srsFlvMuxer.setAuthorization(user, password); + } + + /** + * Some Livestream hosts use Akamai auth that requires RTMP packets to be sent with increasing + * timestamp order regardless of packet type. + * Necessary with Servers like Dacast. + * More info here: + * https://learn.akamai.com/en-us/webhelp/media-services-live/media-services-live-encoder-compatibility-testing-and-qualification-guide-v4.0/GUID-F941C88B-9128-4BF4-A81B-C2E5CFD35BBF.html + */ + public void forceAkamaiTs(boolean enabled) { + srsFlvMuxer.forceAkamaiTs(enabled); + } + + @Override + protected void prepareAudioRtp(boolean isStereo, int sampleRate) { + srsFlvMuxer.setIsStereo(isStereo); + srsFlvMuxer.setSampleRate(sampleRate); + } + + @Override + protected void startStreamRtp(String url) { + if (videoEncoder.getRotation() == 90 || videoEncoder.getRotation() == 270) { + srsFlvMuxer.setVideoResolution(videoEncoder.getHeight(), videoEncoder.getWidth()); + } else { + srsFlvMuxer.setVideoResolution(videoEncoder.getWidth(), videoEncoder.getHeight()); + } + srsFlvMuxer.start(url); + } + + @Override + protected void stopStreamRtp() { + srsFlvMuxer.stop(); + } + + @Override + public void setReTries(int reTries) { + srsFlvMuxer.setReTries(reTries); + } + + @Override + public boolean shouldRetry(String reason) { + return srsFlvMuxer.shouldRetry(reason); + } + + @Override + public void reConnect(long delay) { + srsFlvMuxer.reConnect(delay); + } + + @Override + public boolean hasCongestion() { + return srsFlvMuxer.hasCongestion(); + } + + @Override + protected void getAacDataRtp(ByteBuffer aacBuffer, MediaCodec.BufferInfo info) { + srsFlvMuxer.sendAudio(aacBuffer, info); + } + + @Override + protected void onSpsPpsVpsRtp(ByteBuffer sps, ByteBuffer pps, ByteBuffer vps) { + srsFlvMuxer.setSpsPPs(sps, pps); + } + + @Override + protected void getH264DataRtp(ByteBuffer h264Buffer, MediaCodec.BufferInfo info) { + srsFlvMuxer.sendVideo(h264Buffer, info); + } + + @Override + public void setLogs(boolean enable) { + srsFlvMuxer.setLogs(enable); + } +} diff --git a/app/src/main/java/com/fpvout/digiview/streaming/StreamingService.java b/app/src/main/java/com/fpvout/digiview/streaming/StreamingService.java index 2f819d6..08949e2 100644 --- a/app/src/main/java/com/fpvout/digiview/streaming/StreamingService.java +++ b/app/src/main/java/com/fpvout/digiview/streaming/StreamingService.java @@ -16,8 +16,6 @@ import androidx.core.app.NotificationCompat; import androidx.preference.PreferenceManager; -import com.pedro.rtplibrary.rtmp.RtmpDisplay; - import net.ossrs.rtmp.ConnectCheckerRtmp; public class StreamingService extends Service { @@ -29,7 +27,7 @@ public class StreamingService extends Service { private static ConnectCheckerRtmp connectChecker; private static Intent mediaProjectionData; private static int mediaProjectionResultCode; - private static RtmpDisplay rtmpDisplayBase; + private static CustomRtmpDisplay rtmpDisplayBase; private static int dpi; private String endpoint; @@ -64,7 +62,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { private void prepareStreaming() { stopStreaming(); - rtmpDisplayBase = new RtmpDisplay(appContext, true, connectChecker); + rtmpDisplayBase = new CustomRtmpDisplay(appContext, true, connectChecker); if (!sharedPreferences.getString("StreamRtmpUsername", "").isEmpty() && !sharedPreferences.getString("StreamRtmpPassword", "").isEmpty()) { rtmpDisplayBase.setAuthorization(sharedPreferences.getString("StreamRtmpUsername", ""), sharedPreferences.getString("StreamRtmpPassword", "")); } @@ -79,7 +77,7 @@ private void startStreaming() { streamResolution.getHeight(), StreamFramerate.getFramerate(sharedPreferences.getString("StreamFramerate", StreamFramerate.DEFAULT)), StreamBitrate.getBitrate(sharedPreferences.getString("StreamBitrate", StreamBitrate.DEFAULT)), - 0, + sharedPreferences.getBoolean("StreamPortrait", false) ? 90 : 0, dpi )) { boolean audioInitialized; @@ -160,7 +158,7 @@ public static void init(Context context, ConnectCheckerRtmp connectCheckerRtmp) if (rtmpDisplayBase == null) { sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext); - rtmpDisplayBase = new RtmpDisplay(appContext, true, connectChecker); + rtmpDisplayBase = new CustomRtmpDisplay(appContext, true, connectChecker); if (!sharedPreferences.getString("StreamRtmpUsername", "").isEmpty() && !sharedPreferences.getString("StreamRtmpPassword", "").isEmpty()) { rtmpDisplayBase.setAuthorization(sharedPreferences.getString("StreamRtmpUsername", ""), sharedPreferences.getString("StreamRtmpPassword", "")); } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3a8f651..a969ae2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -64,4 +64,6 @@ Only if your device or the audio source supports it Audio sample rate Audio bitrate + Force portrait mode + Your video stream will be rotated in portrait mode \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 37c61f1..143fe17 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -63,4 +63,6 @@ Only if your device or the audio source supports it Audio sample rate Audio bitrate + Force portrait mode + Your video stream will be rotated in portrait mode \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a909ceb..3404df4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -61,4 +61,6 @@ Only if your device or the audio source supports it Audio sample rate Audio bitrate + Force portrait mode + Your video stream will be rotated in portrait mode \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 7972209..8907014 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -63,5 +63,7 @@ Only if your device or the audio source supports it Audio sample rate Audio bitrate + Force portrait mode + Your video stream will be rotated in portrait mode \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 304fb8b..bc7480d 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -63,5 +63,7 @@ Only if your device or the audio source supports it Audio sample rate Audio bitrate + Force portrait mode + Your video stream will be rotated in portrait mode diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9949102..b95d7a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,5 +64,7 @@ Only if your device or the audio source supports it Audio sample rate Audio bitrate + Force portrait mode + Your video stream will be rotated in portrait mode \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 20df726..068d92d 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -91,6 +91,12 @@ app:entryValues="@array/stream_max_bitrate_values" app:useSimpleSummaryProvider="true" /> + + Date: Mon, 7 Jun 2021 17:56:24 +0200 Subject: [PATCH 12/12] Fix autohide buttons --- app/src/main/java/com/fpvout/digiview/MainActivity.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/fpvout/digiview/MainActivity.java b/app/src/main/java/com/fpvout/digiview/MainActivity.java index ba8da5b..ac92c75 100644 --- a/app/src/main/java/com/fpvout/digiview/MainActivity.java +++ b/app/src/main/java/com/fpvout/digiview/MainActivity.java @@ -295,10 +295,13 @@ private void toggleView(View view, boolean visible, float visibleAlpha, @Nullabl } private void toggleView(FloatingActionButton view, boolean visible, @Nullable Runnable runnable) { + if (view.getHandler() != null) { + view.getHandler().removeCallbacksAndMessages(null); + } + if (!visible) { view.hide(); } else { - view.removeCallbacks(runnable); view.show(); view.postDelayed(runnable, 3000); }