Skip to content

Commit ac572ff

Browse files
committed
updated client lib api to support local video recording.
1 parent aae9e8b commit ac572ff

File tree

11 files changed

+286
-11
lines changed

11 files changed

+286
-11
lines changed

ClientLib/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ ext {
44
VERSION_MAJOR = 2
55
VERSION_MINOR = 7
66
VERSION_PATCH = 0
7-
VERSION_SUFFIX = "beta2"
7+
VERSION_SUFFIX = "beta3"
88

99
PUBLISH_ARTIFACT_ID = 'dronekit-android'
1010
PUBLISH_VERSION = generateVersionName("", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, VERSION_SUFFIX)

ClientLib/src/main/java/com/o3dr/android/client/apis/CameraApi.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import static com.o3dr.services.android.lib.drone.action.CameraActions.ACTION_START_VIDEO_STREAM;
1515
import static com.o3dr.services.android.lib.drone.action.CameraActions.ACTION_STOP_VIDEO_STREAM;
1616
import static com.o3dr.services.android.lib.drone.action.CameraActions.EXTRA_VIDEO_DISPLAY;
17+
import static com.o3dr.services.android.lib.drone.action.CameraActions.EXTRA_VIDEO_ENABLE_LOCAL_RECORDING;
18+
import static com.o3dr.services.android.lib.drone.action.CameraActions.EXTRA_VIDEO_LOCAL_RECORDING_FILENAME;
1719
import static com.o3dr.services.android.lib.drone.action.CameraActions.EXTRA_VIDEO_PROPS_UDP_PORT;
1820
import static com.o3dr.services.android.lib.drone.action.CameraActions.EXTRA_VIDEO_TAG;
1921
import static com.o3dr.services.android.lib.drone.action.CameraActions.EXTRA_VIDEO_PROPERTIES;
@@ -39,6 +41,18 @@ public CameraApi build(Drone drone) {
3941
*/
4042
public static final String VIDEO_PROPS_UDP_PORT = EXTRA_VIDEO_PROPS_UDP_PORT;
4143

44+
/**
45+
* Key to specify whether to enable/disable local recording of the video stream.
46+
* @since 2.7.0
47+
*/
48+
public static final String VIDEO_ENABLE_LOCAL_RECORDING = EXTRA_VIDEO_ENABLE_LOCAL_RECORDING;
49+
50+
/**
51+
* Key to specify the filename to use for the local recording.
52+
* @since 2.7.0
53+
*/
54+
public static final String VIDEO_LOCAL_RECORDING_FILENAME = EXTRA_VIDEO_LOCAL_RECORDING_FILENAME;
55+
4256
/**
4357
* Retrieves a camera api instance
4458
*

ClientLib/src/main/java/com/o3dr/android/client/apis/solo/SoloCameraApi.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
import com.o3dr.services.android.lib.drone.companion.solo.tlv.SoloGoproSetRequest;
1616
import com.o3dr.services.android.lib.model.AbstractCommandListener;
1717

18+
import java.text.SimpleDateFormat;
19+
import java.util.Date;
20+
import java.util.Locale;
1821
import java.util.concurrent.ConcurrentHashMap;
1922

2023
/**
@@ -25,7 +28,7 @@
2528
*/
2629
public class SoloCameraApi extends SoloApi {
2730

28-
private static final String TAG = SoloCameraApi.class.getSimpleName();
31+
private static final SimpleDateFormat FILE_DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.US);
2932

3033
private static final ConcurrentHashMap<Drone, SoloCameraApi> soloCameraApiCache = new ConcurrentHashMap<>();
3134
private static final Builder<SoloCameraApi> apiBuilder = new Builder<SoloCameraApi>() {
@@ -144,17 +147,22 @@ public void onTimeout() {
144147
});
145148
}
146149

150+
public void startVideoStream(final Surface surface, final String tag, final AbstractCommandListener listener) {
151+
startVideoStream(surface, tag, false, listener);
152+
}
153+
147154
/**
148155
* Attempt to grab ownership and start the video stream from the connected drone. Can fail if
149156
* the video stream is already owned by another client.
150157
*
151158
* @param surface Surface object onto which the video is decoded.
152159
* @param tag Video tag.
160+
* @param enableLocalRecording Set to true to enable local recording, false to disable it.
153161
* @param listener Register a callback to receive update of the command execution status.
154162
*
155163
* @since 2.5.0
156164
*/
157-
public void startVideoStream(final Surface surface, final String tag, final AbstractCommandListener listener) {
165+
public void startVideoStream(final Surface surface, final String tag, final boolean enableLocalRecording, final AbstractCommandListener listener) {
158166
if (surface == null) {
159167
postErrorEvent(CommandExecutionError.COMMAND_FAILED, listener);
160168
return;
@@ -169,6 +177,13 @@ public void onFeatureSupportResult(String featureId, int result, Bundle resultIn
169177
case CapabilityApi.FEATURE_SUPPORTED:
170178
final Bundle videoProps = new Bundle();
171179
videoProps.putInt(CameraApi.VIDEO_PROPS_UDP_PORT, SOLO_STREAM_UDP_PORT);
180+
181+
videoProps.putBoolean(CameraApi.VIDEO_ENABLE_LOCAL_RECORDING, enableLocalRecording);
182+
if(enableLocalRecording){
183+
String localRecordingFilename = "solo_stream_" + FILE_DATE_FORMAT.format(new Date());
184+
videoProps.putString(CameraApi.VIDEO_LOCAL_RECORDING_FILENAME, localRecordingFilename);
185+
}
186+
172187
cameraApi.startVideoStream(surface, tag, videoProps, listener);
173188
break;
174189

ClientLib/src/main/java/com/o3dr/services/android/lib/drone/action/CameraActions.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ private CameraActions() {
1717

1818
public static final String EXTRA_VIDEO_PROPERTIES = "extra_video_properties";
1919
public static final String EXTRA_VIDEO_PROPS_UDP_PORT = "extra_video_props_udp_port";
20+
public static final String EXTRA_VIDEO_ENABLE_LOCAL_RECORDING = "extra_video_enable_local_recording";
21+
public static final String EXTRA_VIDEO_LOCAL_RECORDING_FILENAME = "extra_video_local_recording_filename";
2022

2123
public static final String ACTION_STOP_VIDEO_STREAM = PACKAGE_NAME + ".STOP_VIDEO_STREAM";
2224
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
3-
<integer name="core_lib_version">20700</integer>
3+
<integer name="core_lib_version">20703</integer>
44
</resources>

ServiceApp/src/org/droidplanner/services/android/core/drone/autopilot/generic/GenericMavLinkDrone.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public GenericMavLinkDrone(Context context, Handler handler, MAVLinkStreams.MAVL
126126

127127
this.attributeListener = listener;
128128

129-
this.videoMgr = new VideoManager(handler);
129+
this.videoMgr = new VideoManager(context, handler);
130130
}
131131

132132
@Override

ServiceApp/src/org/droidplanner/services/android/utils/video/MediaCodecManager.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,14 @@ public void run() {
9494
private final NALUChunkAssembler naluChunkAssembler;
9595

9696
private final Handler handler;
97+
private final StreamRecorder streamRecorder;
9798

9899
private DequeueCodec dequeueRunner;
99100

100-
public MediaCodecManager(Handler handler) {
101+
public MediaCodecManager(Handler handler, StreamRecorder recorder) {
101102
this.handler = handler;
102103
this.naluChunkAssembler = new NALUChunkAssembler();
104+
this.streamRecorder = recorder;
103105
}
104106

105107
Surface getSurface(){
@@ -201,9 +203,12 @@ private boolean processNALUChunk(NALUChunk naluChunk) {
201203

202204
inputBuffer.order(payload.order());
203205
final int dataLength = payload.position();
204-
inputBuffer.put(payload.array(), 0, dataLength);
206+
byte[] payloadData = payload.array();
207+
inputBuffer.put(payloadData, 0, dataLength);
205208

206209
totalLength += dataLength;
210+
211+
streamRecorder.onNaluChunkUpdated(payloadData, 0, dataLength);
207212
}
208213

209214
mediaCodec.queueInputBuffer(index, 0, totalLength, naluChunk.presentationTime, naluChunk.flags);
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package org.droidplanner.services.android.utils.video;
2+
3+
import android.content.Context;
4+
import android.media.MediaScannerConnection;
5+
import android.net.Uri;
6+
import android.os.Environment;
7+
import android.text.TextUtils;
8+
9+
import com.coremedia.iso.boxes.Container;
10+
import com.googlecode.mp4parser.FileDataSourceImpl;
11+
import com.googlecode.mp4parser.authoring.Movie;
12+
import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
13+
import com.googlecode.mp4parser.authoring.tracks.h264.H264TrackImpl;
14+
15+
import java.io.BufferedOutputStream;
16+
import java.io.File;
17+
import java.io.FileNotFoundException;
18+
import java.io.FileOutputStream;
19+
import java.io.IOException;
20+
import java.nio.channels.FileChannel;
21+
import java.util.concurrent.ExecutorService;
22+
import java.util.concurrent.Executors;
23+
import java.util.concurrent.atomic.AtomicReference;
24+
25+
import timber.log.Timber;
26+
27+
/**
28+
* Created by Fredia Huya-Kouadio on 11/22/15.
29+
*/
30+
class StreamRecorder {
31+
32+
private final AtomicReference<String> recordingFilename = new AtomicReference<>();
33+
34+
private final File mediaRootDir;
35+
private final MediaScannerConnection mediaScanner;
36+
37+
private ExecutorService asyncExecutor;
38+
39+
private BufferedOutputStream h264Writer;
40+
41+
StreamRecorder(Context context) {
42+
this.mediaRootDir = new File(context.getExternalFilesDir(Environment.DIRECTORY_MOVIES), "stream");
43+
if (!this.mediaRootDir.exists()) {
44+
this.mediaRootDir.mkdirs();
45+
}
46+
47+
this.mediaScanner = new MediaScannerConnection(context, new MediaScannerConnection.MediaScannerConnectionClient() {
48+
@Override
49+
public void onMediaScannerConnected() {
50+
51+
}
52+
53+
@Override
54+
public void onScanCompleted(String path, Uri uri) {
55+
Timber.i("Media file %s was scanned successfully: %s", path, uri);
56+
}
57+
});
58+
}
59+
60+
String getRecordingFilename(){
61+
return recordingFilename.get();
62+
}
63+
64+
void startConverterThread() {
65+
if (asyncExecutor == null || asyncExecutor.isShutdown()) {
66+
asyncExecutor = Executors.newSingleThreadExecutor();
67+
}
68+
}
69+
70+
void stopConverterThread() {
71+
if (asyncExecutor != null)
72+
asyncExecutor.shutdown();
73+
}
74+
75+
boolean isRecordingEnabled() {
76+
return !TextUtils.isEmpty(recordingFilename.get());
77+
}
78+
79+
boolean enableRecording(String mediaFilename) {
80+
if (!isRecordingEnabled()) {
81+
recordingFilename.set(mediaFilename);
82+
83+
Timber.i("Enabling local recording to %s", mediaFilename);
84+
File h264File = new File(mediaRootDir, mediaFilename);
85+
if (h264File.exists())
86+
h264File.delete();
87+
88+
try {
89+
h264Writer = new BufferedOutputStream(new FileOutputStream(h264File));
90+
return true;
91+
} catch (FileNotFoundException e) {
92+
Timber.e(e, e.getMessage());
93+
recordingFilename.set(null);
94+
return false;
95+
}
96+
} else {
97+
Timber.w("Video stream recording is already enabled");
98+
return false;
99+
}
100+
}
101+
102+
boolean disableRecording() {
103+
if (isRecordingEnabled()) {
104+
Timber.i("Disabling local recording");
105+
106+
//Close the Buffered output stream
107+
if (h264Writer != null) {
108+
try {
109+
h264Writer.close();
110+
} catch (IOException e) {
111+
Timber.e(e, e.getMessage());
112+
} finally {
113+
h264Writer = null;
114+
115+
//Kickstart conversion of the h264 file to mp4.
116+
convertToMp4(recordingFilename.get());
117+
118+
recordingFilename.set(null);
119+
}
120+
}
121+
}
122+
123+
return true;
124+
}
125+
126+
//TODO: Maybe put this on a background thread to avoid blocking on the write to file.
127+
void onNaluChunkUpdated(byte[] payload, int index, int payloadLength) {
128+
if (isRecordingEnabled() && h264Writer != null) {
129+
try {
130+
h264Writer.write(payload, index, payloadLength);
131+
} catch (IOException e) {
132+
Timber.e(e, e.getMessage());
133+
}
134+
}
135+
}
136+
137+
void convertToMp4(final String filename) {
138+
asyncExecutor.execute(new Runnable() {
139+
@Override
140+
public void run() {
141+
Timber.i("Starting h264 conversion process.");
142+
143+
Timber.i("Converting media file %s", filename);
144+
if (TextUtils.isEmpty(filename)) {
145+
Timber.w("Invalid media filename.");
146+
return;
147+
}
148+
149+
File rawMedia = new File(mediaRootDir, filename);
150+
if (!rawMedia.exists()) {
151+
Timber.w("Media file doesn't exists.");
152+
return;
153+
}
154+
155+
try {
156+
H264TrackImpl h264Track = new H264TrackImpl(new FileDataSourceImpl(rawMedia));
157+
Movie movie = new Movie();
158+
movie.addTrack(h264Track);
159+
Container mp4File = new DefaultMp4Builder().build(movie);
160+
161+
File dstDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
162+
File mp4Media = new File(dstDir, filename + ".mp4");
163+
Timber.i("Generating the mp4 file @ %s", mp4Media.getAbsolutePath());
164+
FileChannel fc = new FileOutputStream(mp4Media).getChannel();
165+
mp4File.writeContainer(fc);
166+
fc.close();
167+
168+
//Delete the h264 file.
169+
Timber.i("Deleting raw h264 media file.");
170+
rawMedia.delete();
171+
172+
//Add the generated file to the mediastore
173+
Timber.i("Adding the generated mp4 file to the media store.");
174+
mediaScanner.scanFile(mp4Media.getAbsolutePath(), null);
175+
176+
} catch (IOException e) {
177+
Timber.e(e, e.getMessage());
178+
}
179+
}
180+
});
181+
}
182+
}

0 commit comments

Comments
 (0)