From 8d31aa730de984087a22454c2c59dcbb5f7f274c Mon Sep 17 00:00:00 2001
From: bangdc90 <65581036+bangdc90@users.noreply.github.com>
Date: Wed, 25 Sep 2024 22:47:57 +0700
Subject: [PATCH] 1. Add VR feature. Screen will be splitted to 2 when mode is
On. (#10)
2. Add record button to OSD, now user can move that button to main
screen to interact
---
.../com/openipc/pixelpilot/VideoActivity.java | 186 +++++++++++++++++-
.../openipc/pixelpilot/osd/MovableLayout.java | 9 +
.../openipc/pixelpilot/osd/OSDManager.java | 1 +
app/src/main/res/drawable/record.png | Bin 0 -> 9619 bytes
app/src/main/res/drawable/recording.png | Bin 0 -> 10344 bytes
app/src/main/res/layout/activity_video.xml | 57 ++++++
app/videonative/src/main/cpp/VideoDecoder.cpp | 86 ++++----
app/videonative/src/main/cpp/VideoDecoder.h | 16 +-
app/videonative/src/main/cpp/VideoPlayer.cpp | 15 +-
app/videonative/src/main/cpp/VideoPlayer.h | 2 +-
.../com/openipc/videonative/VideoPlayer.java | 58 +++---
11 files changed, 345 insertions(+), 85 deletions(-)
create mode 100644 app/src/main/res/drawable/record.png
create mode 100644 app/src/main/res/drawable/recording.png
diff --git a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java
index 9136845..5642d4f 100644
--- a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java
+++ b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java
@@ -21,15 +21,19 @@
import android.util.Base64;
import android.util.Log;
import android.view.MenuItem;
+import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.WindowManager;
import android.widget.PopupMenu;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.ConstraintSet;
import androidx.documentfile.provider.DocumentFile;
import com.github.mikephil.charting.charts.PieChart;
@@ -87,6 +91,24 @@ public void run() {
private OSDManager osdManager;
private ParcelFileDescriptor dvrFd = null;
private Timer dvrIconTimer = null;
+ private Timer recordTimer = null;
+ private int seconds = 0;
+ private boolean isVRMode = false;
+ private boolean isStreaming = false;
+ private ConstraintLayout constraintLayout;
+ private ConstraintSet constraintSet;
+
+ public boolean getVRSetting() {
+ return getSharedPreferences("general", Context.MODE_PRIVATE).getBoolean("vr-mode", false);
+ }
+
+ public void setVRSetting(boolean v)
+ {
+ SharedPreferences prefs = getSharedPreferences("general", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean("vr-mode", v);
+ editor.apply();
+ }
public static int getChannel(Context context) {
return context.getSharedPreferences("general",
@@ -119,6 +141,19 @@ public static String bytesToHex(byte[] bytes) {
return hexString.toString();
}
+ private void resetApp()
+ {
+ // Restart the app
+ Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
+ if (intent != null) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ finish();
+ System.exit(0); // Ensure the app is fully restarted
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "lifecycle onCreate");
@@ -139,7 +174,84 @@ protected void onCreate(Bundle savedInstanceState) {
setContentView(binding.getRoot());
videoPlayer = new VideoPlayer(this);
videoPlayer.setIVideoParamsChanged(this);
- binding.mainVideo.getHolder().addCallback(videoPlayer.configure1());
+ isVRMode = getVRSetting();
+ if(isVRMode) {
+ binding.mainVideo.setVisibility(View.GONE);
+ binding.surfaceViewLeft.getHolder().addCallback(videoPlayer.configure1(0));
+ binding.surfaceViewRight.getHolder().addCallback(videoPlayer.configure1(1));
+
+ SeekBar seekBar = binding.seekBar;
+ // Retrieve saved progress value
+ SharedPreferences sharedPreferences = getSharedPreferences("SeekBarPrefs", MODE_PRIVATE);
+ int savedProgress = sharedPreferences.getInt("seekBarProgress", 0); // Default to 0 if no value is found
+ seekBar.setProgress(savedProgress);
+ seekBar.setVisibility(View.VISIBLE);
+ constraintLayout = binding.frameLayout;
+ constraintSet = new ConstraintSet();
+ constraintSet.clone(constraintLayout);
+
+ // Apply the saved margin
+ int margin = savedProgress * 10; // Adjust the multiplier as needed
+ constraintSet.setMargin(R.id.surfaceViewLeft, ConstraintSet.END, margin);
+ constraintSet.setMargin(R.id.surfaceViewRight, ConstraintSet.START, margin);
+ constraintSet.applyTo(constraintLayout);
+
+ // Hide SeekBar after 3 seconds
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ seekBar.setVisibility(View.GONE);
+ }
+ }, 3000);
+
+ // Show SeekBar when touched
+ constraintLayout.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ seekBar.setVisibility(View.VISIBLE);
+ // Hide SeekBar again after 3 seconds of inactivity
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ seekBar.setVisibility(View.GONE);
+ }
+ }, 3000);
+ }
+ return false;
+ }
+ });
+
+ seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ int margin = progress * 10; // Adjust the multiplier as needed
+ constraintSet.setMargin(R.id.surfaceViewLeft, ConstraintSet.END, margin);
+ constraintSet.setMargin(R.id.surfaceViewRight, ConstraintSet.START, margin);
+ constraintSet.applyTo(constraintLayout);
+ // Save progress value
+ SharedPreferences sharedPreferences = getSharedPreferences("SeekBarPrefs", MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putInt("seekBarProgress", progress);
+ editor.apply();
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+ }
+ else {
+ binding.surfaceViewRight.setVisibility(View.GONE);
+ binding.surfaceViewLeft.setVisibility(View.GONE);
+ binding.mainVideo.getHolder().addCallback(videoPlayer.configure1(0));
+ }
osdManager = new OSDManager(this, binding);
osdManager.setUp();
@@ -160,8 +272,37 @@ protected void onCreate(Bundle savedInstanceState) {
PieData noData = new PieData(new PieDataSet(new ArrayList<>(), ""));
chart.setData(noData);
+ binding.imgBtnRecord.setOnClickListener(item -> {
+ if(!isStreaming) return;
+
+ if (dvrFd == null) {
+ Uri dvrUri = openDvrFile();
+ if (dvrUri != null) {
+ startDvr(dvrUri);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ startActivityForResult(intent, PICK_DVR_REQUEST_CODE);
+ }
+ } else {
+ stopDvr();
+ }
+ });
+
binding.btnSettings.setOnClickListener(v -> {
PopupMenu popup = new PopupMenu(this, v);
+ SubMenu vrMenu = popup.getMenu().addSubMenu("VR mode");
+ MenuItem vrItem = vrMenu.add(getVRSetting() ? "On" : "Off");
+ vrItem.setOnMenuItemClickListener(item -> {
+ isVRMode = !getVRSetting();
+ setVRSetting(isVRMode);
+ vrItem.setTitle(isVRMode ? "On" : "Off");
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
+ item.setActionView(new View(this));
+ resetApp();
+ return false;
+ });
+
SubMenu chnMenu = popup.getMenu().addSubMenu("Channel");
int channelPref = getChannel(this);
chnMenu.setHeaderTitle("Current: " + channelPref);
@@ -211,6 +352,7 @@ protected void onCreate(Bundle savedInstanceState) {
SubMenu recording = popup.getMenu().addSubMenu("Recording");
MenuItem dvrBtn = recording.add(dvrFd == null ? "Start" : "Stop");
dvrBtn.setOnMenuItemClickListener(item -> {
+ if(!isStreaming) return false;
if (dvrFd == null) {
Uri dvrUri = openDvrFile();
if (dvrUri != null) {
@@ -285,6 +427,7 @@ private Uri openDvrFile() {
String dvrFolder = getSharedPreferences("general",
Context.MODE_PRIVATE).getString("dvr_folder_", "");
if (dvrFolder.isEmpty()) {
+ Log.e(TAG, "dvrFolder is empty");
return null;
}
Uri uri = Uri.parse(dvrFolder);
@@ -297,6 +440,8 @@ private Uri openDvrFile() {
String filename = "pixelpilot_" + formattedNow + ".mp4";
DocumentFile newFile = pickedDir.createFile("video/mp4", filename);
Toast.makeText(this, "Recording to " + filename, Toast.LENGTH_SHORT).show();
+ if(newFile == null)
+ Log.e(TAG, "dvr newFile null");
return newFile != null ? newFile.getUri() : null;
}
return null;
@@ -309,10 +454,26 @@ private void startDvr(Uri dvrUri) {
try {
dvrFd = getContentResolver().openFileDescriptor(dvrUri, "rw");
videoPlayer.startDvr(dvrFd.getFd(), getDvrMP4());
+ binding.imgBtnRecord.setImageResource(R.drawable.recording);
} catch (IOException e) {
Log.e(TAG, "Failed to open dvr file ", e);
dvrFd = null;
}
+
+ binding.txtRecordLabel.setVisibility(View.VISIBLE);
+ recordTimer = new Timer();
+ recordTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ int minutes = seconds / 60;
+ int secs = seconds % 60;
+
+ String timeFormatted = String.format("%02d:%02d", minutes, secs);
+ runOnUiThread(() -> binding.txtRecordLabel.setText(timeFormatted));
+ seconds++;
+ }
+ }, 0, 1000);
+
dvrIconTimer = new Timer();
dvrIconTimer.schedule(new TimerTask() {
@Override
@@ -328,10 +489,20 @@ private void stopDvr() {
return;
}
binding.imgRecIndicator.setVisibility(View.INVISIBLE);
+ binding.imgBtnRecord.setImageResource(R.drawable.record);
videoPlayer.stopDvr();
- dvrIconTimer.cancel();
- dvrIconTimer.purge();
- dvrIconTimer = null;
+ if(recordTimer != null) {
+ recordTimer.cancel();
+ recordTimer.purge();
+ recordTimer = null;
+ seconds = 0;
+ binding.txtRecordLabel.setVisibility(View.GONE);
+ }
+ if(dvrIconTimer != null) {
+ dvrIconTimer.cancel();
+ dvrIconTimer.purge();
+ dvrIconTimer = null;
+ }
try {
dvrFd.close();
} catch (IOException e) {
@@ -511,9 +682,6 @@ public void onDecodingInfoChanged(final DecodingInfo decodingInfo) {
runOnUiThread(() -> {
if (lastCodec != decodingInfo.nCodec) {
lastCodec = decodingInfo.nCodec;
- videoPlayer.stopAndRemoveReceiverDecoder();
- videoPlayer.addAndStartDecoderReceiver(binding.mainVideo.getHolder().getSurface());
- videoPlayer.start();
}
if (decodingInfo.currentFPS > 0) {
binding.tvMessage.setVisibility(View.GONE);
@@ -574,9 +742,13 @@ public void onWfbNgStatsChanged(WfbNGStats data) {
paddedDigits(data.count_p_dec_ok, 6),
paddedDigits(data.count_p_fec_recovered, 6),
paddedDigits(data.count_p_lost, 6)));
+ isStreaming = true;
}
} else {
binding.tvLinkStatus.setText("No wfb-ng data.");
+ isStreaming = false;
+ binding.imgBtnRecord.setImageResource(R.drawable.record);
+ stopDvr();
}
});
}
diff --git a/app/src/main/java/com/openipc/pixelpilot/osd/MovableLayout.java b/app/src/main/java/com/openipc/pixelpilot/osd/MovableLayout.java
index 7d49e0a..74b8846 100644
--- a/app/src/main/java/com/openipc/pixelpilot/osd/MovableLayout.java
+++ b/app/src/main/java/com/openipc/pixelpilot/osd/MovableLayout.java
@@ -42,6 +42,15 @@ private void init(Context context) {
defaultY = (float) displaySize.y / 2 - ((float) displaySize.y / 4);
}
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ // Intercept touch events and pass them to onTouchEvent
+ if (isMovable) {
+ return true;
+ }
+ return false;
+ }
+
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isMovable) {
diff --git a/app/src/main/java/com/openipc/pixelpilot/osd/OSDManager.java b/app/src/main/java/com/openipc/pixelpilot/osd/OSDManager.java
index 9e78f3a..719feee 100644
--- a/app/src/main/java/com/openipc/pixelpilot/osd/OSDManager.java
+++ b/app/src/main/java/com/openipc/pixelpilot/osd/OSDManager.java
@@ -91,6 +91,7 @@ public void onFinish() {
listOSDItems.add(new OSDElement("Pitch", binding.itemPitch));
listOSDItems.add(new OSDElement("RC Link", binding.itemRCLink));
listOSDItems.add(new OSDElement("Recording Indicator", binding.itemRecIndicator));
+ listOSDItems.add(new OSDElement("Recording Button", binding.btnRecord));
listOSDItems.add(new OSDElement("Roll", binding.itemRoll));
listOSDItems.add(new OSDElement("Satellites", binding.itemSat));
listOSDItems.add(new OSDElement("Status", binding.itemStatus));
diff --git a/app/src/main/res/drawable/record.png b/app/src/main/res/drawable/record.png
new file mode 100644
index 0000000000000000000000000000000000000000..2bc447cbce24fd7eec01bd388e7557b470b5de1a
GIT binary patch
literal 9619
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
-K!_AMdxk$NSfN9CdJAuI0SW^LOs|IcFzZim03@1VI#r
zos}yD;lQsrXay1c_!iUm3;ZCn?Dj`MaKJ3~AKrS5OLDA9C
zI)S0V5l4JjemY^{0fkehauB2pF{~`yVlMylHunB?>)7d;Fh=j+gJPQ1yEf>S-g03P
zR=d5sAZL}fp;{uj;O_ErA^9vy+bsA|WF!I)h4|?jcL)~_kk2
z?}JxPQ_rKEn8%S^&i~%SR@`YBH9|o$IMwWGPBCxZP{+PNL>>yXOLC0OC|$jPC#{pe
zGb0h$lZ#vHTmV*0-?&G1>G2$d?DU@VA$p|XxFp|Xv9RnQQRkXIeuPk8)VeW^yLVJV
zFHoiK&VU(-PKeRIvNT!YnklOpMIR+BqW0>ve5{9JM5Iqs!bAjkdp%Z=rkad>(R&H2D0`hxW9zmF
z>1V@NrR$fEA#?0K>g|XlIJT+^cAgE__`3V%liq|?I|?gbc;Mnz(P4ViO>&>HJY=w`
zX)m&gGk|+cXmM%q!Y_~non>)41}#K$$Nit*E>`6nT=>$g>e{JIs&}{~dO5dMk?2Yd
zf%^VvR55A6RT3Xaq;IU~=to*ORM)yikMaTvyFAxt9lBfN%lz=Io6=g>LgW@w-0m9Y
zmiywbYL4AYT2jpE{kd?ed~XqDL6P;SkmxR-z%Q4pyZm^`742j{O?q$lJ`xsm=2#{_
zsrPW_!_Kvh29FbVX)L@EX~|Okd=ia8YY9fSDJ~7KAoX3z?dsM`j!1LkabBROSR6sf
zOtkLy>Dz4x6+H4f|GOABO^_zEWNmm$j9V=nA%~`km&RAYU46cTE9k_SwSq2`?&=td
zTOz1hu@&0P?;42<}Ev*;fWWC
za%$7|10KGErg<#|I)A;}oc5*>sgq6b6l9KXnINh`1=(Jq@8^%So*c^s@7hs&Cj5yY
z#kDTlT<#o&ZDZ*1X-Sg54;+`GBQksi-PCQ-rz#J%d+M}&k6%sSV4O;G!VhPnkH(dQ
zQcpqp_r)}ts*uhqp+il~4%YExqkx1P!P1wZqw2CJM4X^2(nZ!i7dmi<3emOQ7H(#b
ztt(uTV`Yb7uRor7m%p_@I(44}uq5;JHv+<3l7DkD;EeQ+4S_Fp20}73o=_t
zxyeR$huDHlYC~W7Q_ggN&^(z^+H`A&6BKDCD-P`tnZ&LcT)@#a8|%O|5^4}`>h0)~
zKN_O7=sGYdzHpRp#4H-$Ql3PGvV2IWFRIbKpXN9e&DqBbe}DDl-(>SnznE15J+R!{
zlv1nSw9cC3q9xD1(#j7^ey=fbAylczw#RL|jLp%`U%YrZ5=wO14laRWPCp@O4l+@<$XMT@oNsE4Q@DO2q|X!
z>_*#a+q`>(Ua&$I${kmhTW4#c$Q@;(T3Us
zNk>nFKyukW3FsHZ5}ee7P!K)p<#mcyO%YV4rLL*8&=9aQDprQ&pR8s@>K1_IS)(ry
z*_=I#v@*k$>@Z5lMi)Go%^YR54kl!eYjN)rvMyK9*>z
z>>(LNt6+b;bCuZUQqzftha#mJ1!sOD8nTCE6|MBD;ah!ZZToIlr(rJd0p(3)0(Kk7X*zp?BXnXc0$Uz#+|F(qBqm0ipae^NOj&p4RH8Bn9|Fg`nVB&*C$
zC*aqTN&ReI-}X)HeKet4q~sby=;CvMDeC40W)A|?T>o0_)EEM3W{%1td8~&Gyr}~?
zQ?rZnyH-CZDtjd3IbqJ?U70{pv%@)Em%1%}{Y_MMPHw@E{7GG;eeXPsb`d^Vkv?yd
zXDiG85UEcJn0J_{v_=}q9ckh{wuTU_J1MpRg?-QFec`ClJY*DkVR1ttt>LjPOYT7U
z1pXR9zgVF)>9f2%Tcee)Gd7cFYZH*gJ8vpV*Pf^KxxIpRF?8Cr)o{BrIyNxYoD?0K
zg7R+M{ENsuIZRSeRNTqc{Gp4!?Oy-uPp4o};-~opQ{xnxfik|*o)sRMdAAzQ=Zo#W
zuJBFIjDDV0b6A5Flc-mc6nTk)5c~%5ba@ZSB5ST(wG3Uo@l3bHqgEnwCoyCGH(^i%
zh|6Fi9IYx-kl}5OprV8iUmwLwr8dyofo5sQzC0)1V{Hg!&+I@2x1l(~2ZXZ4&XEd#Q{Uc;M
z6p=G}4oQG#ORDW$cWYf&kcxDQ%*=fS{cVw)#);4wc;O
zG@s||2}Z{ywIgs_O2jtfoYFehGq%t55N=wf9L=I2D|AChmv?WQ_U5h*Sj7)Zyj_}f
zzgdO7U+(g3f-cxz-iSk~!)v#kGK8w&T^NW=1qC2O3ZXk11y_N3`l%>a62NJjQ^Je>_QpoG4D}hXEeAl6!
zC6;|1WJJz4hQ?Cj$72ic3{fVFF`H8pj@y#d$T3^j^lmdl+fzcAH!1&C7I10uJ7R%R
zzwwkPYggo1&sVY)Hp$1>#e358b7(ceTluz$!KF!k&17Wa_a^Z2i9|=vdvMWnGNP$#
zDo@`I;1+mC+(D2wE@@B&d%Qp|EzPpG13SWPo{(y-X}m(&0;v4`bQPd7s=JhZq|~zS
zFEV1D+|KcKjlW2X>eNOyN)4@6Wzrnm8PNPQk=?=8@8%N~2=8#f^$eD%;3sCXd#*7x@B?8-ELq
zyS+Z)%*{mx_q;r(h&m*#=v(lM4#!}u>pA^^Ff6Hf`1MHx7=)v>+;FA)03aTO$(+{8
zr(F-pNHNd?+V(CSs@=W*MwtOmXUro{+bW+mwVx-4?3X+NkV-!{9+_
zq1(~?amnnS7i4ptwDwB2BG6TC!3m0=DBslhd2Lh_jz!1#GjvQsK}zwk)T7tX6GLM*
ztxV5&{~qIqgiN1JtrZDhfMKLX{i)lk%_({W`!WwbitOZ
zFB=WNQLWu({G1*ch!aR*yI$;H7vlnUeRjnI7D5cyPf<5sMjj?BH@(c6Nyw9vW4NBq
z-IEK4o)y>KcTgjL^EDIiR9e}gFVN--;#{BO{$k>
zxKj5*Z`5jajGuc&`rueJ?DgMF&x&i|Bd^1mw7`Ob%I%H~5gmN$Slq`O4dM*f2wnaW
z&4-;g;v6@#kDx9Dn
zn0(JaROAOlo^`qZ8d6YFH0Sa-s(G;IF{mJ}N`+C>_o1-o;}6K!!#s8&4D;b!wn|P5
z9F{7sE3fSYi9Z7;l()oT#?k-wGa@RBUF!KvuN43H-c$;bT>>C>|9nyj816X*>4uOO
za+|{)xW-{3{8)z;+(Sb+W&_}h>uDu|>3ZY8Lhge!j_X#62m5V;Ddu}N-=&r9D=2qq
zztF39hopsjiFwGLpdnbxj<0Y0e2ZrRWGV;DMA;#E+`LVS%w7kusdNp)b+f@N{VIh`
zW4!O(RS^^@fqX}rN+JL;!~vB-L*u`;6y!s`gD|!={*CXTDmHTzVxPtc*5|1PxIitD)%;Ja9=W8#-U)>Q+WOnfN!pi+xNo3piW?)$b335?TiRM
zqx_p`VSyTCVvC)fuB4>MR5dVy`s)Td%ARj`6^De;mKo4g0%B;Q4;b!&|RC
z_kw}-e+>Nb%8Z^5ZU@?eLZNcSLX4e3!TZ}O<~0Yi0iVhrKXt~!(TzyGoHh}^O
zE$s!d8{T^p44*C!85a#Iz#twYteMwZuuPcmHaP!`rneU0g7aA6YfpLh|7+mqc)JE>
z;egVe*9-<(G2rVMfhk=cS4CsUSzHVY3O)w?rvZ)8L+F1EX`KU#+LJ$|sK|^8`ahQZ
zr}h7z!7+%&C;(Z}{##$4acO}B_xiGm8u-6CsNIgi^hmgEB)h+?2CmikcL?y%&j5^3
z%WL)}^PMmq{>aqeA4*hE^mTW4f~4+1($>JM2BwGJtrA*Zry%ARH&Pi~|7**FMRx#;
z_@%tU4}-%AM<9f<<$3301K`n_|J5H1flwTsYHIF$B%;ZfNZPx@fMWB&u>9ZQpBS*d
zsx#SQg-~iKP)~i5{7w7KwYN{;GEqbHBXDE1wDDxmR5Q0BNNhn9d(lQu8AYaJP#7d_
z^RFH>oU3>W-}?0|n-)j`a~|11EzV$eNY14{?C>&6FM#n&-Vfr{i75t`XK?FLvim7<
z2qd}c8P0A+d$>Gz-~Z6Jekz*~jcYi!tc3^nM9ieL%Ck8um+gYXCtH$-{Icdw1JZ+g
zcjBFYrvitlSzdV%du^Kc_uelP(E|bbyX$0*V$m5lnPoI4jret2yyX2NndV@KOg(F&0q;63vcY7g|~Z+3R7}b6VfMGz4H#F5^a!o8Bo1SZ?Bw
z-KjL-_TJ>;57252jGyxhGDGnAQ^OfgM_n
zJ*3?++n}s&I|S8zSTx`}f_J!99?WK@j7ydt4aa3F_oC?zz;VYJ7$dg|LLg#+LY*Dg
z`1*_Q2}dKz(A0Km;X~vJ7Bd|36gB15P@(DWNdl9lCCI>>!pSd_dsEHy0mS0g1BjJ9
zbV?Q9vbJ3Z1};LW_I&fQZ{7=;&}t2hpWiCD0?}%TyG3$njw5rPI*VN~_$`7<;Gw+X
z!1^EhX2zX-gE8l;I{JEC-mePifrFrMTsfjUu3I4vU%w7;t4}`Q!J&m~cnX4OEIaA$
z<;Vfxu|mBK`eKEJK(BR~Z`{O7f<4t#A@W^P{aS2a?8xVxGb!o%5WYwTi#T3h^6WjQ
z0{D8s`N|8g&A<DKoNbK`vkNV$=Me8wPMH1q84nuKvCeoI#w7JE4t-O(sIfPM@E^Ru5h(s=Y$)TkkB>u2x9goape4fOJu||wAi5ARW)LEM_j!A}tN#t-1m;i;-me33uBEps^aZyMdSeTO;Qo&$bAZrR
zlC;ZnGmt_qZV4mU?Wx4vHKFBhRnVZ2b~b+Oh^0a0_g8?({TRp>uB`$!z@Vo
zOVYgS!Zq7v{@noli^bUB9*R5R1U&Ea```R=P$s%JZi6}Zn|Z>>&36*p3n2iyd=TDa
zWg2d|z@+u@(g+CRZya)jfLzdyVgb<0*CRhFgqENT2gw>jAhkJ%r8eK?nR$e08`7l*
z!^>;OmtzUhIiu5Bi(S2BfUoZyZ`_;Z{H9iJP`mMMx5b8U;TQ=zXc)%fKc7@qK3jLv
z6lR~AUQxSZQ2XD+3jXoAt*YU6?;A2SbjlVa4q)kxsbi-B_GgKJ>7S8-<*AR~K6L($
z-wY3JY$|2XYB=DyyeUu7$s)98T)F$C84TI^X^(qjOI4@nsR^S5rgD=%;)mp7$q#kL
zr1eMw$o={dkU1tEV9(hA1Umlh?YS`hp1@R*eF?`CCl!J5(BErD`tfDHW9i~H*n^Qc
zh8)0-Y@Vu+UYZ9KTT$rT#!OWLud!tJNbM`7RXgUAgo4Md4&m%8-&6(qL4F@uwVnc<3l5_Lh}*a|BdxF%CH63R|s@vdh@DhKcS
z3c6Lp!foLZq9I$@0egrN8j7l?E`d^=|N6!{#2fSC+mXkvu^|U6!^zCt@W6ulGb94T
zRl8n9A(v?lN+zH}qulf$r)AfJv*6(E(JMjSBN)_7K;2^%tyY?D2*Sa01N#?jkKnhI
z5Q<4o=dqf}iJggiN-eR%%C_hHRend%P{4z-03DK25&B?UxdLc2i5yBg#8Go%^yq8D
zhk|B`L9uI_y
zi@#zL2dq-YSN>ZK6u6_A;Apo?dYph8_ZBNP_;CI`;3%iWK-!T?bANXjMF>DEzMZ3(
zv&BqsW&|Z^1#51}L6YhE#(q$(Y<6VBQd$J@>!zPH8K-Mj#5{OrmP|3vB&@}y;2#2I
z=Rv6)OX}sdioD7Y2&Y0P(ZxWhm`Zc(Rzy~j;6wU!
znj;4kO}EtU6kbQ}0Sfgk+Bl_8UOyBE^;J88HR_j>pv>(v*u*ZS#u*x)6_sb77hCjE
zNSY(sP2`F_+0mF_L+U;aj>txr;IKwjb5|o@)RF9TR=i|>R{5&EAH5(v8Zib)3bbnD
z45-kz8lP2qPP}@!E=0`qQg{j*)VsC`Cfwqa%XWe*+XM=z8}m0t2u}rZpk9_GffGW5FsDfK1tHC$-gi>>Oa&PUoy;+(ZSN1fKXD9o{hzX*Y
zZQ!8$=MS)(s0-@GXOoZMyYj{6p%Li8d2kSZ0Pfty0=))GX9TF4qc{|LV&UMbsDrz5
zbS5l=VxA=BwbJy`Jq2ZkO9v!w&vK8HY`;P=FO?c1D!>Xr>s>b&S?!-~FfcxA1CHhs
z+N2i9N{U*6Cv^qGZsI@p%PHEFg2KD4FI`F)ZMwy-=L6t*Hku9k3DKaRz#TP5R|@m5
z@@5{N2}e2;tV!K5gHYtIWLp;}$`B}b@j7b7giVAzz+z!7>FN>5%4E&BW8KU-@kLNS&1-+o@qN4_F&p{PhYyNBj
zxLiF!MPTWr_;6fTgzO>cG8D23G@1IeaEFS~-foN4ppEG}cs{&(zz2}yI3$Z~A`P6s
z-QtCl1Od?%>pJ%?N}{Wgw#e{GQ(xaRpCCwNHTEq4D1NeVSKWOFR|-LM8i=|1qg1O+
zLow(#ZNChS$4hw;7GMF)jAbb>BC5rNBy_8ELPaZ??VsaghK{iDKm`hFR)DUlb*VZ3APy-h*!Hldcz3>=y?j?`a2$8_n(v?pU5QQye3bod#JaO@E=a}+
z($696gHmvnyrI>qWlE{)EpcZ3?<#$^!CiO!28qIk9Lqmw&x}Q4A`<0Gl5T-IiheNp
zu7hA_Yd9j(9m~7&h(I@ObkJUuI|CYqxu^-UnB&J8A+}`wdgtHTJoJoF;I
z^9JBQP1p}{v!L;M@cwGyTC{S{{@J{be-X`(T=X$&UB9yvga%R1A95bjgt3v5Q7II*
z>V~HEND2ol-%=in#T-biv?968)soDqS*<3yzd2^`U93O)Q4_5}u1F_1o~J^NUOK06
zheQ?(##ZfQ+@3ptCLy*`Lu(Xm-umOOmc8IBjh*An;SPWnZ&3I~K?ZWXOkr%3<7Eo_
zhE9_WvK4vEP65wARtybIk!mk%Z
z^HJRp+!MJNdA=n}A&?u7zih6tu%oTg*$^tX(b;|7R9?6eodM0&;i`3@I
z{0*U?ogg%DL~e6ZNG(}=1J<)P2&xDJq=2lKt)P!CJ4uZ(26=+llNh_ITxf*gMBGsf
zu%i@s6l{Ho68~I_4qMsL)ti$r1!T9r-=1du@|N<
zxEcOrw1bZYZu$-?#H?b`1a8ctF7u$by6EE^$J52LuTDpSDvoh*mzZhFy&Po)ExS{-){vf|kL|2!Z2x07lnz}~B(X{=~5
zT53p1o>87nVe9iOEcxsFMi7gn43TTsB2C6_Jgrv$@-slKcAFu$j|e{Eu&TSfm2Wa<
zK-i5tapCVi`~co;v_3JxNwa0fzCZ;mxIk>7PBXS$Vr4Xb%4jYW3kcQ_IJ6!+x%|n<
z6|}X>%{2uS%&56O2hl8K7}=4O@s$0~%m|Zs_r{$!-YYJf3BUKa844H$ne|J8tUo`5
zXOEu9QIj}9d~nU{R9xqDbcy_uP1v<35e?jMq$6t6@5QyDJ=upNM^jS!7k;W3D&?UV7LeQ|wH;?Vtqj1k!A?zf*i(S78pd{-StH50EKWu>#57UJ|O1sW4e
zIu&X&lyfiexDx*b+HlNmaqy|g$*p5sAB`n-$}TiKCKZq1h9*b1{ZD>41n#;xzvPx#
WwyI`k?hE*!3SwA0S>4^qO#EN%@z*{8
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable/recording.png b/app/src/main/res/drawable/recording.png
new file mode 100644
index 0000000000000000000000000000000000000000..19a202a5782e5b119c2ccea96277b968f0744731
GIT binary patch
literal 10344
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
literal 0
HcmV?d00001
diff --git a/app/src/main/res/layout/activity_video.xml b/app/src/main/res/layout/activity_video.xml
index 79ee930..a0e3a09 100644
--- a/app/src/main/res/layout/activity_video.xml
+++ b/app/src/main/res/layout/activity_video.xml
@@ -15,6 +15,38 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/videonative/src/main/cpp/VideoDecoder.cpp b/app/videonative/src/main/cpp/VideoDecoder.cpp
index 9aa6fa5..e7ded3a 100644
--- a/app/videonative/src/main/cpp/VideoDecoder.cpp
+++ b/app/videonative/src/main/cpp/VideoDecoder.cpp
@@ -21,34 +21,35 @@ VideoDecoder::VideoDecoder(JNIEnv *env) {
resetStatistics();
}
-void VideoDecoder::setOutputSurface(JNIEnv *env, jobject surface) {
+void VideoDecoder::setOutputSurface(JNIEnv *env, jobject surface, jint idx) {
if (surface == nullptr) {
MLOGD << "Set output null surface";
//assert(decoder.window!=nullptr);
- if (decoder.window == nullptr) {
+ if (decoder.window[idx] == nullptr || decoder.codec[idx] == nullptr) {
//MLOGD<<"Decoder window is already null";
return;
}
std::lock_guard lock(mMutexInputPipe);
inputPipeClosed = true;
- if (decoder.configured) {
- AMediaCodec_stop(decoder.codec);
- AMediaCodec_delete(decoder.codec);
+ if (decoder.configured[idx]) {
+ AMediaCodec_stop(decoder.codec[idx]);
+ AMediaCodec_delete(decoder.codec[idx]);
+ decoder.codec[idx] = nullptr;
mKeyFrameFinder.reset();
- decoder.configured = false;
- if (mCheckOutputThread->joinable()) {
- mCheckOutputThread->join();
- mCheckOutputThread.reset();
+ decoder.configured[idx] = false;
+ if (mCheckOutputThread[idx]->joinable()) {
+ mCheckOutputThread[idx]->join();
+ mCheckOutputThread[idx].reset();
}
}
- ANativeWindow_release(decoder.window);
- decoder.window = nullptr;
+ ANativeWindow_release(decoder.window[idx]);
+ decoder.window[idx] = nullptr;
resetStatistics();
} else {
MLOGD << "Set output non-null surface";
// Throw warning if the surface is set without clearing it first
- assert(decoder.window == nullptr);
- decoder.window = ANativeWindow_fromSurface(env, surface);
+ assert(decoder.window[idx] == nullptr);
+ decoder.window[idx] = ANativeWindow_fromSurface(env, surface);
// open the input pipe - now the decoder will start as soon as enough data is available
inputPipeClosed = false;
}
@@ -83,8 +84,9 @@ void VideoDecoder::interpretNALU(const NALU &nalu) {
mKeyFrameFinder.saveIfKeyFrame(nalu);
return;
}
- if (decoder.configured) {
- feedDecoder(nalu);
+ if (decoder.configured[0] || decoder.configured[1]) {
+ feedDecoder(nalu, 0);
+ feedDecoder(nalu, 1);
decodingInfo.nNALUSFeeded++;
// manually feeding AUDs doesn't seem to change anything for high latency streams
// Only for the x264 sw encoded example stream it might improve latency slightly
@@ -98,14 +100,17 @@ void VideoDecoder::interpretNALU(const NALU &nalu) {
mKeyFrameFinder.saveIfKeyFrame(nalu);
if (mKeyFrameFinder.allKeyFramesAvailable(IS_H265)) {
MLOGD << "Configuring decoder...";
- configureStartDecoder();
+ configureStartDecoder(0);
+ configureStartDecoder(1);
}
}
}
-void VideoDecoder::configureStartDecoder() {
+void VideoDecoder::configureStartDecoder(int idx) {
+ if(decoder.window[idx] == nullptr)
+ return;
const std::string MIME = IS_H265 ? "video/hevc" : "video/avc";
- decoder.codec = AMediaCodec_createDecoderByType(MIME.c_str());
+ decoder.codec[idx] = AMediaCodec_createDecoderByType(MIME.c_str());
AMediaFormat *format = AMediaFormat_new();
AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, MIME.c_str());
@@ -127,7 +132,7 @@ void VideoDecoder::configureStartDecoder() {
MLOGD << "Configuring decoder:" << AMediaFormat_toString(format);
- auto status = AMediaCodec_configure(decoder.codec, format, decoder.window, nullptr, 0);
+ auto status = AMediaCodec_configure(decoder.codec[idx], format, decoder.window[idx], nullptr, 0);
AMediaFormat_delete(format);
switch (status) {
@@ -161,26 +166,28 @@ void VideoDecoder::configureStartDecoder() {
}
- if (decoder.codec == nullptr) {
+ if (decoder.codec[idx] == nullptr) {
MLOGD << "Cannot configure decoder";
//set csd-0 and csd-1 back to 0, maybe they were just faulty but we have better luck with the next ones
//mKeyFrameFinder.reset();
return;
}
- AMediaCodec_start(decoder.codec);
- mCheckOutputThread = std::make_unique(&VideoDecoder::checkOutputLoop, this);
- NDKThreadHelper::setName(mCheckOutputThread->native_handle(), "LLDCheckOutput");
- decoder.configured = true;
+ AMediaCodec_start(decoder.codec[idx]);
+ mCheckOutputThread[idx] = std::make_unique(&VideoDecoder::checkOutputLoop, this, idx);
+ NDKThreadHelper::setName(mCheckOutputThread[idx]->native_handle(), "LLDCheckOutput");
+ decoder.configured[idx] = true;
}
-void VideoDecoder::feedDecoder(const NALU &nalu) {
+void VideoDecoder::feedDecoder(const NALU &nalu, int idx) {
+ if(!decoder.codec[idx])
+ return;
const auto now = std::chrono::steady_clock::now();
const auto deltaParsing = now - nalu.creationTime;
while (true) {
- const auto index = AMediaCodec_dequeueInputBuffer(decoder.codec, BUFFER_TIMEOUT_US);
+ const auto index = AMediaCodec_dequeueInputBuffer(decoder.codec[idx], BUFFER_TIMEOUT_US);
if (index >= 0) {
size_t inputBufferSize;
- uint8_t *buf = AMediaCodec_getInputBuffer(decoder.codec, (size_t) index,
+ uint8_t *buf = AMediaCodec_getInputBuffer(decoder.codec[idx], (size_t) index,
&inputBufferSize);
// I have not seen any case where the input buffer returned by MediaCodec is too small to hold the NALU
// But better be safe than crashing with a memory exception
@@ -194,7 +201,7 @@ void VideoDecoder::feedDecoder(const NALU &nalu) {
std::memcpy(buf, nalu.getData(), (size_t) nalu.getSize());
const uint64_t presentationTimeUS = (uint64_t) duration_cast(
steady_clock::now().time_since_epoch()).count();
- AMediaCodec_queueInputBuffer(decoder.codec, (size_t) index, 0, (size_t) nalu.getSize(),
+ AMediaCodec_queueInputBuffer(decoder.codec[idx], (size_t) index, 0, (size_t) nalu.getSize(),
presentationTimeUS, flag);
waitForInputB.add(steady_clock::now() - now);
parsingTime.add(deltaParsing);
@@ -217,13 +224,15 @@ void VideoDecoder::feedDecoder(const NALU &nalu) {
}
}
-void VideoDecoder::checkOutputLoop() {
+void VideoDecoder::checkOutputLoop(int idx) {
NDKThreadHelper::setProcessThreadPriorityAttachDetach(javaVm, -16, "DecoderCheckOutput");
AMediaCodecBufferInfo info;
bool decoderSawEOS = false;
bool decoderProducedUnknown = false;
while (!decoderSawEOS && !decoderProducedUnknown) {
- const ssize_t index = AMediaCodec_dequeueOutputBuffer(decoder.codec, &info,
+ if(!decoder.codec[idx])
+ break;
+ const ssize_t index = AMediaCodec_dequeueOutputBuffer(decoder.codec[idx], &info,
BUFFER_TIMEOUT_US);
if (index >= 0) {
const auto now = steady_clock::now();
@@ -234,22 +243,27 @@ void VideoDecoder::checkOutputLoop() {
//-> renderOutputBufferAndRelease which is in https://android.googlesource.com/platform/frameworks/av/+/3fdb405/media/libstagefright/MediaCodec.cpp
//-> Message kWhatReleaseOutputBuffer -> onReleaseOutputBuffer
// also https://android.googlesource.com/platform/frameworks/native/+/5c1139f/libs/gui/SurfaceTexture.cpp
- AMediaCodec_releaseOutputBuffer(decoder.codec, (size_t) index, true);
+ if(!decoder.codec[idx])
+ break;
+ AMediaCodec_releaseOutputBuffer(decoder.codec[idx], (size_t) index, true);
//but the presentationTime is in US
- decodingTime.add(std::chrono::microseconds(nowUS - info.presentationTimeUs));
- nDecodedFrames.add(1);
+ if(idx == 0)
+ {
+ decodingTime.add(std::chrono::microseconds(nowUS - info.presentationTimeUs));
+ nDecodedFrames.add(1);
+ }
if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) {
MLOGD << "Decoder saw EOS";
decoderSawEOS = true;
continue;
}
} else if (index == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
- auto format = AMediaCodec_getOutputFormat(decoder.codec);
+ auto format = AMediaCodec_getOutputFormat(decoder.codec[idx]);
int width = 0, height = 0;
AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_WIDTH, &width);
AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_HEIGHT, &height);
MLOGD << "Actual Width and Height in output " << width << "," << height;
- if (onDecoderRatioChangedCallback != nullptr && width != 0 && height != 0) {
+ if (idx == 0 && onDecoderRatioChangedCallback != nullptr && width != 0 && height != 0) {
onDecoderRatioChangedCallback({width, height});
}
MLOGD << "AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED " << width << " " << height << " "
@@ -267,7 +281,7 @@ void VideoDecoder::checkOutputLoop() {
//every 2 seconds recalculate the current fps and bitrate
const auto now = steady_clock::now();
const auto delta = now - decodingInfo.lastCalculation;
- if (delta > DECODING_INFO_RECALCULATION_INTERVAL) {
+ if (idx == 0 && delta > DECODING_INFO_RECALCULATION_INTERVAL) {
decodingInfo.lastCalculation = steady_clock::now();
decodingInfo.currentFPS = (float) nDecodedFrames.getDeltaSinceLastCall() /
(float) duration_cast(delta).count();
diff --git a/app/videonative/src/main/cpp/VideoDecoder.h b/app/videonative/src/main/cpp/VideoDecoder.h
index 2645290..c90bdee 100644
--- a/app/videonative/src/main/cpp/VideoDecoder.h
+++ b/app/videonative/src/main/cpp/VideoDecoder.h
@@ -60,9 +60,9 @@ struct VideoRatio {
class VideoDecoder {
private:
struct Decoder {
- bool configured = false;
- AMediaCodec *codec = nullptr;
- ANativeWindow *window = nullptr;
+ bool configured[2] = {false, false};
+ AMediaCodec *codec[2] = {nullptr, nullptr};
+ ANativeWindow *window[2] = {nullptr, nullptr};
};
public:
//Make sure to do no heavy lifting on this callback, since it is called from the low-latency mCheckOutputThread thread (best to copy values and leave processing to another thread)
@@ -80,7 +80,7 @@ class VideoDecoder {
// After acquiring the surface, the decoder will be started as soon as enough configuration data was passed to it
// When releasing the surface, the decoder will be stopped if running and any resources will be freed
// After releasing the surface it is safe for the android os to delete it
- void setOutputSurface(JNIEnv *env, jobject surface);
+ void setOutputSurface(JNIEnv *env, jobject surface, jint idx);
//register the specified callbacks. Only one can be registered at a time
void registerOnDecoderRatioChangedCallback(DECODER_RATIO_CHANGED decoderRatioChangedC);
@@ -96,20 +96,20 @@ class VideoDecoder {
private:
//Initialize decoder with SPS / PPS data from KeyFrameFinder
//Set Decoder.configured to true on success
- void configureStartDecoder();
+ void configureStartDecoder(int idx);
//Wait for input buffer to become available before feeding NALU
- void feedDecoder(const NALU &nalu);
+ void feedDecoder(const NALU &nalu, int idx);
//Runs until EOS arrives at output buffer or decoder is stopped
- void checkOutputLoop();
+ void checkOutputLoop(int idx);
//Debug log
void printAvgLog();
void resetStatistics();
- std::unique_ptr mCheckOutputThread = nullptr;
+ std::unique_ptr mCheckOutputThread[2] = {nullptr, nullptr};
bool USE_SW_DECODER_INSTEAD = false;
//Holds the AMediaCodec instance, as well as the state (configured or not configured)
Decoder decoder{};
diff --git a/app/videonative/src/main/cpp/VideoPlayer.cpp b/app/videonative/src/main/cpp/VideoPlayer.cpp
index bcd090e..0ec079f 100644
--- a/app/videonative/src/main/cpp/VideoPlayer.cpp
+++ b/app/videonative/src/main/cpp/VideoPlayer.cpp
@@ -38,6 +38,12 @@ void VideoPlayer::processQueue() {
MP4E_mux_t *mux = MP4E_open(0 /*sequential_mode*/, dvr_mp4_fragmentation, fout, write_callback);
mp4_h26x_writer_t mp4wr;
float framerate = 0;
+ if(mux == nullptr)
+ {
+ __android_log_print(ANDROID_LOG_ERROR, TAG,
+ "dvr open failed");
+ return;
+ }
while (true) {
last_dvr_write = get_time_ms();
@@ -104,11 +110,11 @@ void VideoPlayer::onNewNALU(const NALU &nalu) {
enqueueNALU(nalu_);
}
-void VideoPlayer::setVideoSurface(JNIEnv *env, jobject surface) {
+void VideoPlayer::setVideoSurface(JNIEnv *env, jobject surface, jint i) {
//reset the parser so the statistics start again from 0
// mParser.reset();
//set the jni object for settings
- videoDecoder.setOutputSurface(env, surface);
+ videoDecoder.setOutputSurface(env, surface, i);
}
@@ -159,6 +165,7 @@ void VideoPlayer::startDvr(JNIEnv *env, jint fd, jint dvr_fmp4_enabled) {
}
void VideoPlayer::stopDvr() {
+ __android_log_print(ANDROID_LOG_DEBUG, TAG, "Stop dvr");
stopProcessing();
}
@@ -204,8 +211,8 @@ JNI_METHOD(void, nativeStop)
}
JNI_METHOD(void, nativeSetVideoSurface)
-(JNIEnv *env, jclass jclass1, jlong videoPlayerN, jobject surface) {
- native(videoPlayerN)->setVideoSurface(env, surface);
+(JNIEnv *env, jclass jclass1, jlong videoPlayerN, jobject surface, jint index) {
+ native(videoPlayerN)->setVideoSurface(env, surface, index);
}
JNI_METHOD(jstring, getVideoInfoString)
diff --git a/app/videonative/src/main/cpp/VideoPlayer.h b/app/videonative/src/main/cpp/VideoPlayer.h
index b07f913..b2f6c5a 100644
--- a/app/videonative/src/main/cpp/VideoPlayer.h
+++ b/app/videonative/src/main/cpp/VideoPlayer.h
@@ -27,7 +27,7 @@ class VideoPlayer {
* Set the surface the decoder can be configured with. When @param surface==nullptr
* It is guaranteed that the surface is not used by the decoder anymore when this call returns
*/
- void setVideoSurface(JNIEnv *env, jobject surface);
+ void setVideoSurface(JNIEnv *env, jobject surface, jint i);
/*
* Start the receiver and ground recorder if enabled
diff --git a/app/videonative/src/main/java/com/openipc/videonative/VideoPlayer.java b/app/videonative/src/main/java/com/openipc/videonative/VideoPlayer.java
index 6048285..06d0382 100644
--- a/app/videonative/src/main/java/com/openipc/videonative/VideoPlayer.java
+++ b/app/videonative/src/main/java/com/openipc/videonative/VideoPlayer.java
@@ -47,7 +47,7 @@ public VideoPlayer(final AppCompatActivity parent) {
public static native void nativeStop(long nativeInstance, Context context);
- public static native void nativeSetVideoSurface(long nativeInstance, Surface surface);
+ public static native void nativeSetVideoSurface(long nativeInstance, Surface surface, int index);
public static native void nativeStartDvr(long nativeInstance, int fd, int fmp4_enabled);
@@ -78,9 +78,9 @@ public void setIVideoParamsChanged(final IVideoParamsChanged iVideoParamsChanged
mVideoParamsChanged = iVideoParamsChanged;
}
- private void setVideoSurface(final @Nullable Surface surface) {
+ private void setVideoSurface(final @Nullable Surface surface, int index) {
verifyApplicationThread();
- nativeSetVideoSurface(nativeVideoPlayer, surface);
+ nativeSetVideoSurface(nativeVideoPlayer, surface, index);
}
public synchronized void start() {
@@ -128,8 +128,8 @@ public void stopDvr() {
* d) Receiving Data from a file in the phone file system
* e) and more
*/
- public void addAndStartDecoderReceiver(Surface surface) {
- setVideoSurface(surface);
+ public void addAndStartDecoderReceiver(Surface surface, int index) {
+ setVideoSurface(surface, index);
}
/**
@@ -137,9 +137,9 @@ public void addAndStartDecoderReceiver(Surface surface) {
* Stop the Decoder
* Free resources
*/
- public void stopAndRemoveReceiverDecoder() {
+ public void stopAndRemoveReceiverDecoder(int index) {
stop();
- setVideoSurface(null);
+ setVideoSurface(null, index);
}
/**
@@ -148,11 +148,11 @@ public void stopAndRemoveReceiverDecoder() {
*
* @return Callback that should be added to SurfaceView.Holder
*/
- public SurfaceHolder.Callback configure1() {
+ public SurfaceHolder.Callback configure1(int index) {
return new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
- addAndStartDecoderReceiver(holder.getSurface());
+ addAndStartDecoderReceiver(holder.getSurface(), index);
}
@Override
@@ -162,30 +162,30 @@ public void surfaceChanged(SurfaceHolder holder, int format, int width, int heig
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
- stopAndRemoveReceiverDecoder();
+ stopAndRemoveReceiverDecoder(index);
}
};
}
- /**
- * Configure for use with VideoSurfaceHolder (OpenGL)
- * The callback will handle the lifecycle of the video player
- *
- * @return Callback that should be added to VideoSurfaceHolder
- */
- public ISurfaceTextureAvailable configure2() {
- return new ISurfaceTextureAvailable() {
- @Override
- public void surfaceTextureCreated(SurfaceTexture surfaceTexture, Surface surface) {
- addAndStartDecoderReceiver(surface);
- }
-
- @Override
- public void surfaceTextureDestroyed() {
- stopAndRemoveReceiverDecoder();
- }
- };
- }
+// /**
+// * Configure for use with VideoSurfaceHolder (OpenGL)
+// * The callback will handle the lifecycle of the video player
+// *
+// * @return Callback that should be added to VideoSurfaceHolder
+// */
+// public ISurfaceTextureAvailable configure2() {
+// return new ISurfaceTextureAvailable() {
+// @Override
+// public void surfaceTextureCreated(SurfaceTexture surfaceTexture, Surface surface) {
+// addAndStartDecoderReceiver(surface, index);
+// }
+//
+// @Override
+// public void surfaceTextureDestroyed() {
+// stopAndRemoveReceiverDecoder(index);
+// }
+// };
+// }
public long getNativeInstance() {
return nativeVideoPlayer;