Skip to content

Commit 4023e65

Browse files
wxkly8888vince-leicaniqdev
authored
Live image recording and single image acquire (#158)
* update timer * recording video updated * add recording logic * adjust snapshot ui * fix error * update ipcam view * fix string error * fix PR issue * fix PR error * add file path toast when start recording * extact methods from IpcamSnapshotActivity into MjpegRecordingHandler * Apply suggestions from code review Co-authored-by: niqdev <[email protected]> * improve code * fix missing import * fix missing import Co-authored-by: Xinkai Wu <[email protected]> Co-authored-by: niqdev <[email protected]>
1 parent e5141ef commit 4023e65

File tree

9 files changed

+270
-26
lines changed

9 files changed

+270
-26
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
android:allowBackup="true"
1111
android:icon="@mipmap/ic_launcher"
1212
android:label="@string/app_name"
13+
android:usesCleartextTraffic="true"
1314
android:supportsRtl="true"
1415
android:theme="@style/AppTheme"
1516
tools:ignore="AllowBackup">

app/src/main/java/com/github/niqdev/ipcam/IpCamSnapshotActivity.java

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,26 @@
77
import android.util.Log;
88
import android.view.Menu;
99
import android.view.MenuItem;
10+
import android.view.View;
1011
import android.widget.ImageView;
12+
import android.widget.TextView;
1113
import android.widget.Toast;
1214

1315
import com.github.niqdev.mjpeg.DisplayMode;
1416
import com.github.niqdev.mjpeg.Mjpeg;
17+
import com.github.niqdev.mjpeg.MjpegRecordingHandler;
1518
import com.github.niqdev.mjpeg.MjpegView;
16-
import com.github.niqdev.mjpeg.OnFrameCapturedListener;
19+
20+
import java.io.BufferedOutputStream;
21+
import java.io.ByteArrayOutputStream;
22+
import java.io.File;
23+
import java.io.FileOutputStream;
24+
import java.io.IOException;
25+
import java.text.SimpleDateFormat;
26+
import java.util.Date;
27+
import java.util.Locale;
28+
import java.util.Timer;
29+
import java.util.TimerTask;
1730

1831
import androidx.appcompat.app.AppCompatActivity;
1932
import butterknife.BindView;
@@ -23,21 +36,29 @@
2336
import static com.github.niqdev.ipcam.settings.SettingsActivity.PREF_AUTH_USERNAME;
2437
import static com.github.niqdev.ipcam.settings.SettingsActivity.PREF_IPCAM_URL;
2538

26-
public class IpCamSnapshotActivity extends AppCompatActivity implements OnFrameCapturedListener {
39+
public class IpCamSnapshotActivity extends AppCompatActivity {
2740

2841
private static final int TIMEOUT = 5;
42+
private static final String TAG = "IpCamSnapshotActivity";
43+
2944
@BindView(R.id.mjpegViewSnapshot)
3045
MjpegView mjpegView;
3146
@BindView(R.id.imageView)
32-
ImageView imageView;
33-
private Bitmap lastPreview = null;
47+
ImageView snapshotView;
48+
@BindView(R.id.record_text)
49+
TextView timerText;
50+
private Timer timer = new Timer();
51+
int cnt = 0;
52+
private MjpegRecordingHandler recordingHandler;
3453

3554
@Override
3655
protected void onCreate(Bundle savedInstanceState) {
3756
super.onCreate(savedInstanceState);
3857
setContentView(R.layout.activity_ipcam_snapshot);
3958
ButterKnife.bind(this);
40-
mjpegView.setOnFrameCapturedListener(this);
59+
recordingHandler=new MjpegRecordingHandler(this);
60+
mjpegView.setOnFrameCapturedListener(recordingHandler);
61+
timerText.setText("00:00:00");
4162
}
4263

4364
private String getPreference(String key) {
@@ -68,6 +89,13 @@ private void loadIpCam() {
6889
});
6990
}
7091

92+
private String getStringTime(int cnt) {
93+
int hour = cnt / 3600;
94+
int min = cnt % 3600 / 60;
95+
int second = cnt % 60;
96+
return String.format(Locale.CHINA, "%02d:%02d:%02d", hour, min, second);
97+
}
98+
7199
@Override
72100
protected void onResume() {
73101
super.onResume();
@@ -84,28 +112,57 @@ protected void onPause() {
84112
public boolean onOptionsItemSelected(MenuItem item) {
85113
switch (item.getItemId()) {
86114
case R.id.action_capture:
87-
runOnUiThread(new Runnable() {
88-
@Override
89-
public void run() {
90-
if (lastPreview != null) {
91-
imageView.setImageBitmap(lastPreview);
92-
}
115+
runOnUiThread(() -> {
116+
if (recordingHandler.getLastBitmap() != null) {
117+
snapshotView.setVisibility(View.VISIBLE);
118+
snapshotView.setImageBitmap(recordingHandler.getLastBitmap());
119+
recordingHandler.saveBitmapToFile();
93120
}
94121
});
95122
return true;
123+
case R.id.action_recording:
124+
if (!recordingHandler.isRecording()) {
125+
startRecording();
126+
} else {
127+
stopRecording();
128+
}
129+
item.setIcon(recordingHandler.isRecording() ? R.drawable.recording : R.drawable.ic_videocam_white_48dp);
130+
return true;
96131
default:
97132
return super.onOptionsItemSelected(item);
98133
}
99134
}
100135

136+
private void stopRecording() {
137+
recordingHandler.stopRecording();
138+
timer.cancel();
139+
timer.purge();
140+
timer = null;
141+
timerText.setVisibility(View.GONE);
142+
143+
}
144+
145+
private void startRecording() {
146+
cnt = 0;
147+
timer = new Timer();
148+
TimerTask timerTask = new TimerTask() {
149+
@Override
150+
public void run() {
151+
runOnUiThread(() -> {
152+
timerText.setVisibility(View.VISIBLE);
153+
timerText.setText(getStringTime(cnt++));
154+
});
155+
}
156+
};
157+
timer.schedule(timerTask, 0, 1000);
158+
timerText.setVisibility(View.VISIBLE);
159+
timerText.setText("00:00:00");
160+
recordingHandler.startRecording();
161+
}
162+
101163
@Override
102164
public boolean onCreateOptionsMenu(Menu menu) {
103165
getMenuInflater().inflate(R.menu.menu_capture, menu);
104166
return true;
105167
}
106-
107-
@Override
108-
public void onFrameCaptured(Bitmap bitmap) {
109-
lastPreview = bitmap;
110-
}
111168
}
741 Bytes
Loading

app/src/main/res/layout/activity_ipcam_snapshot.xml

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
android:layout_height="match_parent"
77
android:orientation="vertical"
88
tools:context="com.github.niqdev.ipcam.IpCamSnapshotActivity">
9-
109
<RelativeLayout
1110
android:layout_width="match_parent"
1211
android:layout_height="0dp"
1312
android:layout_weight="1">
13+
<com.github.niqdev.mjpeg.MjpegSurfaceView
14+
android:id="@+id/mjpegViewSnapshot"
15+
android:layout_width="match_parent"
16+
android:layout_height="match_parent"
17+
stream:type="stream_default" />
1418

1519
<ImageView
1620
android:id="@+id/imageView"
@@ -19,13 +23,20 @@
1923
android:layout_alignParentBottom="true"
2024
android:layout_alignParentEnd="true"
2125
android:layout_alignParentRight="true"
26+
android:visibility="gone"
2227
stream:srcCompat="@mipmap/ic_launcher" />
2328

24-
<com.github.niqdev.mjpeg.MjpegSurfaceView
25-
android:id="@+id/mjpegViewSnapshot"
26-
android:layout_width="match_parent"
27-
android:layout_height="match_parent"
28-
stream:type="stream_default" />
29+
<TextView
30+
android:id="@+id/record_text"
31+
android:layout_width="wrap_content"
32+
android:layout_height="wrap_content"
33+
android:visibility="gone"
34+
android:layout_centerHorizontal="true"
35+
android:layout_marginTop="20dp"
36+
android:textSize="18sp"
37+
android:textColor="@android:color/holo_red_dark"
38+
android:layout_alignParentTop="true"
39+
/>
2940
</RelativeLayout>
3041

3142
</LinearLayout>

app/src/main/res/menu/menu_capture.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,9 @@
77
android:icon="@drawable/ic_camera_white_48dp"
88
android:title="@string/menu_snapshot"
99
app:showAsAction="always|withText" />
10-
10+
<item
11+
android:id="@+id/action_recording"
12+
android:icon="@drawable/ic_videocam_white_48dp"
13+
android:title="@string/menu_snapshot"
14+
app:showAsAction="always|withText" />
1115
</menu>

mjpeg-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamDefault.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ private int parseContentLength(byte[] headerBytes) throws IOException, IllegalAr
5858
props.load(headerIn);
5959
return Integer.parseInt(props.getProperty(CONTENT_LENGTH));
6060
}
61+
byte[] readHeader() throws IOException{
62+
mark(FRAME_MAX_LENGTH);
63+
int headerLen = getStartOfSequence(this, SOI_MARKER);
64+
reset();
65+
byte[] header = new byte[headerLen];
66+
readFully(header);
67+
return header;
68+
}
69+
// no more accessible
70+
byte[] readMjpegFrame(byte[] header) throws IOException {
71+
try {
72+
mContentLength = parseContentLength(header);
73+
} catch (IllegalArgumentException iae) {
74+
mContentLength = getEndOfSeqeunce(this, EOF_MARKER);
75+
}
76+
reset();
77+
byte[] frameData = new byte[mContentLength];
78+
skipBytes(header.length);
79+
readFully(frameData);
80+
return frameData;
81+
}
6182

6283
// no more accessible
6384
Bitmap readMjpegFrame() throws IOException {
@@ -77,4 +98,5 @@ Bitmap readMjpegFrame() throws IOException {
7798
readFully(frameData);
7899
return BitmapFactory.decodeStream(new ByteArrayInputStream(frameData));
79100
}
101+
80102
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.github.niqdev.mjpeg;
2+
3+
import android.content.Context;
4+
import android.graphics.Bitmap;
5+
import android.util.Log;
6+
import android.widget.Toast;
7+
import java.io.BufferedOutputStream;
8+
import java.io.ByteArrayOutputStream;
9+
import java.io.File;
10+
import java.io.FileNotFoundException;
11+
import java.io.FileOutputStream;
12+
import java.io.IOException;
13+
import java.text.SimpleDateFormat;
14+
import java.util.Date;
15+
16+
public class MjpegRecordingHandler implements OnFrameCapturedListener {
17+
private static final String TAG = "MjpegRecordingHandler";
18+
private Context context;
19+
private BufferedOutputStream bos;
20+
private boolean isRecording = false;
21+
private Bitmap lastBitmap = null;
22+
23+
public MjpegRecordingHandler(Context context) {
24+
this.context = context;
25+
}
26+
27+
/**
28+
* start recording the live image
29+
*/
30+
public void startRecording() {
31+
try {
32+
String mjpegFilePath = createMjpegFile().getAbsolutePath();
33+
FileOutputStream fos = new FileOutputStream(mjpegFilePath);
34+
bos = new BufferedOutputStream(fos);
35+
Toast.makeText(context, "start recording, file path is:" + mjpegFilePath, Toast.LENGTH_LONG).show();
36+
isRecording = true;
37+
} catch (FileNotFoundException e) {
38+
e.printStackTrace();
39+
}
40+
}
41+
42+
/**
43+
* stop recording the live image
44+
*/
45+
public void stopRecording() {
46+
isRecording = false;
47+
}
48+
49+
public boolean isRecording() {
50+
return isRecording;
51+
}
52+
53+
public void setRecording(boolean recording) {
54+
isRecording = recording;
55+
}
56+
57+
public Bitmap getLastBitmap() {
58+
return lastBitmap;
59+
}
60+
61+
/**
62+
* save the last acquired bitmap into jpg file.
63+
*/
64+
public void saveBitmapToFile() {
65+
FileOutputStream fos = null;
66+
BufferedOutputStream bos = null;
67+
String imagePath = createJpgFile().getAbsolutePath();
68+
try {
69+
fos = new FileOutputStream(imagePath);
70+
bos = new BufferedOutputStream(fos);
71+
ByteArrayOutputStream jpegByteArrayOutputStream = new ByteArrayOutputStream();
72+
lastBitmap.compress(Bitmap.CompressFormat.JPEG, 75, jpegByteArrayOutputStream);
73+
byte[] jpegByteArray = jpegByteArrayOutputStream.toByteArray();
74+
bos.write(jpegByteArray);
75+
bos.flush();
76+
Toast.makeText(context, "saved image:" + imagePath, Toast.LENGTH_LONG).show();
77+
} catch (IOException e) {
78+
e.printStackTrace();
79+
}
80+
}
81+
82+
/**
83+
* Create jpg file in app external cache directory. the directory path is /sdcard/Android/data/com.github.niqdev.ipcam/files
84+
*
85+
* @return File
86+
*/
87+
private File createJpgFile() {
88+
return createSavingFile("photo", "jpg");
89+
}
90+
91+
private File createSavingFile(String prefix, String extension) {
92+
Date T = new Date();
93+
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
94+
String szFileName = prefix + "-" + sdf.format(T);
95+
try {
96+
String path = context.getExternalFilesDir(null).getPath() + "/" + szFileName + "." + extension;
97+
File file = new File(path);
98+
if (!file.exists()) {
99+
file.createNewFile();
100+
}
101+
Log.d(TAG, "file path is " + file.getAbsolutePath());
102+
return file;
103+
} catch (IOException e) {
104+
e.printStackTrace();
105+
}
106+
return null;
107+
}
108+
109+
/**
110+
* Create mjpeg file in app external cache directory. the directory path is /sdcard/Android/data/com.github.niqdev.ipcam/files
111+
*
112+
* @return File
113+
*/
114+
private File createMjpegFile() {
115+
return createSavingFile("video", "mjpeg");
116+
}
117+
118+
@Override
119+
public void onFrameCaptured(Bitmap bitmap) {
120+
lastBitmap = bitmap;
121+
}
122+
123+
@Override
124+
public void onFrameCapturedWithHeader(byte[] bitmapData, byte[] header) {
125+
if (isRecording) {
126+
try {
127+
bos.write(header);
128+
bos.write(bitmapData);
129+
bos.flush();
130+
} catch (IOException e) {
131+
Log.e(TAG, e.getMessage());
132+
}
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)