package org.droidplanner.services.android.impl.utils.video;
import android.content.Context;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import com.coremedia.iso.boxes.Container;
import com.googlecode.mp4parser.FileDataSourceImpl;
import com.googlecode.mp4parser.authoring.Movie;
import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
import com.googlecode.mp4parser.authoring.tracks.h264.H264TrackImpl;
import com.o3dr.android.client.utils.video.MediaCodecManager;
import com.o3dr.android.client.utils.video.NaluChunk;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import timber.log.Timber;
/**
* Created by Fredia Huya-Kouadio on 11/22/15.
*/
class StreamRecorder implements MediaCodecManager.NaluChunkListener{
private final AtomicReference<String> recordingFilename = new AtomicReference<>();
private final AtomicBoolean areParametersSet = new AtomicBoolean(false);
private final File mediaRootDir;
private final Context context;
private final MediaScannerConnection.OnScanCompletedListener scanCompletedListener = new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
Timber.i("Media file %s was scanned successfully: %s", path, uri);
}
};
private ExecutorService asyncExecutor;
private BufferedOutputStream h264Writer;
StreamRecorder(Context context) {
this.context = context;
this.mediaRootDir = new File(context.getExternalFilesDir(Environment.DIRECTORY_MOVIES), "stream");
if (!this.mediaRootDir.exists()) {
this.mediaRootDir.mkdirs();
}
}
String getRecordingFilename(){
return recordingFilename.get();
}
void startConverterThread() {
if (asyncExecutor == null || asyncExecutor.isShutdown()) {
asyncExecutor = Executors.newSingleThreadExecutor();
}
}
void stopConverterThread() {
if (asyncExecutor != null)
asyncExecutor.shutdown();
}
boolean isRecordingEnabled() {
return !TextUtils.isEmpty(recordingFilename.get());
}
boolean enableRecording(String mediaFilename) {
if (!isRecordingEnabled()) {
areParametersSet.set(false);
recordingFilename.set(mediaFilename);
Timber.i("Enabling local recording to %s", mediaFilename);
File h264File = new File(mediaRootDir, mediaFilename);
if (h264File.exists())
h264File.delete();
try {
h264Writer = new BufferedOutputStream(new FileOutputStream(h264File));
return true;
} catch (FileNotFoundException e) {
Timber.e(e, e.getMessage());
recordingFilename.set(null);
return false;
}
} else {
Timber.w("Video stream recording is already enabled");
return false;
}
}
boolean disableRecording() {
if (isRecordingEnabled()) {
Timber.i("Disabling local recording");
//Close the Buffered output stream
if (h264Writer != null) {
try {
h264Writer.close();
} catch (IOException e) {
Timber.e(e, e.getMessage());
} finally {
h264Writer = null;
//Kickstart conversion of the h264 file to mp4.
convertToMp4(recordingFilename.get());
recordingFilename.set(null);
}
}
}
areParametersSet.set(false);
return true;
}
//TODO: Maybe put this on a background thread to avoid blocking on the write to file.
@Override
public void onNaluChunkUpdated(NaluChunk parametersSet, NaluChunk dataChunk) {
if (isRecordingEnabled() && h264Writer != null) {
if(areParametersSet.get()) {
try {
writeNaluChunk(h264Writer, dataChunk);
} catch (IOException e) {
Timber.e(e, e.getMessage());
}
}
else{
try {
areParametersSet.set(writeNaluChunk(h264Writer, parametersSet));
} catch (IOException e) {
Timber.e(e, e.getMessage());
}
}
}
}
private boolean writeNaluChunk(BufferedOutputStream bos, NaluChunk naluChunk) throws IOException {
if(naluChunk == null)
return false;
int payloadCount = naluChunk.payloads.length;
for (int i = 0; i < payloadCount; i++) {
ByteBuffer payload = naluChunk.payloads[i];
if (payload.capacity() == 0)
continue;
final int dataLength = payload.position();
byte[] payloadData = payload.array();
bos.write(payloadData, 0, dataLength);
}
return true;
}
void convertToMp4(final String filename) {
if (TextUtils.isEmpty(filename)) {
Timber.w("Invalid media filename.");
return;
}
final File rawMedia = new File(mediaRootDir, filename);
if (!rawMedia.exists()) {
Timber.w("Media file doesn't exists.");
return;
}
if(rawMedia.length() == 0){
Timber.w("Media file is empty.");
return;
}
asyncExecutor.execute(new Runnable() {
@Override
public void run() {
Timber.i("Starting h264 conversion process for media file %s.", filename);
try {
H264TrackImpl h264Track = new H264TrackImpl(new FileDataSourceImpl(rawMedia));
Movie movie = new Movie();
movie.addTrack(h264Track);
Container mp4File = new DefaultMp4Builder().build(movie);
File dstDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
File mp4Media = new File(dstDir, filename + ".mp4");
Timber.i("Generating the mp4 file @ %s", mp4Media.getAbsolutePath());
FileChannel fc = new FileOutputStream(mp4Media).getChannel();
mp4File.writeContainer(fc);
fc.close();
//Delete the h264 file.
Timber.i("Deleting raw h264 media file.");
rawMedia.delete();
//Add the generated file to the mediastore
Timber.i("Adding the generated mp4 file to the media store.");
MediaScannerConnection.scanFile(context,
new String[]{mp4Media.getAbsolutePath()}, null, scanCompletedListener);
} catch (IOException | NullPointerException e) {
Timber.e(e, e.getMessage());
}
}
});
}
}