Skip to content

Commit c07310c

Browse files
committed
Merge pull request #353 from dronekit/video_recording
Add support for in app video recording
2 parents f299b5f + 5cfb1e2 commit c07310c

File tree

12 files changed

+292
-19
lines changed

12 files changed

+292
-19
lines changed

ClientLib/build.gradle

+1-1
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

+14
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

+17-2
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

+2
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
}
+1-1
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/build.gradle

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ dependencies {
66
compile 'com.google.android.gms:play-services-analytics:7.3.0'
77
compile 'com.google.android.gms:play-services-location:7.3.0'
88

9-
compile 'com.android.support:support-v4:22.1.1'
10-
compile 'com.android.support:appcompat-v7:22.1.1'
11-
compile 'com.android.support:cardview-v7:21.0.0'
12-
compile 'com.android.support:recyclerview-v7:21.0.2'
9+
compile 'com.android.support:support-v4:22.2.1'
10+
compile 'com.android.support:appcompat-v7:22.2.1'
11+
compile 'com.android.support:cardview-v7:22.2.1'
12+
compile 'com.android.support:recyclerview-v7:22.2.1'
1313

1414
compile files('libs/d2xx.jar')
1515
compile files('libs/jeromq-0.3.4.jar')
@@ -31,6 +31,9 @@ dependencies {
3131
//Java semver library
3232
compile 'com.github.zafarkhaja:java-semver:0.9.0'
3333

34+
//MP4 generation library
35+
compile 'com.googlecode.mp4parser:isoparser:1.1.7'
36+
3437
testCompile 'junit:junit:4.12'
3538
testCompile "org.robolectric:robolectric:3.0"
3639
}

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

+1-3
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
@@ -172,8 +172,6 @@ public MagnetometerCalibrationImpl getMagnetometerCalibration() {
172172

173173
@Override
174174
public void destroy(){
175-
events.removeAllDroneListeners();
176-
177175
ParameterManager parameterManager = getParameterManager();
178176
if (parameterManager != null)
179177
parameterManager.setParameterListener(null);

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

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

0 commit comments

Comments
 (0)