Skip to content

Instantly share code, notes, and snippets.

@kmturley
Created November 10, 2015 16:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kmturley/5cc2b481af1a6169f80e to your computer and use it in GitHub Desktop.
Save kmturley/5cc2b481af1a6169f80e to your computer and use it in GitHub Desktop.
Extending video editor plugin functionality
package org.apache.cordova.videoeditor;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.ffmpeg.android.FfmpegController;
import org.ffmpeg.android.Clip;
import org.ffmpeg.android.ShellUtils;
import org.ffmpeg.android.ShellUtils.ShellCallback;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.util.Log;
/**
* VideoEditor plugin for Android
* Created by Ross Martin 2-2-15
*/
public class VideoEditor extends CordovaPlugin {
private static final String TAG = "VideoEditor";
private CallbackContext callback;
private FfmpegController ffmpegController;
private static final int HighQuality = 0;
private static final int MediumQuality = 1;
private static final int LowQuality = 2;
private static final int M4V = 0;
private static final int MPEG4 = 1;
private static final int M4A = 2;
private static final int QUICK_TIME = 3;
private static final int MPEG = 4;
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
Log.d(TAG, "execute method starting");
this.callback = callbackContext;
if (action.equals("transcodeVideo")) {
try {
this.transcodeVideo(args);
} catch (IOException e) {
callback.error(e.toString());
}
return true;
} else if (action.equals("createThumbnail")) {
try {
this.createThumbnail(args);
} catch (IOException e) {
callback.error(e.toString());
}
return true;
} else if (action.equals("killVideoProcessor")) {
try {
this.killVideoProcessor(args);
} catch (IOException e) {
callback.error(e.toString());
}
return true;
}
return false;
}
private void transcodeVideo(JSONArray args) throws JSONException, IOException {
Log.d(TAG, "transcodeVideo firing");
/* transcodeVideo arguments:
fileUri: video input url
outputFileName: output file name
quality: transcode quality
outputFileType: output file type
optimizeForNetworkUse: optimize for network use
saveToLibrary: bool - save to gallery
*/
JSONObject options = args.optJSONObject(0);
Log.d(TAG, "options: " + options.toString());
final String outputFileName = options.optString(
"outputFileName",
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH).format(new Date())
);
final int videoQuality = options.optInt("quality", HighQuality);
final int outputType = options.optInt("outputFileType", MPEG4);
String outputExtension;
final int outputWidth;
final int outputHeight;
switch(outputType) {
case QUICK_TIME:
outputExtension = ".mov";
break;
case M4A:
outputExtension = ".m4a";
break;
case M4V:
outputExtension = ".m4v";
break;
case MPEG:
outputExtension = ".mpg";
break;
case MPEG4:
default:
outputExtension = ".mp4";
break;
}
switch(videoQuality) {
case LowQuality:
outputWidth = 320;
outputHeight = 320;
break;
case MediumQuality:
outputWidth = 480;
outputHeight = 480;
break;
case HighQuality:
default:
outputWidth = 640;
outputHeight = 640;
break;
}
final Context appContext = cordova.getActivity().getApplicationContext();
final PackageManager pm = appContext.getPackageManager();
ApplicationInfo ai;
try {
ai = pm.getApplicationInfo(cordova.getActivity().getPackageName(), 0);
} catch (final NameNotFoundException e) {
ai = null;
}
final String appName = (String) (ai != null ? pm.getApplicationLabel(ai) : "Unknown");
final boolean saveToLibrary = options.optBoolean("saveToLibrary", true);
File mediaStorageDir;
if (saveToLibrary) {
mediaStorageDir = new File(
Environment.getExternalStorageDirectory() + "/Movies",
appName
);
} else {
mediaStorageDir = new File(appContext.getExternalCacheDir().getPath());
}
if (!mediaStorageDir.exists()) {
if (!mediaStorageDir.mkdir()) {
callback.error("Can't access or make Movies directory");
return;
}
}
final String outputFilePath = new File(
mediaStorageDir.getPath(),
"VID_" + outputFileName + outputExtension
).getAbsolutePath();
Log.v(TAG, "outputFilePath: " + outputFilePath);
final String videoStartTime = options.getString("startTime");
final String videoEndTime = options.getString("endTime");
final int overlayIndex = options.optInt("overlayIndex");
Log.d(TAG, "videoStartTime: " + videoStartTime);
Log.d(TAG, "videoEndTime: " + videoEndTime);
Log.d(TAG, "overlayIndex: " + overlayIndex);
final File inFile = this.resolveLocalFileSystemURI(options.getString("fileUri")); // intro
final File inFile2 = this.resolveLocalFileSystemURI(options.getString("fileUri2")); // recording
final File inFile3 = this.resolveLocalFileSystemURI(options.getString("fileUri3")); // outro
final File inFile4 = this.resolveLocalFileSystemURI(options.getString("fileUri4")); // overlay
cordova.getThreadPool().execute(new Runnable() {
public void run() {
try {
ffmpegController = new FfmpegController(appContext, appContext.getCacheDir());
TranscodeCallback tcCallback = new TranscodeCallback();
ArrayList<Clip> listVideos = new ArrayList<Clip>();
// intro
if (inFile.exists()) {
Clip clip = new Clip();
clip.path = new File(inFile.getAbsolutePath()).getCanonicalPath();
ffmpegController.getInfo(clip);
listVideos.add(clip);
}
// recording
if (inFile2.exists()) {
Clip clip2 = new Clip();
clip2.path = new File(inFile2.getAbsolutePath()).getCanonicalPath();
clip2.startTime = videoStartTime;
clip2.endTime = videoEndTime;
ffmpegController.getInfo(clip2);
listVideos.add(clip2);
}
// outro
if (inFile3.exists()) {
Clip clip3 = new Clip();
clip3.path = new File(inFile3.getAbsolutePath()).getCanonicalPath();
ffmpegController.getInfo(clip3);
listVideos.add(clip3);
}
// overlay
String overlayPath = null;
if (inFile4.exists()) {
overlayPath = inFile4.getAbsolutePath();
}
// output
Clip clipOut = new Clip();
clipOut.path = new File(outputFilePath).getCanonicalPath();
// run command line tools
int exitValue = ffmpegController.concatAndTrimFilesMP4Stream(listVideos, clipOut, false, false, new ShellUtils.ShellCallback() {
@Override
public void shellOut(String shellLine) {
//System.out.println("fc>" + shellLine);
// https://github.com/apache/cordova-plugin-file-transfer/blob/master/src/android/FileTransfer.java#L482
try {
JSONObject jsonObj = new JSONObject();
jsonObj.put("loaded", shellLine.toString());
jsonObj.put("total", 0);
jsonObj.put("lengthComputable", true);
PluginResult progressResult = new PluginResult(PluginResult.Status.OK, jsonObj);
progressResult.setKeepCallback(true);
callback.sendPluginResult(progressResult);
} catch (JSONException e) {
Log.d(TAG, "PluginResult error: " + e);
}
}
@Override
public void processComplete(int exitValue) {
}
}, overlayPath, overlayIndex);
// create a file from the path returned
File outFile = new File(outputFilePath);
// check if the file exists
if (outFile.exists()) {
// if the render was cancelled, return error meesage
if (exitValue == 255) {
outFile.delete();
callback.error("render was cancelled");
return;
} else {
if (saveToLibrary) {
// remove the original input file when saving to gallery
// comment out or remove the delete based on your needs
//if (!inFile.delete()) {
// Log.d(TAG, "unable to delete in file");
//}
Intent scanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
//scanIntent.setData(Uri.fromFile(inFile));
//scanIntent.setData(Uri.fromFile(inFile2));
//scanIntent.setData(Uri.fromFile(inFile3));
//scanIntent.setData(Uri.fromFile(inFile4));
scanIntent.setData(Uri.fromFile(outFile));
appContext.sendBroadcast(scanIntent);
}
callback.success(outputFilePath);
}
} else {
Log.d(TAG, "outputFile doesn't exist!");
callback.error("an error ocurred during transcoding");
return;
}
} catch (Throwable e) {
Log.d(TAG, "transcode exception ", e);
callback.error(e.toString());
}
}
});
}
private class TranscodeCallback implements ShellCallback {
@Override
public void shellOut(String shellLine) {
Log.v(TAG, "shellOut: " + shellLine);
}
@Override
public void processComplete(int exitValue) {
Log.v(TAG, "processComplete: " + exitValue);
}
}
private void killVideoProcessor(JSONArray args) throws JSONException, IOException {
Log.d(TAG, "killVideoProcessor");
ffmpegController.killVideoProcessor(false, true);
}
@SuppressWarnings("unused")
private void createThumbnail(JSONArray args) throws JSONException, IOException {
Log.d(TAG, "createThumbnail firing");
/* createThumbnail arguments:
fileUri: video input url
outputFileName: output file name
*/
JSONObject options = args.optJSONObject(0);
Log.d(TAG, "options: " + options.toString());
File inFile = this.resolveLocalFileSystemURI(options.getString("fileUri"));
if (!inFile.exists()) {
Log.d(TAG, "input file does not exist");
callback.error("input video does not exist.");
return;
}
String srcVideoPath = inFile.getAbsolutePath();
String outputFileName = options.optString(
"outputFileName",
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH).format(new Date())
);
Context appContext = cordova.getActivity().getApplicationContext();
PackageManager pm = appContext.getPackageManager();
ApplicationInfo ai;
try {
ai = pm.getApplicationInfo(cordova.getActivity().getPackageName(), 0);
} catch (final NameNotFoundException e) {
ai = null;
}
final String appName = (String) (ai != null ? pm.getApplicationLabel(ai) : "Unknown");
File tempStoragePath = appContext.getExternalCacheDir();
File outputFile = new File(
tempStoragePath.getPath(),
"PIC_" + outputFileName + ".jpg"
);
Bitmap thumbnail = ThumbnailUtils.createVideoThumbnail(srcVideoPath, MediaStore.Images.Thumbnails.MINI_KIND);
FileOutputStream theOutputStream;
try {
if (!outputFile.exists()) {
if (!outputFile.createNewFile()) {
callback.error("Could not save thumbnail.");
}
}
if (outputFile.canWrite()) {
theOutputStream = new FileOutputStream(outputFile);
if (theOutputStream != null) {
thumbnail.compress(CompressFormat.JPEG, 75, theOutputStream);
} else {
callback.error("Could not save thumbnail; target not writeable");
}
}
} catch (IOException e) {
callback.error(e.toString());
}
callback.success(outputFile.getAbsolutePath());
}
@SuppressWarnings("deprecation")
private File resolveLocalFileSystemURI(String url) throws IOException, JSONException {
Log.d(TAG, "resolveLocalFileSystemURI: " + url);
if (url == null || url.isEmpty() || url == "null") {
return new File("file:///dummy.txt");
}
String decoded = URLDecoder.decode(url, "UTF-8");
File fp = null;
// Handle the special case where you get an Android content:// uri.
if (decoded.startsWith("content:")) {
fp = new File(getPath(this.cordova.getActivity().getApplicationContext(), Uri.parse(decoded)));
} else {
// Test to see if this is a valid URL first
@SuppressWarnings("unused")
URL testUrl = new URL(decoded);
if (decoded.startsWith("file://")) {
int questionMark = decoded.indexOf("?");
if (questionMark < 0) {
fp = new File(decoded.substring(7, decoded.length()));
} else {
Log.d(TAG, "4: " + decoded.substring(7, questionMark));
fp = new File(decoded.substring(7, questionMark));
}
} else if (decoded.startsWith("file:/")) {
fp = new File(decoded.substring(6, decoded.length()));
} else {
fp = new File(decoded);
}
}
if (!fp.exists()) {
throw new FileNotFoundException();
}
if (!fp.canRead()) {
throw new IOException();
}
return fp;
}
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
* other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @author paulburke
*/
public static String getPath(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment