From 0299b4dbf0331ce8d085eb68d75c30fe74a4ebd1 Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 1 Jun 2016 19:54:36 +0800 Subject: [PATCH 1/7] Add Activity to control. --- app/build.gradle | 25 + app/src/main/java/com/code/DialogItem.java | 63 + app/src/main/java/com/code/FileUtil.java | 53 + app/src/main/java/com/code/MainActivity.java | 150 ++ .../net/sourceforge/sox/CrossfadeCat.java | 99 + .../net/sourceforge/sox/SoxController.java | 380 ++++ .../main/java/org/ffmpeg/android/Clip.java | 66 + .../org/ffmpeg/android/FfmpegController.java | 1841 +++++++++++++++++ .../java/org/ffmpeg/android/ShellUtils.java | 234 +++ .../android/filters/CropVideoFilter.java | 133 ++ .../android/filters/DrawBoxVideoFilter.java | 52 + .../android/filters/DrawTextVideoFilter.java | 124 ++ .../android/filters/FadeVideoFilter.java | 28 + .../android/filters/OverlayVideoFilter.java | 65 + .../android/filters/RedactVideoFilter.java | 23 + .../android/filters/TransposeVideoFilter.java | 29 + .../ffmpeg/android/filters/VideoFilter.java | 25 + .../org/ffmpeg/android/test/ConcatTest.java | 62 + .../org/ffmpeg/android/test/ConvertTest.java | 5 + .../ffmpeg/android/test/CrossfadeTest.java | 188 ++ .../org/ffmpeg/android/test/FilterTest.java | 65 + .../java/org/ffmpeg/android/test/MixTest.java | 325 +++ .../java/org/ffmpeg/android/test/Tests.java | 75 + .../res/drawable-hdpi/ic_action_search.png | Bin 0 -> 3120 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4996 bytes .../res/drawable-mdpi/ic_action_search.png | Bin 0 -> 3030 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 3065 bytes .../res/drawable-xhdpi/ic_action_search.png | Bin 0 -> 3199 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 6679 bytes app/src/main/res/layout/main.xml | 16 + app/src/main/res/layout/main_activity.xml | 18 + app/src/main/res/raw/ffmpeg | Bin 0 -> 15413156 bytes app/src/main/res/raw/sox | Bin 0 -> 8617076 bytes app/src/main/res/values-v11/styles.xml | 5 + app/src/main/res/values-v14/styles.xml | 5 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 5 + 37 files changed, 4162 insertions(+) create mode 100644 app/build.gradle create mode 100644 app/src/main/java/com/code/DialogItem.java create mode 100644 app/src/main/java/com/code/FileUtil.java create mode 100644 app/src/main/java/com/code/MainActivity.java create mode 100644 app/src/main/java/net/sourceforge/sox/CrossfadeCat.java create mode 100644 app/src/main/java/net/sourceforge/sox/SoxController.java create mode 100644 app/src/main/java/org/ffmpeg/android/Clip.java create mode 100644 app/src/main/java/org/ffmpeg/android/FfmpegController.java create mode 100644 app/src/main/java/org/ffmpeg/android/ShellUtils.java create mode 100644 app/src/main/java/org/ffmpeg/android/filters/CropVideoFilter.java create mode 100644 app/src/main/java/org/ffmpeg/android/filters/DrawBoxVideoFilter.java create mode 100644 app/src/main/java/org/ffmpeg/android/filters/DrawTextVideoFilter.java create mode 100644 app/src/main/java/org/ffmpeg/android/filters/FadeVideoFilter.java create mode 100644 app/src/main/java/org/ffmpeg/android/filters/OverlayVideoFilter.java create mode 100644 app/src/main/java/org/ffmpeg/android/filters/RedactVideoFilter.java create mode 100644 app/src/main/java/org/ffmpeg/android/filters/TransposeVideoFilter.java create mode 100644 app/src/main/java/org/ffmpeg/android/filters/VideoFilter.java create mode 100644 app/src/main/java/org/ffmpeg/android/test/ConcatTest.java create mode 100644 app/src/main/java/org/ffmpeg/android/test/ConvertTest.java create mode 100644 app/src/main/java/org/ffmpeg/android/test/CrossfadeTest.java create mode 100644 app/src/main/java/org/ffmpeg/android/test/FilterTest.java create mode 100644 app/src/main/java/org/ffmpeg/android/test/MixTest.java create mode 100644 app/src/main/java/org/ffmpeg/android/test/Tests.java create mode 100644 app/src/main/res/drawable-hdpi/ic_action_search.png create mode 100644 app/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_search.png create mode 100644 app/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_search.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/layout/main.xml create mode 100644 app/src/main/res/layout/main_activity.xml create mode 100755 app/src/main/res/raw/ffmpeg create mode 100644 app/src/main/res/raw/sox create mode 100644 app/src/main/res/values-v11/styles.xml create mode 100644 app/src/main/res/values-v14/styles.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f6f248c --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 19 + buildToolsVersion "23.0.2" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 15 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + +} +dependencies { + compile 'com.google.code.gson:gson:2.4' + compile 'com.jakewharton:butterknife:6.0.0' + compile 'com.googlecode.mp4parser:isoparser:1.1.7' +// compile project(':imagevideo') +} diff --git a/app/src/main/java/com/code/DialogItem.java b/app/src/main/java/com/code/DialogItem.java new file mode 100644 index 0000000..65e274b --- /dev/null +++ b/app/src/main/java/com/code/DialogItem.java @@ -0,0 +1,63 @@ +package com.code; + +/** + * MovieClip下的对话项. + * 原始JSON: + *

+ * { + * * dialog_id : 922045 + * role_id : 646 + * content_en : Mom, what happened on the plane. I'm sorry. + * content_cn : 妈妈,飞机上的事。我很抱歉。 + * time_begin : 700 + * time_end : 6200 + * fea : http://www.mofunenglish.com/storage/pool0/text/26/53/20150130195314734526001421.fea + * fea_v2 : http://www.mofunenglish.com/storage/pool0/text/114/9/201503251350216963090011373528.fea + * fea_content : Mom, what happened on the plane. I'm sorry. + * fea_byte : 9009 + * fea_v2_byte : 9025 + * expl_count : 0 + * } + *

+ * Created by Administrator on 13-8-17. + */ +public class DialogItem { + + + public long dialog_id; + public int role_id; + public String content_en; + public String content_cn; + public int time_begin; + public int time_end; + public String fea; + public String fea_v2; + public String fea_content; + public int fea_byte; + public int fea_v2_byte; + public int expl_count; + + /** + * 获取对应文件路径 + * + * @return + */ + public String getMp3FilePath() { + return "/storage/emulated/0/.mofunshow/records" + "/" + this.dialog_id + ".mp3"; + } + + public String getRecordFilePath() { + return "/storage/emulated/0/.mofunshow/records" + "/" + this.dialog_id + ".wav"; + } + + /** + * 判断是否属于某角色,若传入0则认为是全角色,返回true + * + * @param roleId + * @return + */ + public boolean isBelongToRole(long roleId) { + return (roleId <= 0) || (roleId > 0 + && roleId == role_id); + } +} diff --git a/app/src/main/java/com/code/FileUtil.java b/app/src/main/java/com/code/FileUtil.java new file mode 100644 index 0000000..2e40bca --- /dev/null +++ b/app/src/main/java/com/code/FileUtil.java @@ -0,0 +1,53 @@ +package com.code; + + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Created by Administrator on 13-8-19. + */ +public class FileUtil { + + + public static boolean writeJsonFile(String file_name, String json_content) { + FileOutputStream fout = null; + + try { + fout = new FileOutputStream(file_name); + byte[] bytes = json_content.getBytes(); + fout.write(bytes); + + } catch (Exception e) { + e.printStackTrace(); + return false; + } finally { + if (fout != null) { + try { + fout.flush(); + fout.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return true; + } + + public static String readJsonFile(String file_name) { + String json_content = ""; + try { + FileInputStream fin = new FileInputStream(file_name); + int length = fin.available(); + byte[] buffer = new byte[length]; + fin.read(buffer); + json_content = new String(buffer, "UTF-8"); + fin.close(); + } catch (Exception e) { + e.printStackTrace(); + return json_content; + } + return json_content; + } +} diff --git a/app/src/main/java/com/code/MainActivity.java b/app/src/main/java/com/code/MainActivity.java new file mode 100644 index 0000000..78301e7 --- /dev/null +++ b/app/src/main/java/com/code/MainActivity.java @@ -0,0 +1,150 @@ +package com.code; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.media.MediaMetadataRetriever; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; + +import org.ffmpeg.android.R; +import org.ffmpeg.android.test.MixTest; + +import java.io.File; +import java.io.FileOutputStream; + +import butterknife.ButterKnife; +import butterknife.InjectView; + +/** + * @author Chad + * @title com.code + * @description + * @modifier + * @date + * @since 16/5/31 上午12:04 + **/ +public class MainActivity extends Activity { + + @InjectView(R.id.text_mix) + Button textMix; + String mp4FilePath = "/storage/emulated/0/.mofunshow/movies/90331/20160520192149267651000519.mp4"; + String bgMp3FilePath = "/storage/emulated/0/.mofunshow/movies/90331/20160520191953876045000830.aac"; + String jsonFilePath = "/storage/emulated/0/.mofunshow/movies/90331/20160520192149267651000519.json"; + String bgWavFilePath = "/storage/emulated/0/.mofunshow/movies/90331/2015082214101800000358814.wav"; + String mp4OutPath = "/storage/emulated/0/.mofunshow/movies/90331/test_out.mp4"; + String mp4AllPath = "/storage/emulated/0/.mofunshow/movies/90331/test_out_all.mp4"; + String pngPath = "/storage/emulated/0/.mofunshow/movies/90331/1.png"; + String pngTempPath = "/storage/emulated/0/.mofunshow/movies/90331/temp.png"; + String tempPath = "/storage/emulated/0/ffmpeg/"; + @InjectView(R.id.test_merge) + Button testMerge; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + ButterKnife.inject(this); + new File(tempPath).mkdirs(); + textMix.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { +// List dialog_list; +// dialog_list = new Gson().fromJson(FileUtil.readJsonFile(jsonFilePath), new TypeToken>() {}.getType()); +// try { +// MixTest.test(tempPath,mp4FilePath,bgMp3FilePath,new Clip(mp4OutPath),getApplicationContext()); +// } catch (Exception e) { +// e.printStackTrace(); +// } + +// try { +// List list = new ArrayList(); +// list.add(getImageFileFromVideo(mp4FilePath,pngTempPath)); +// MixTest.testJpegToMp4(mp4FilePath,tempPath,list,mp4OutPath,getApplicationContext()); +// } catch (Exception e) { +// e.printStackTrace(); +// } + try { + MixTest.testMakeLastFrameFilter(getImageFileFromVideo(mp4FilePath, pngTempPath), getTimeLengthFromVideo(mp4FilePath), mp4FilePath, tempPath, mp4OutPath, getApplicationContext()); + } catch (Exception e) { + e.printStackTrace(); + } +// ImageConvertVideo imageConvertVideo = null; +// Bitmap bitmap = getBitmapsFromVideo(mp4FilePath); +// try { +// imageConvertVideo = new ImageConvertVideo(new File(mp4OutPath), 3); +// imageConvertVideo.encodeImage(bitmap, 1, 1); +// imageConvertVideo.encodeImage(bitmap, 2, 1); +// imageConvertVideo.encodeImage(bitmap, 3, 1); +// imageConvertVideo.encodeImage(bitmap, 4, 1); +// } catch (IOException e) { +// e.printStackTrace(); +// } finally { +// if (imageConvertVideo != null) { +// try { +// imageConvertVideo.finish(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// } + } + }); + testMerge.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + try { + MixTest.testMergeMp4(mp4FilePath, mp4OutPath, tempPath, mp4AllPath, getApplicationContext()); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + } + + public Bitmap getBitmapsFromVideo(String mp4FilePath) { + Bitmap bitmap = null; + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(mp4FilePath); + // 取得视频的长度(单位为毫秒) + String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + // 取得视频的长度(单位为秒) + int seconds = Integer.valueOf(time) / 1000; + // 得到每一秒时刻的bitmap比如第一秒,第二秒 + bitmap = retriever.getFrameAtTime(seconds * 1000 * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); + return bitmap; + } + + public long getTimeLengthFromVideo(String mp4FilePath) { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(mp4FilePath); + // 取得视频的长度(单位为毫秒) + String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + // 取得视频的长度(单位为秒) + int milins = Integer.valueOf(time); + return milins; + } + + public String getImageFileFromVideo(String mp4FilePath, String outPath) { + Bitmap bitmap = null; + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(mp4FilePath); + // 取得视频的长度(单位为毫秒) + String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + // 取得视频的长度(单位为秒) + int millSeconds = Integer.valueOf(time); + int seconds = millSeconds / 1000; + // 得到每一秒时刻的bitmap比如第一秒,第二秒 + bitmap = retriever.getFrameAtTime(millSeconds * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); + FileOutputStream fos = null; + try { + fos = new FileOutputStream(outPath); + bitmap.compress(Bitmap.CompressFormat.PNG, 80, fos); + fos.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return outPath; + } +} diff --git a/app/src/main/java/net/sourceforge/sox/CrossfadeCat.java b/app/src/main/java/net/sourceforge/sox/CrossfadeCat.java new file mode 100644 index 0000000..72d9ecc --- /dev/null +++ b/app/src/main/java/net/sourceforge/sox/CrossfadeCat.java @@ -0,0 +1,99 @@ +package net.sourceforge.sox; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +/** + * Concatenates two files together with a crossfade of user + * defined mClipLength. + *

+ * It is a Java port of the scripts/crossfade_cat.sh script + * in the sox source tree. + *

+ * Original script by Kester Clegg, with modifications by Chris + * Bagwell. + * + * @author Abel Luck + */ +// TODO make runnable? +public class CrossfadeCat { + private final static String TAG = "SOX-XFADE"; + private SoxController mController; + private String mFirstFile; + private String mSecondFile; + private double mFadeLength; + private String mFinalMix; + + public CrossfadeCat(SoxController controller, String firstFile, String secondFile, double fadeLength, String outFile) { + mController = controller; + mFirstFile = firstFile; + mSecondFile = secondFile; + mFadeLength = fadeLength; + mFinalMix = outFile; + + //double mClipLength = mController.getLength(mFirstFile); + } + + public boolean start() throws Exception { + // find mClipLength of first file + + + // Obtain trimLength seconds of fade out position from the first File + double firstFileLength = mController.getLength(mFirstFile); + double trimLength = firstFileLength - mFadeLength; + + String trimmedOne = mController.trimAudio(mFirstFile, trimLength, mFadeLength); + + if (trimmedOne == null) + throw new IOException("audio trim did not complete: " + mFirstFile); + + // We assume a fade out is needed (i.e., firstFile doesn't already fade out) + + String fadedOne = mController.fadeAudio(trimmedOne, "t", 0, mFadeLength, mFadeLength); + if (fadedOne == null) + throw new IOException("audio fade did not complete: " + trimmedOne); + + // Get crossfade section from the second file + String trimmedTwo = mController.trimAudio(mSecondFile, 0, mFadeLength); + if (trimmedTwo == null) + throw new IOException("audio trim did not complete: " + mSecondFile); + + String fadedTwo = mController.fadeAudio(trimmedTwo, "t", mFadeLength, -1, -1); + if (fadedTwo == null) + throw new IOException("audio fade did not complete: " + trimmedTwo); + + // Mix crossfaded files together at full volume + ArrayList files = new ArrayList(); + files.add(fadedOne); + files.add(fadedTwo); + + String crossfaded = new File(mFirstFile).getCanonicalPath() + "-x-" + new File(mSecondFile).getName() + ".wav"; + crossfaded = mController.combineMix(files, crossfaded); + if (crossfaded == null) + throw new IOException("crossfade did not complete"); + + // Trim off crossfade sections from originals + String trimmedThree = mController.trimAudio(mFirstFile, 0, trimLength); + if (trimmedThree == null) + throw new IOException("crossfade trim beginning did not complete"); + + String trimmedFour = mController.trimAudio(mSecondFile, mFadeLength, -1); + if (trimmedFour == null) + throw new IOException("crossfade trim end did not complete"); + + // Combine into final mix + files.clear(); + files.add(trimmedThree); + files.add(crossfaded); + files.add(trimmedFour); + mFinalMix = mController.combine(files, mFinalMix); + + if (mFinalMix == null) + throw new IOException("final mix did not complete"); + + return true; + } + + +} diff --git a/app/src/main/java/net/sourceforge/sox/SoxController.java b/app/src/main/java/net/sourceforge/sox/SoxController.java new file mode 100644 index 0000000..d23fde9 --- /dev/null +++ b/app/src/main/java/net/sourceforge/sox/SoxController.java @@ -0,0 +1,380 @@ +package net.sourceforge.sox; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import org.ffmpeg.android.R; +import org.ffmpeg.android.ShellUtils.ShellCallback; + +import android.content.Context; +import android.util.Log; + +public class SoxController { + private final static String TAG = "SOX"; + String[] libraryAssets = {"sox"}; + private String soxBin; + private File fileBinDir; + private ShellCallback callback; + + public SoxController(Context context, File fileAppRoot, ShellCallback _callback) throws FileNotFoundException, IOException { + callback = _callback; + + installBinaries(context, false); + fileBinDir = new File(soxBin).getParentFile(); + + } + + + public void installBinaries(Context context, boolean overwrite) { + soxBin = installBinary(context, R.raw.sox, "sox", overwrite); + } + + public String getBinaryPath() { + return soxBin; + } + + private static String installBinary(Context ctx, int resId, String filename, boolean upgrade) { + try { + File f = new File(ctx.getDir("bin", 0), filename); + if (f.exists()) { + f.delete(); + } + copyRawFile(ctx, resId, f, "0755"); + return f.getCanonicalPath(); + } catch (Exception e) { + Log.e(TAG, "installBinary failed: " + e.getLocalizedMessage()); + return null; + } + } + + /** + * Copies a raw resource file, given its ID to the given location + * + * @param ctx context + * @param resid resource id + * @param file destination file + * @param mode file permissions (E.g.: "755") + * @throws IOException on error + * @throws InterruptedException when interrupted + */ + private static void copyRawFile(Context ctx, int resid, File file, String mode) throws IOException, InterruptedException { + final String abspath = file.getAbsolutePath(); + // Write the iptables binary + final FileOutputStream out = new FileOutputStream(file); + final InputStream is = ctx.getResources().openRawResource(resid); + byte buf[] = new byte[1024]; + int len; + while ((len = is.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.close(); + is.close(); + // Change the permissions + Runtime.getRuntime().exec("chmod " + mode + " " + abspath).waitFor(); + } + + + private class LengthParser implements ShellCallback { + public double length; + public int retValue = -1; + + @Override + public void shellOut(String shellLine) { + if (!shellLine.startsWith("Length")) + return; + String[] split = shellLine.split(":"); + if (split.length != 2) return; + + String lengthStr = split[1].trim(); + + try { + length = Double.parseDouble(lengthStr); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + + @Override + public void processComplete(int exitValue) { + retValue = exitValue; + + } + } + + /** + * Retrieve the length of the audio file + * sox file.wav 2>&1 -n stat | grep Length | cut -d : -f 2 | cut -f 1 + * + * @return the length in seconds or null + */ + public double getLength(String path) { + ArrayList cmd = new ArrayList(); + + cmd.add(soxBin); + cmd.add(path); + cmd.add("-n"); + cmd.add("stat"); + + LengthParser sc = new LengthParser(); + + try { + execSox(cmd, sc); + } catch (Exception e) { + return -1; + } + + return sc.length; + } + + /** + * Discard all audio not between start and length (length = end by default) + * sox -e signed-integer -b 16 outFile trim + * + * @param start + * @param length (optional) + * @return path to trimmed audio + */ + public String trimAudio(String path, double start, double length) throws Exception { + ArrayList cmd = new ArrayList(); + + File file = new File(path); + String outFile = file.getCanonicalPath() + "_trimmed.wav"; + cmd.add(soxBin); + cmd.add(path); + cmd.add("-e"); + cmd.add("signed-integer"); + cmd.add("-b"); + cmd.add("16"); + cmd.add(outFile); + cmd.add("trim"); + cmd.add(start + ""); + if (length != -1) + cmd.add(length + ""); + + int rc = execSox(cmd, callback); + if (rc != 0) { + outFile = null; + } + + if (file.exists()) + return outFile; + else + return null; + + } + + /** + * Fade audio file + * sox outFile fade + * + * @param path + * @param type + * @param fadeInLength specify 0 if no fade in is desired + * @param stopTime (optional) + * @param fadeOutLength (optional) + * @return + */ + public String fadeAudio(String path, String type, double fadeInLength, double stopTime, double fadeOutLength) throws IOException { + + final List curves = Arrays.asList(new String[]{"q", "h", "t", "l", "p"}); + + if (!curves.contains(type)) { + throw new RuntimeException("fadeAudio: passed invalid type: " + type); + + } + + File file = new File(path); + String outFile = file.getCanonicalPath() + "_faded.wav"; + + ArrayList cmd = new ArrayList(); + cmd.add(soxBin); + cmd.add(path); + cmd.add(outFile); + cmd.add("fade"); + cmd.add(type); + cmd.add(fadeInLength + ""); + if (stopTime != -1) + cmd.add(stopTime + ""); + if (fadeOutLength != -1) + cmd.add(fadeOutLength + ""); + + try { + int rc = execSox(cmd, callback); + if (rc != 0) { + //Log.e(TAG, "fadeAudio receieved non-zero return code!"); + + outFile = null; + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return outFile; + } + + /** + * Combine and mix audio files + * sox -m -v 1.0 file[0] -v 1.0 file[1] ... -v 1.0 file[n] outFile + * TODO support passing of volume + * + * @param files + * @return combined and mixed file (null on failure) + */ + public String combineMix(List files, String outFile) { + ArrayList cmd = new ArrayList(); + cmd.add(soxBin); + cmd.add("-m"); + + for (String file : files) { + cmd.add("-v"); + cmd.add("1.0"); + cmd.add(file); + } + cmd.add(outFile); + + try { + int rc = execSox(cmd, callback); + if (rc != 0) { + // Log.e(TAG, "combineMix receieved non-zero return code!"); + outFile = null; + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return outFile; + } + + /** + * Simple combiner + * sox file[0] file[1] ... file[n] + * + * @param files + * @param outFile + * @return outFile or null on failure + */ + public String combine(List files, String outFile) throws Exception { + ArrayList cmd = new ArrayList(); + cmd.add(soxBin); + + for (String file : files) { + cmd.add(file); + } + cmd.add(outFile); + + int rc = execSox(cmd, callback); + if (rc != 0) { + throw new Exception("exit code: " + rc); + + } + + return outFile; + } + + /** + * Takes a seconds.frac value and formats it into: + * hh:mm:ss:ss.frac + * + * @param seconds + */ + /* + public String formatTimePeriod(double seconds) { + + long milliTime = (long)(seconds * 100f); + Date dateTime = new Date(milliTime); + return String.format(Locale.US, "%s:%s.%s", dateTime.getHours(),dateTime.getMinutes(),dateTime.getSeconds()); + }*/ + public int execSox(List cmd, ShellCallback sc) throws IOException, + InterruptedException { + + String soxBin = new File(fileBinDir, "sox").getCanonicalPath(); + + Runtime.getRuntime().exec("chmod 700 " + soxBin); + return execProcess(cmd, sc); + } + + private int execProcess(List cmds, ShellCallback sc) + throws IOException, InterruptedException { + + //ensure that the arguments are in the correct Locale format + for (String cmd : cmds) { + cmd = String.format(Locale.US, "%s", cmd); + } + + ProcessBuilder pb = new ProcessBuilder(cmds); + pb.directory(fileBinDir); + + StringBuffer cmdlog = new StringBuffer(); + + for (String cmd : cmds) { + cmdlog.append(cmd); + cmdlog.append(' '); + } + + sc.shellOut(cmdlog.toString()); + + // pb.redirectErrorStream(true); + Process process = pb.start(); + + // any error message? + StreamGobbler errorGobbler = new StreamGobbler( + process.getErrorStream(), "ERROR", sc); + + // any output? + StreamGobbler outputGobbler = new StreamGobbler( + process.getInputStream(), "OUTPUT", sc); + + // kick them off + errorGobbler.start(); + outputGobbler.start(); + + int exitVal = process.waitFor(); + + while (outputGobbler.isAlive() || errorGobbler.isAlive()) ; + + sc.processComplete(exitVal); + + return exitVal; + } + + class StreamGobbler extends Thread { + InputStream is; + String type; + ShellCallback sc; + + StreamGobbler(InputStream is, String type, ShellCallback sc) { + this.is = is; + this.type = type; + this.sc = sc; + } + + public void run() { + try { + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr); + String line = null; + while ((line = br.readLine()) != null) + if (sc != null) + sc.shellOut(line); + + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + } +} diff --git a/app/src/main/java/org/ffmpeg/android/Clip.java b/app/src/main/java/org/ffmpeg/android/Clip.java new file mode 100644 index 0000000..d4e3f26 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/Clip.java @@ -0,0 +1,66 @@ +package org.ffmpeg.android; + +public class Clip implements Cloneable { + + public int width = -1; + public int height = -1; + + public String videoCodec; + public String videoFps; + public int videoBitrate = -1; + public String videoBitStreamFilter; + + public String audioCodec; + public int audioChannels = -1; + public int audioBitrate = -1; + public String audioQuality; + public int audioVolume = -1; + public String audioBitStreamFilter; + + public String path; + public String format; + public String mimeType; + + public String startTime; //00:00:00 or seconds format + public double duration = -1; //00:00:00 or seconds format + + public String videoFilter; + public String audioFilter; + + public String qscale; + public String aspect; + public int passCount = 1; //default + + public Clip() { + + } + + public Clip(String path) { + this.path = path; + } + + public Clip clone() throws CloneNotSupportedException { + return (Clip) super.clone(); + } + + public boolean isImage() { + if (mimeType != null) + return mimeType.startsWith("image"); + else + return false; + } + + public boolean isVideo() { + if (mimeType != null) + return mimeType.startsWith("video"); + else + return false; + } + + public boolean isAudio() { + if (mimeType != null) + return mimeType.startsWith("audio"); + else + return false; + } +} diff --git a/app/src/main/java/org/ffmpeg/android/FfmpegController.java b/app/src/main/java/org/ffmpeg/android/FfmpegController.java new file mode 100644 index 0000000..3c88139 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/FfmpegController.java @@ -0,0 +1,1841 @@ +package org.ffmpeg.android; + + +import android.content.Context; +import android.graphics.Bitmap; +import android.media.MediaMetadataRetriever; +import android.util.Log; + +import org.ffmpeg.android.ShellUtils.ShellCallback; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.StringTokenizer; + +public class FfmpegController { + + + private String mFfmpegBin; + + private final static String TAG = "FFMPEG"; + + private File mFileTemp; + + private String mCmdCat = "sh cat"; + + public FfmpegController(Context context, File fileTemp) throws FileNotFoundException, IOException { + mFileTemp = fileTemp; + + installBinaries(context, false); + } + + public void installBinaries(Context context, boolean overwrite) { + mFfmpegBin = installBinary(context, R.raw.ffmpeg, "ffmpeg", overwrite); + } + + public String getBinaryPath() { + return mFfmpegBin; + } + + private static String installBinary(Context ctx, int resId, String filename, boolean upgrade) { + try { + File f = new File(ctx.getDir("bin", 0), filename); + if (f.exists()) { + f.delete(); + } + copyRawFile(ctx, resId, f, "0755"); + return f.getCanonicalPath(); + } catch (Exception e) { + Log.e(TAG, "installBinary failed: " + e.getLocalizedMessage()); + return null; + } + } + + /** + * Copies a raw resource file, given its ID to the given location + * + * @param ctx context + * @param resid resource id + * @param file destination file + * @param mode file permissions (E.g.: "755") + * @throws IOException on error + * @throws InterruptedException when interrupted + */ + private static void copyRawFile(Context ctx, int resid, File file, String mode) throws IOException, InterruptedException { + final String abspath = file.getAbsolutePath(); + // Write the iptables binary + final FileOutputStream out = new FileOutputStream(file); + final InputStream is = ctx.getResources().openRawResource(resid); + byte buf[] = new byte[1024]; + int len; + while ((len = is.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.close(); + is.close(); + // Change the permissions + Runtime.getRuntime().exec("chmod " + mode + " " + abspath).waitFor(); + } + + + private void execFFMPEG(List cmd, ShellCallback sc, File fileExec) throws IOException, InterruptedException { + + enablePermissions(); + + execProcess(cmd, sc, fileExec); + } + + private void enablePermissions() throws IOException { + Runtime.getRuntime().exec("chmod 700 " + mFfmpegBin); + + } + + private void execFFMPEG(List cmd, ShellCallback sc) throws IOException, InterruptedException { + execFFMPEG(cmd, sc, new File(mFfmpegBin).getParentFile()); + } + + private int execProcess(List cmds, ShellCallback sc, File fileExec) throws IOException, InterruptedException { + + //ensure that the arguments are in the correct Locale format + for (String cmd : cmds) { + cmd = String.format(Locale.US, "%s", cmd); + } + + ProcessBuilder pb = new ProcessBuilder(cmds); + pb.directory(fileExec); + + StringBuffer cmdlog = new StringBuffer(); + + for (String cmd : cmds) { + cmdlog.append(cmd); + cmdlog.append(' '); + } + + sc.shellOut(cmdlog.toString()); + + //pb.redirectErrorStream(true); + + Process process = pb.start(); + + + // any error message? + StreamGobbler errorGobbler = new StreamGobbler( + process.getErrorStream(), "ERROR", sc); + + // any output? + StreamGobbler outputGobbler = new + StreamGobbler(process.getInputStream(), "OUTPUT", sc); + + errorGobbler.start(); + outputGobbler.start(); + + int exitVal = process.waitFor(); + + sc.processComplete(exitVal); + + return exitVal; + + } + + + private int execProcess(String cmd, ShellCallback sc, File fileExec) throws IOException, InterruptedException { + + //ensure that the argument is in the correct Locale format + cmd = String.format(Locale.US, "%s", cmd); + + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.directory(fileExec); + + // pb.redirectErrorStream(true); + Process process = pb.start(); + + + // any error message? + StreamGobbler errorGobbler = new + StreamGobbler(process.getErrorStream(), "ERROR", sc); + + // any output? + StreamGobbler outputGobbler = new + StreamGobbler(process.getInputStream(), "OUTPUT", sc); + + // kick them off + errorGobbler.start(); + outputGobbler.start(); + + + int exitVal = process.waitFor(); + + sc.processComplete(exitVal); + + return exitVal; + + + } + + + public class Argument { + String key; + String value; + + public static final String VIDEOCODEC = "-vcodec"; + public static final String AUDIOCODEC = "-acodec"; + + public static final String VIDEOBITSTREAMFILTER = "-vbsf"; + public static final String AUDIOBITSTREAMFILTER = "-absf"; + + public static final String VERBOSITY = "-v"; + public static final String FILE_INPUT = "-i"; + public static final String SIZE = "-s"; + public static final String FRAMERATE = "-r"; + public static final String FORMAT = "-f"; + public static final String BITRATE_VIDEO = "-b:v"; + + public static final String BITRATE_AUDIO = "-b:a"; + public static final String CHANNELS_AUDIO = "-ac"; + public static final String FREQ_AUDIO = "-ar"; + public static final String NO_AUDIO = "-an"; + + public static final String STARTTIME = "-ss"; + public static final String DURATION = "-t"; + + + } + + public void processVideo(Clip in, Clip out, boolean enableExperimental, ShellCallback sc) throws Exception { + + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + + if (in.format != null) { + cmd.add(Argument.FORMAT); + cmd.add(in.format); + } + + if (in.videoCodec != null) { + cmd.add(Argument.VIDEOCODEC); + cmd.add(in.videoCodec); + } + + if (in.audioCodec != null) { + cmd.add(Argument.AUDIOCODEC); + cmd.add(in.audioCodec); + } + + cmd.add("-i"); + cmd.add(new File(in.path).getCanonicalPath()); + + if (out.videoBitrate > 0) { + cmd.add(Argument.BITRATE_VIDEO); + cmd.add(out.videoBitrate + "k"); + } + + if (out.width > 0) { + cmd.add(Argument.SIZE); + cmd.add(out.width + "x" + out.height); + + } + if (out.videoFps != null) { + cmd.add(Argument.FRAMERATE); + cmd.add(out.videoFps); + } + + if (out.videoCodec != null) { + cmd.add(Argument.VIDEOCODEC); + cmd.add(out.videoCodec); + } + + if (out.videoBitStreamFilter != null) { + cmd.add(Argument.VIDEOBITSTREAMFILTER); + cmd.add(out.videoBitStreamFilter); + } + + + if (out.videoFilter != null) { + cmd.add("-vf"); + cmd.add(out.videoFilter); + } + + if (out.audioCodec != null) { + cmd.add(Argument.AUDIOCODEC); + cmd.add(out.audioCodec); + } + + if (out.audioBitStreamFilter != null) { + cmd.add(Argument.AUDIOBITSTREAMFILTER); + cmd.add(out.audioBitStreamFilter); + } + if (out.audioChannels > 0) { + cmd.add(Argument.CHANNELS_AUDIO); + cmd.add(out.audioChannels + ""); + } + + if (out.audioBitrate > 0) { + cmd.add(Argument.BITRATE_AUDIO); + cmd.add(out.audioBitrate + "k"); + } + + if (out.format != null) { + cmd.add("-f"); + cmd.add(out.format); + } + + if (out.duration > 0) { + cmd.add(Argument.DURATION); + cmd.add(String.valueOf(out.duration)); + } + + if (enableExperimental) { + cmd.add("-strict"); + cmd.add("-2");//experimental + } + + cmd.add(new File(out.path).getCanonicalPath()); + + execFFMPEG(cmd, sc); + + } + + + public Clip createSlideshowFromImagesAndAudio(ArrayList images, Clip audio, Clip out, int durationPerSlide, ShellCallback sc) throws Exception { + + final String imageBasePath = new File(mFileTemp, "image-").getCanonicalPath(); + final String imageBaseVariablePath = imageBasePath + "%03d.jpg"; + + + ArrayList cmd = new ArrayList(); + + + String newImagePath = null; + int imageCounter = 0; + + Clip imageCover = images.get(0); //add the first image twice + + cmd = new ArrayList(); + cmd.add(mFfmpegBin); + cmd.add("-y"); + + cmd.add("-i"); + cmd.add(new File(imageCover.path).getCanonicalPath()); + + if (out.width != -1 && out.height != -1) { + cmd.add("-s"); + cmd.add(out.width + "x" + out.height); + } + + newImagePath = imageBasePath + String.format(Locale.US, "%03d", imageCounter++) + ".jpg"; + cmd.add(newImagePath); + + execFFMPEG(cmd, sc); + + for (Clip image : images) { + cmd = new ArrayList(); + cmd.add(mFfmpegBin); + cmd.add("-y"); + + cmd.add("-i"); + cmd.add(new File(image.path).getCanonicalPath()); + + if (out.width != -1 && out.height != -1) { + cmd.add("-s"); + cmd.add(out.width + "x" + out.height); + } + + newImagePath = imageBasePath + String.format(Locale.US, "%03d", imageCounter++) + ".jpg"; + cmd.add(newImagePath); + + execFFMPEG(cmd, sc); + + + } + + //then combine them + cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + + cmd.add("-loop"); + cmd.add("0"); + + cmd.add("-f"); + cmd.add("image2"); + + cmd.add("-r"); + cmd.add("1/" + durationPerSlide); + + cmd.add("-i"); + cmd.add(imageBaseVariablePath); + + cmd.add("-strict"); + cmd.add("-2");//experimental + + String fileTempMpg = new File(mFileTemp, "tmp.mpg").getCanonicalPath(); + + cmd.add(fileTempMpg); + + execFFMPEG(cmd, sc); + + //now combine and encode + cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + + cmd.add("-i"); + cmd.add(fileTempMpg); + + if (audio != null && audio.path != null) { + cmd.add("-i"); + cmd.add(new File(audio.path).getCanonicalPath()); + + cmd.add("-map"); + cmd.add("0:0"); + + cmd.add("-map"); + cmd.add("1:0"); + + cmd.add(Argument.AUDIOCODEC); + cmd.add("aac"); + + cmd.add(Argument.BITRATE_AUDIO); + cmd.add("128k"); + + } + + cmd.add("-strict"); + cmd.add("-2");//experimental + + cmd.add(Argument.VIDEOCODEC); + + + if (out.videoCodec != null) + cmd.add(out.videoCodec); + else + cmd.add("mpeg4"); + + if (out.videoBitrate != -1) { + cmd.add(Argument.BITRATE_VIDEO); + cmd.add(out.videoBitrate + "k"); + } + + cmd.add(new File(out.path).getCanonicalPath()); + + + execFFMPEG(cmd, sc); + + return out; + } + + public Clip removeVideoAudio(Clip videoIn, Clip out, ShellCallback sc) throws Exception { + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + + cmd.add("-i"); + cmd.add(new File(videoIn.path).getCanonicalPath()); + + cmd.add(Argument.AUDIOCODEC); + if (out.audioCodec != null) + cmd.add(out.audioCodec); + else { + cmd.add("copy"); + + } + + cmd.add(Argument.NO_AUDIO); + if (out.videoBitrate != -1) { + cmd.add(Argument.BITRATE_VIDEO); + cmd.add(out.videoBitrate + "k"); + } + + if (out.videoFps != null) { + cmd.add(Argument.FRAMERATE); + cmd.add(out.videoFps); + } + + if (out.audioBitrate != -1) { + cmd.add(Argument.BITRATE_AUDIO); + cmd.add(out.audioBitrate + "k"); + } + cmd.add("-y"); + + cmd.add("-cutoff"); + cmd.add("15000"); + + if (out.width > 0) { + cmd.add(Argument.SIZE); + cmd.add(out.width + "x" + out.height); + + } + + if (out.format != null) { + cmd.add("-f"); + cmd.add(out.format); + } + + File fileOut = new File(out.path); + cmd.add(fileOut.getCanonicalPath()); + + execFFMPEG(cmd, sc); + + + return out; + + } + + + /* + * ffmpeg -y -loop 0 -f image2 -r 0.5 -i image-%03d.jpg -s:v 1280x720 -b:v 1M \ + -i soundtrack.mp3 -t 01:05:00 -map 0:0 -map 1:0 out.avi + + -loop_input – loops the images. Disable this if you want to stop the encoding when all images are used or the soundtrack is finished. + +-r 0.5 – sets the framerate to 0.5, which means that each image will be shown for 2 seconds. Just take the inverse, for example if you want each image to last for 3 seconds, set it to 0.33. + +-i image-%03d.jpg – use these input files. %03d means that there will be three digit numbers for the images. + +-s 1280x720 – sets the output frame size. + +-b 1M – sets the bitrate. You want 500MB for one hour, which equals to 4000MBit in 3600 seconds, thus a bitrate of approximately 1MBit/s should be sufficient. + +-i soundtrack.mp3 – use this soundtrack file. Can be any format. + +-t 01:05:00 – set the output length in hh:mm:ss format. + +out.avi – create this output file. Change it as you like, for example using another container like MP4. + */ + public Clip combineAudioAndVideo(Clip videoIn, Clip audioIn, Clip out, ShellCallback sc) throws Exception { + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + + cmd.add("-i"); + cmd.add(new File(videoIn.path).getCanonicalPath()); + + cmd.add("-i"); + cmd.add(new File(audioIn.path).getCanonicalPath()); + + + cmd.add("-strict"); + cmd.add("-2");//experimental + + cmd.add(Argument.AUDIOCODEC); + if (out.audioCodec != null) + cmd.add(out.audioCodec); + else { + cmd.add("copy"); + + } + + cmd.add(Argument.VIDEOCODEC); + if (out.videoCodec != null) + cmd.add(out.videoCodec); + else { + cmd.add("copy"); + } + + if (out.videoBitrate != -1) { + cmd.add(Argument.BITRATE_VIDEO); + cmd.add(out.videoBitrate + "k"); + } + + if (out.videoFps != null) { + cmd.add(Argument.FRAMERATE); + cmd.add(out.videoFps); + } + + if (out.audioBitrate != -1) { + cmd.add(Argument.BITRATE_AUDIO); + cmd.add(out.audioBitrate + "k"); + } + cmd.add("-y"); + + cmd.add("-cutoff"); + cmd.add("15000"); + + if (out.width > 0) { + cmd.add(Argument.SIZE); + cmd.add(out.width + "x" + out.height); + + } + + if (out.format != null) { + cmd.add("-f"); + cmd.add(out.format); + } + + File fileOut = new File(out.path); + cmd.add(fileOut.getCanonicalPath()); + + execFFMPEG(cmd, sc); + + + return out; + + } + + public Clip mergMp4(String configPath, String outPath, ShellCallback sc) throws Exception { + Clip result = new Clip(); + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + +// cmd.add("-loop"); +// cmd.add("1"); + + cmd.add("-f"); + + cmd.add("concat"); + + cmd.add("-i"); + cmd.add(new File(configPath).getCanonicalPath()); + + cmd.add("-c"); + cmd.add("copy"); + + + result.path = outPath; + result.mimeType = "video/mp4"; + + cmd.add(new File(result.path).getCanonicalPath()); + + execFFMPEG(cmd, sc); + + return result; + } + + public Clip filterMp4Complex(Clip mediaIn, Clip pngIn, String outPath, ShellCallback sc) throws Exception { + Clip result = new Clip(); + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + +// cmd.add("-loop"); +// cmd.add("1"); + + cmd.add("-i"); + cmd.add(new File(mediaIn.path).getCanonicalPath()); + + cmd.add("-i"); + cmd.add(new File(pngIn.path).getCanonicalPath()); + + cmd.add("-filter_complex"); + String filter = "geq=lum='if(lte(T,0.6), 255*T*(1/0.6),255)',format=gray[grad];" + + "[0:v]boxblur=8[blur];" + + "[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];" + + "[lay]drawtext=fontfile=/system/fonts/DroidSans.ttf:text=ABCDEF:fontsize=23:fontcolor=white@1.0:x=main_w/2:y=main_h/2[text];" + + "[text][grad]alphamerge[alpha];" + + "[0:v][alpha]overlay"; + cmd.add(filter); + + result.path = outPath; + result.videoBitrate = mediaIn.videoBitrate; + result.videoFps = mediaIn.videoFps; + result.mimeType = "video/mp4"; + + cmd.add(new File(result.path).getCanonicalPath()); + + execFFMPEG(cmd, sc); + + return result; + } + + public Clip convertImageToMP4(Clip mediaIn, Clip mediaLogo, int duration, String outPath, ShellCallback sc) throws Exception { + Clip result = new Clip(); + ArrayList cmd = new ArrayList(); + + // ffmpeg -loop 1 -i IMG_1338.jpg -t 10 -r 29.97 -s 640x480 -qscale 5 test.mp4 + + cmd = new ArrayList(); + + //convert images to MP4 + cmd.add(mFfmpegBin); + cmd.add("-y"); + + cmd.add("-loop"); + cmd.add("1"); + + cmd.add("-i"); + cmd.add(new File(mediaIn.path).getCanonicalPath()); + + cmd.add("-i"); + cmd.add(new File(mediaLogo.path).getCanonicalPath()); + + cmd.add(Argument.FRAMERATE); + cmd.add("24"); + + cmd.add(Argument.DURATION); + cmd.add(String.valueOf(duration)); + + cmd.add("-preset"); + cmd.add("medium"); + + cmd.add("-c:v"); + cmd.add("libx264"); + + cmd.add("-pix_fmt"); + cmd.add("yuv420p"); + + cmd.add("-map"); + cmd.add("0:0"); + + cmd.add("-filter_complex"); + String filter = "geq=lum='if(lte(T,0.6), 255*T*(1/0.6),255)',format=gray[grad];" + + "[0:v]boxblur=8[blur];" + + "[blur]setpts=PTS-STARTPTS[setpts]" + + "[setpts][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];" + + "[lay]drawtext=fontfile=/system/fonts/DroidSans.ttf:text=ABCDEF:fontsize=23:fontcolor=white@1.0:x=main_w/2:y=main_h/2[text];" + + "[text][grad]alphamerge[alpha];" + + "[0:v][alpha]overlay"; + cmd.add(filter); + + if (mediaIn.width != -1) { + cmd.add(Argument.SIZE); + cmd.add(mediaIn.width + "x" + mediaIn.height); + // cmd.add("-vf"); + // cmd.add("\"scale=-1:" + mediaIn.width + "\""); + } + + if (mediaIn.videoBitrate != -1) { + cmd.add(Argument.BITRATE_VIDEO); + cmd.add(mediaIn.videoBitrate + "k"); + } + + cmd.add("-profile:v"); + cmd.add("high"); + + cmd.add("-g"); + cmd.add("12"); + + +// cmd.add("-map 0:0 -map 1:0"); + // -ar 44100 -acodec pcm_s16le -f s16le -ac 2 -i /dev/zero -acodec aac -ab 128k \ + // -map 0:0 -map 1:0 + + result.path = outPath; + result.videoBitrate = mediaIn.videoBitrate; + result.videoFps = mediaIn.videoFps; + result.mimeType = "video/mp4"; + + cmd.add(new File(result.path).getCanonicalPath()); + + execFFMPEG(cmd, sc); + + return result; + } + + public Clip makeLastFrameFilter(float startTime, Clip mediaIn, Clip mediaLogo, int duration, String outPath, ShellCallback sc) throws Exception { + Clip result = new Clip(); + ArrayList cmd = new ArrayList(); + + // ffmpeg -loop 1 -i IMG_1338.jpg -t 10 -r 29.97 -s 640x480 -qscale 5 test.mp4 + + cmd = new ArrayList(); + + //convert images to MP4 + cmd.add(mFfmpegBin); + cmd.add("-y"); +// +// cmd.add("-loop"); +// cmd.add("1"); + + cmd.add(Argument.STARTTIME); + cmd.add(String.valueOf(startTime)); + + cmd.add(Argument.DURATION); + cmd.add(String.valueOf(duration)); + + cmd.add("-i"); + cmd.add(new File(mediaIn.path).getCanonicalPath()); + + cmd.add("-i"); + cmd.add(new File(mediaLogo.path).getCanonicalPath()); + + cmd.add(Argument.FRAMERATE); + cmd.add("24"); + + cmd.add("-preset"); + cmd.add("medium"); + + +// cmd.add(Argument.AUDIOCODEC); +// if (mediaIn.audioCodec != null) +// cmd.add(mediaIn.audioCodec); +// else { +// cmd.add("copy"); +// +// } +// +// cmd.add(Argument.VIDEOCODEC); +// if (mediaIn.videoCodec != null) +// cmd.add(mediaIn.videoCodec); +// else { +// cmd.add("copy"); +// } +// +// cmd.add("-t"); +// cmd.add(duration + ""); + +// cmd.add("-qscale"); +// cmd.add("5"); //a good value 1 is best 30 is worst + + cmd.add("-c:v"); + cmd.add("libx264"); + + cmd.add("-pix_fmt"); + cmd.add("yuv420p"); + + cmd.add("-map"); + cmd.add("0:0"); + + cmd.add("-filter_complex"); + String filter = "geq=lum='if(lte(T,0.6), 255*T*(1/0.6),255)',format=gray[grad];" + + "[0:v]boxblur=8[blur];" + + "[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];" + + "[lay]drawtext=fontfile=/system/fonts/DroidSans.ttf:text=猪八戒香肠嘴™配音作品:fontsize=23:fontcolor=white@1.0:x=main_w/2:y=main_h/2[text];" + + "[text][grad]alphamerge[alpha];" + + "[0:v][alpha]overlay"; + cmd.add(filter); + + if (mediaIn.width != -1) { + cmd.add(Argument.SIZE); + cmd.add(mediaIn.width + "x" + mediaIn.height); + // cmd.add("-vf"); + // cmd.add("\"scale=-1:" + mediaIn.width + "\""); + } + + if (mediaIn.videoBitrate != -1) { + cmd.add(Argument.BITRATE_VIDEO); + cmd.add(mediaIn.videoBitrate + "k"); + } + + cmd.add("-profile:v"); + cmd.add("high"); + + cmd.add("-g"); + cmd.add("12"); + + +// cmd.add("-map 0:0 -map 1:0"); + // -ar 44100 -acodec pcm_s16le -f s16le -ac 2 -i /dev/zero -acodec aac -ab 128k \ + // -map 0:0 -map 1:0 + + result.path = outPath; + result.videoBitrate = mediaIn.videoBitrate; + result.videoFps = mediaIn.videoFps; + result.mimeType = "video/mp4"; + + cmd.add(new File(result.path).getCanonicalPath()); + + execFFMPEG(cmd, sc); + + return result; + } + + + public Clip makeLastFrameFilter2(String lastFrame, float startTime, Clip mediaIn, Clip mediaLogo, int duration, String outPath, ShellCallback sc) throws Exception { + Clip result = new Clip(); + ArrayList cmd = new ArrayList(); + + // ffmpeg -loop 1 -i IMG_1338.jpg -t 10 -r 29.97 -s 640x480 -qscale 5 test.mp4 + + cmd = new ArrayList(); + + //convert images to MP4 + cmd.add(mFfmpegBin); + cmd.add("-y"); +// +// cmd.add("-loop"); +// cmd.add("1"); + + cmd.add(Argument.STARTTIME); + cmd.add(String.valueOf(startTime)); + + cmd.add(Argument.DURATION); + cmd.add(String.valueOf(duration)); + + cmd.add("-i"); + cmd.add(new File(mediaIn.path).getCanonicalPath()); + + cmd.add("-i"); + cmd.add(new File(mediaLogo.path).getCanonicalPath()); + + cmd.add("-i"); + cmd.add(new File(lastFrame).getCanonicalPath()); + + cmd.add(Argument.FRAMERATE); + cmd.add("24"); + + cmd.add("-preset"); + cmd.add("medium"); + + +// cmd.add(Argument.AUDIOCODEC); +// if (mediaIn.audioCodec != null) +// cmd.add(mediaIn.audioCodec); +// else { +// cmd.add("copy"); +// +// } +// +// cmd.add(Argument.VIDEOCODEC); +// if (mediaIn.videoCodec != null) +// cmd.add(mediaIn.videoCodec); +// else { +// cmd.add("copy"); +// } +// +// cmd.add("-t"); +// cmd.add(duration + ""); + +// cmd.add("-qscale"); +// cmd.add("5"); //a good value 1 is best 30 is worst + + cmd.add("-c:v"); + cmd.add("libx264"); + + cmd.add("-pix_fmt"); + cmd.add("yuv420p"); + + cmd.add("-map"); + cmd.add("0:0"); + + cmd.add("-filter_complex"); +// String filter = "geq=lum='if(lte(T,0.6), 255*T*(1/0.6),255)',format=gray[grad];" +//// + "[3:v]select='eq(n,LAST_FRAME_INDEX)',setpts=PTS-STARTPTS[lastframe];" +// + "[0:v][2:v]overlay=0:0 [a];" +// + "[a]boxblur=8[blur];" +// + "[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];" +// + "[lay]drawtext=fontfile=/system/fonts/DroidSans.ttf:text=猪八戒香肠嘴™配音作品:fontsize=23:fontcolor=white@1.0:x=main_w/2:y=main_h/2[text];" +// + "[text][grad]alphamerge[alpha];" +// + "[a][alpha]overlay"; + String filter = "geq=lum='if(lte(T,0.6), 255*T*(1/0.6),255)',format=gray[grad];" + + "[0:v][2:v]overlay=0:0 [lay1];" + + "[lay1]boxblur=8[blur];" + + "[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];" + + "[lay]drawtext=fontfile=/system/fonts/DroidSans.ttf:text=猪八戒香肠嘴™配音作品:fontsize=23:fontcolor=white@1.0:x=main_w/2:y=main_h/2[text];" + + "[text][grad]alphamerge[alpha];" + + "[0:v][2:v]overlay=0:0[lay1];" + + "[lay1][alpha]overlay"; + + cmd.add(filter); + + if (mediaIn.width != -1) { + cmd.add(Argument.SIZE); + cmd.add(mediaIn.width + "x" + mediaIn.height); + // cmd.add("-vf"); + // cmd.add("\"scale=-1:" + mediaIn.width + "\""); + } + + if (mediaIn.videoBitrate != -1) { + cmd.add(Argument.BITRATE_VIDEO); + cmd.add(mediaIn.videoBitrate + "k"); + } + + cmd.add("-profile:v"); + cmd.add("high"); + + cmd.add("-g"); + cmd.add("12"); + + +// cmd.add("-map 0:0 -map 1:0"); + // -ar 44100 -acodec pcm_s16le -f s16le -ac 2 -i /dev/zero -acodec aac -ab 128k \ + // -map 0:0 -map 1:0 + + result.path = outPath; + result.videoBitrate = mediaIn.videoBitrate; + result.videoFps = mediaIn.videoFps; + result.mimeType = "video/mp4"; + + cmd.add(new File(result.path).getCanonicalPath()); + + execFFMPEG(cmd, sc); + + return result; + } + //based on this gist: https://gist.github.com/3757344 + //ffmpeg -i input1.mp4 -vcodec copy -vbsf h264_mp4toannexb -acodec copy part1.ts + //ffmpeg -i input2.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts intermediate2.ts + + public Clip convertToMP4Stream(Clip mediaIn, String startTime, double duration, String outPath, ShellCallback sc) throws Exception { + ArrayList cmd = new ArrayList(); + + Clip mediaOut = new Clip(); + mediaOut.path = outPath; + + String mediaPath = new File(mediaIn.path).getCanonicalPath(); + + cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + + if (startTime != null) { + cmd.add(Argument.STARTTIME); + cmd.add(startTime); + } + + if (duration != -1) { + cmd.add(Argument.DURATION); + + double dValue = mediaIn.duration; + int hours = (int) (dValue / 3600f); + dValue -= (hours * 3600); + + cmd.add("0"); + cmd.add(String.format(Locale.US, "%s", hours)); + cmd.add(":"); + + int min = (int) (dValue / 60f); + dValue -= (min * 60); + + cmd.add("0"); + cmd.add(String.format(Locale.US, "%s", min)); + cmd.add(":"); + + cmd.add(String.format(Locale.US, "%f", dValue)); + + //cmd.add("00:00:" + String.format(Locale.US,"%f",mediaIn.duration)); + + + } + + cmd.add("-i"); + cmd.add(mediaPath); + + cmd.add("-f"); + cmd.add("mpegts"); + + cmd.add("-c"); + cmd.add("copy"); + + cmd.add("-an"); + + //cmd.add(Argument.VIDEOBITSTREAMFILTER); + cmd.add("-bsf:v"); + cmd.add("h264_mp4toannexb"); + + File fileOut = new File(mediaOut.path); + mediaOut.path = fileOut.getCanonicalPath(); + + cmd.add(mediaOut.path); + + execFFMPEG(cmd, sc); + + return mediaOut; + } + + + public Clip convertToWaveAudio(Clip mediaIn, String outPath, int sampleRate, int channels, ShellCallback sc) throws Exception { + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + + if (mediaIn.startTime != null) { + cmd.add("-ss"); + cmd.add(mediaIn.startTime); + } + + if (mediaIn.duration != -1) { + cmd.add("-t"); + cmd.add(String.format(Locale.US, "%f", mediaIn.duration)); + } + + cmd.add("-i"); + cmd.add(new File(mediaIn.path).getCanonicalPath()); + + + cmd.add("-ar"); + cmd.add(sampleRate + ""); + + cmd.add("-ac"); + cmd.add(channels + ""); + + cmd.add("-vn"); + + Clip mediaOut = new Clip(); + + File fileOut = new File(outPath); + mediaOut.path = fileOut.getCanonicalPath(); + + cmd.add(mediaOut.path); + + execFFMPEG(cmd, sc); + + return mediaOut; + } + + public Clip convertTo3GPAudio(Clip mediaIn, Clip mediaOut, ShellCallback sc) throws Exception { + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + cmd.add("-i"); + cmd.add(new File(mediaIn.path).getCanonicalPath()); + + if (mediaIn.startTime != null) { + cmd.add("-ss"); + cmd.add(mediaIn.startTime); + } + + if (mediaIn.duration != -1) { + cmd.add("-t"); + cmd.add(String.format(Locale.US, "%f", mediaIn.duration)); + + } + + cmd.add("-vn"); + + if (mediaOut.audioCodec != null) { + cmd.add("-acodec"); + cmd.add(mediaOut.audioCodec); + } + + if (mediaOut.audioBitrate != -1) { + cmd.add("-ab"); + cmd.add(mediaOut.audioBitrate + "k"); + } + + cmd.add("-strict"); + cmd.add("-2"); + + File fileOut = new File(mediaOut.path); + + cmd.add(fileOut.getCanonicalPath()); + + execFFMPEG(cmd, sc); + + return mediaOut; + } + + public Clip convert(Clip mediaIn, String outPath, ShellCallback sc) throws Exception { + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + cmd.add("-i"); + cmd.add(new File(mediaIn.path).getCanonicalPath()); + + if (mediaIn.startTime != null) { + cmd.add("-ss"); + cmd.add(mediaIn.startTime); + } + + if (mediaIn.duration != -1) { + cmd.add("-t"); + cmd.add(String.format(Locale.US, "%f", mediaIn.duration)); + + } + + + Clip mediaOut = new Clip(); + + + File fileOut = new File(outPath); + + mediaOut.path = fileOut.getCanonicalPath(); + + cmd.add(mediaOut.path); + + execFFMPEG(cmd, sc); + + return mediaOut; + } + + public Clip convertToMPEG(Clip mediaIn, String outPath, ShellCallback sc) throws Exception { + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + cmd.add("-i"); + cmd.add(new File(mediaIn.path).getCanonicalPath()); + + if (mediaIn.startTime != null) { + cmd.add("-ss"); + cmd.add(mediaIn.startTime); + } + + if (mediaIn.duration != -1) { + cmd.add("-t"); + cmd.add(String.format(Locale.US, "%f", mediaIn.duration)); + + } + + + //cmd.add("-strict"); + //cmd.add("experimental"); + + //everything to mpeg + cmd.add("-f"); + cmd.add("mpeg"); + + Clip mediaOut = mediaIn.clone(); + + File fileOut = new File(outPath); + + mediaOut.path = fileOut.getCanonicalPath(); + + cmd.add(mediaOut.path); + + execFFMPEG(cmd, sc); + + return mediaOut; + } + + public void concatAndTrimFilesMPEG(ArrayList videos, Clip out, boolean preConvert, ShellCallback sc) throws Exception { + + int idx = 0; + + if (preConvert) { + for (Clip mdesc : videos) { + if (mdesc.path == null) + continue; + + //extract MPG video + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + cmd.add("-i"); + cmd.add(mdesc.path); + + if (mdesc.startTime != null) { + cmd.add("-ss"); + cmd.add(mdesc.startTime); + } + + if (mdesc.duration != -1) { + cmd.add("-t"); + cmd.add(String.format(Locale.US, "%f", mdesc.duration)); + + } + + /* + cmd.add ("-acodec"); + cmd.add("pcm_s16le"); + + cmd.add ("-vcodec"); + cmd.add("mpeg2video"); + */ + + if (out.audioCodec == null) + cmd.add("-an"); //no audio + + //cmd.add("-strict"); + //cmd.add("experimental"); + + //everything to mpeg + cmd.add("-f"); + cmd.add("mpeg"); + cmd.add(out.path + '.' + idx + ".mpg"); + + execFFMPEG(cmd, sc); + + idx++; + } + } + + StringBuffer cmdRun = new StringBuffer(); + + cmdRun.append(mCmdCat); + + idx = 0; + + for (Clip vdesc : videos) { + if (vdesc.path == null) + continue; + + if (preConvert) + cmdRun.append(out.path).append('.').append(idx++).append(".mpg").append(' '); //leave a space at the end! + else + cmdRun.append(vdesc.path).append(' '); + } + + String mCatPath = out.path + ".full.mpg"; + + cmdRun.append("> "); + cmdRun.append(mCatPath); + + String[] cmds = {"sh", "-c", cmdRun.toString()}; + Runtime.getRuntime().exec(cmds).waitFor(); + + + Clip mInCat = new Clip(); + mInCat.path = mCatPath; + + processVideo(mInCat, out, false, sc); + + out.path = mCatPath; + } + + public void extractAudio(Clip mdesc, String audioFormat, File audioOutPath, ShellCallback sc) throws IOException, InterruptedException { + + //no just extract the audio + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + cmd.add("-i"); + cmd.add(new File(mdesc.path).getCanonicalPath()); + + cmd.add("-vn"); + + if (mdesc.startTime != null) { + cmd.add("-ss"); + cmd.add(mdesc.startTime); + } + + if (mdesc.duration != -1) { + cmd.add("-t"); + cmd.add(String.format(Locale.US, "%f", mdesc.duration)); + + } + + cmd.add("-f"); + cmd.add(audioFormat); //wav + + //everything to WAV! + cmd.add(audioOutPath.getCanonicalPath()); + + execFFMPEG(cmd, sc); + + } + + private class FileMover { + + InputStream inputStream; + File destination; + + public FileMover(InputStream _inputStream, File _destination) { + inputStream = _inputStream; + destination = _destination; + } + + public void moveIt() throws IOException { + + OutputStream destinationOut = new BufferedOutputStream(new FileOutputStream(destination)); + + int numRead; + byte[] buf = new byte[1024]; + while ((numRead = inputStream.read(buf)) >= 0) { + destinationOut.write(buf, 0, numRead); + } + + destinationOut.flush(); + destinationOut.close(); + } + } + + public int killVideoProcessor(boolean asRoot, boolean waitFor) throws IOException { + int killDelayMs = 300; + + int result = -1; + + int procId = -1; + + while ((procId = ShellUtils.findProcessId(mFfmpegBin)) != -1) { + + // Log.d(TAG, "Found PID=" + procId + " - killing now..."); + + String[] cmd = {ShellUtils.SHELL_CMD_KILL + ' ' + procId + ""}; + + try { + result = ShellUtils.doShellCommand(cmd, new ShellCallback() { + + @Override + public void shellOut(String msg) { + + } + + @Override + public void processComplete(int exitValue) { + + + } + + }, asRoot, waitFor); + Thread.sleep(killDelayMs); + } catch (Exception e) { + } + } + + return result; + } + + + public Clip trim(Clip mediaIn, boolean withSound, String outPath, ShellCallback sc) throws Exception { + ArrayList cmd = new ArrayList(); + + Clip mediaOut = new Clip(); + + String mediaPath = mediaIn.path; + + cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + + if (mediaIn.startTime != null) { + cmd.add(Argument.STARTTIME); + cmd.add(mediaIn.startTime); + } + + if (mediaIn.duration != -1) { + cmd.add("-t"); + cmd.add(String.format(Locale.US, "%f", mediaIn.duration)); + + } + + cmd.add("-i"); + cmd.add(mediaPath); + + if (!withSound) + cmd.add("-an"); + + cmd.add("-strict"); + cmd.add("-2");//experimental + + mediaOut.path = outPath; + + cmd.add(mediaOut.path); + + execFFMPEG(cmd, sc); + + return mediaOut; + } + + public void concatAndTrimFilesMP4Stream(ArrayList videos, Clip out, boolean preconvertClipsToMP4, boolean useCatCmd, ShellCallback sc) throws Exception { + + + File fileExportOut = new File(out.path); + + StringBuffer sbCat = new StringBuffer(); + + int tmpIdx = 0; + + + for (Clip vdesc : videos) { + + Clip mdOut = null; + + if (preconvertClipsToMP4) { + File fileOut = new File(mFileTemp, tmpIdx + "-trim.mp4"); + if (fileOut.exists()) + fileOut.delete(); + + boolean withSound = false; + + mdOut = trim(vdesc, withSound, fileOut.getCanonicalPath(), sc); + + fileOut = new File(mFileTemp, tmpIdx + ".ts"); + if (fileOut.exists()) + fileOut.delete(); + + mdOut = convertToMP4Stream(mdOut, null, -1, fileOut.getCanonicalPath(), sc); + } else { + File fileOut = new File(mFileTemp, tmpIdx + ".ts"); + if (fileOut.exists()) + fileOut.delete(); + mdOut = convertToMP4Stream(vdesc, vdesc.startTime, vdesc.duration, fileOut.getCanonicalPath(), sc); + } + + if (mdOut != null) { + if (sbCat.length() > 0) + sbCat.append("|"); + + sbCat.append(new File(mdOut.path).getCanonicalPath()); + tmpIdx++; + } + } + + File fileExportOutTs = new File(fileExportOut.getCanonicalPath() + ".ts"); + + if (useCatCmd) { + + //cat 0.ts 1.ts > foo.ts + StringBuffer cmdBuff = new StringBuffer(); + + cmdBuff.append(mCmdCat); + cmdBuff.append(" "); + + StringTokenizer st = new StringTokenizer(sbCat.toString(), "|"); + + while (st.hasMoreTokens()) + cmdBuff.append(st.nextToken()).append(" "); + + cmdBuff.append("> "); + + cmdBuff.append(fileExportOut.getCanonicalPath() + ".ts"); + + Runtime.getRuntime().exec(cmdBuff.toString()); + + ArrayList cmd = new ArrayList(); + + cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + cmd.add("-i"); + + cmd.add(fileExportOut.getCanonicalPath() + ".ts"); + + cmd.add("-c"); + cmd.add("copy"); + + cmd.add("-an"); + + cmd.add(fileExportOut.getCanonicalPath()); + + execFFMPEG(cmd, sc, null); + + + } else { + + //ffmpeg -i "concat:intermediate1.ts|intermediate2.ts" -c copy -bsf:a aac_adtstoasc output.mp4 + ArrayList cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + cmd.add("-i"); + cmd.add("concat:" + sbCat.toString()); + + cmd.add("-c"); + cmd.add("copy"); + + cmd.add("-an"); + + cmd.add(fileExportOut.getCanonicalPath()); + + execFFMPEG(cmd, sc); + + } + + if ((!fileExportOut.exists()) || fileExportOut.length() == 0) { + throw new Exception("There was a problem rendering the video: " + fileExportOut.getCanonicalPath()); + } + + + } + + public Clip getInfo(Clip in) throws IOException, InterruptedException { + ArrayList cmd = new ArrayList(); + + cmd = new ArrayList(); + + cmd.add(mFfmpegBin); + cmd.add("-y"); + cmd.add("-i"); + + cmd.add(new File(in.path).getCanonicalPath()); + + InfoParser ip = new InfoParser(in); + execFFMPEG(cmd, ip, null); + + try { + Thread.sleep(200); + } catch (Exception e) { + } + + + return in; + + } + + private class InfoParser implements ShellCallback { + + private Clip mMedia; + private int retValue; + + public InfoParser(Clip media) { + mMedia = media; + } + + @Override + public void shellOut(String shellLine) { + if (shellLine.contains("Duration:")) { + +// Duration: 00:01:01.75, start: 0.000000, bitrate: 8184 kb/s + + String[] timecode = shellLine.split(",")[0].split(":"); + + + double duration = 0; + + duration = Double.parseDouble(timecode[1].trim()) * 60 * 60; //hours + duration += Double.parseDouble(timecode[2].trim()) * 60; //minutes + duration += Double.parseDouble(timecode[3].trim()); //seconds + + mMedia.duration = duration; + + + } + + // Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080, 16939 kb/s, 30.02 fps, 30 tbr, 90k tbn, 180k tbc + else if (shellLine.contains(": Video:")) { + String[] line = shellLine.split(":"); + String[] videoInfo = line[3].split(","); + + mMedia.videoCodec = videoInfo[0]; + } + + //Stream #0:1(eng): Audio: aac (mp4a / 0x6134706D), 48000 Hz, stereo, s16, 121 kb/s + else if (shellLine.contains(": Audio:")) { + String[] line = shellLine.split(":"); + String[] audioInfo = line[3].split(","); + + mMedia.audioCodec = audioInfo[0]; + + } + + + // + //Stream #0.0(und): Video: h264 (Baseline), yuv420p, 1280x720, 8052 kb/s, 29.97 fps, 90k tbr, 90k tbn, 180k tbc + //Stream #0.1(und): Audio: mp2, 22050 Hz, 2 channels, s16, 127 kb/s + + } + + @Override + public void processComplete(int exitValue) { + retValue = exitValue; + + } + } + + private class StreamGobbler extends Thread { + InputStream is; + String type; + ShellCallback sc; + + StreamGobbler(InputStream is, String type, ShellCallback sc) { + this.is = is; + this.type = type; + this.sc = sc; + } + + public void run() { + try { + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr); + String line = null; + while ((line = br.readLine()) != null) + if (sc != null) + sc.shellOut(line); + + } catch (IOException ioe) { + // Log.e(TAG,"error reading shell slog",ioe); + ioe.printStackTrace(); + } + } + } + + public static Bitmap getVideoFrame(String videoPath, long frameTime) throws Exception { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + + try { + retriever.setDataSource(videoPath); + return retriever.getFrameAtTime(frameTime, MediaMetadataRetriever.OPTION_CLOSEST); + + } finally { + try { + retriever.release(); + } catch (RuntimeException ex) { + } + } + } +} + +/* + * Main options: +-L show license +-h show help +-? show help +-help show help +--help show help +-version show version +-formats show available formats +-codecs show available codecs +-bsfs show available bit stream filters +-protocols show available protocols +-filters show available filters +-pix_fmts show available pixel formats +-sample_fmts show available audio sample formats +-loglevel loglevel set libav* logging level +-v loglevel set libav* logging level +-debug flags set debug flags +-report generate a report +-f fmt force format +-i filename input file name +-y overwrite output files +-n do not overwrite output files +-c codec codec name +-codec codec codec name +-pre preset preset name +-t duration record or transcode "duration" seconds of audio/video +-fs limit_size set the limit file size in bytes +-ss time_off set the start time offset +-itsoffset time_off set the input ts offset +-itsscale scale set the input ts scale +-timestamp time set the recording timestamp ('now' to set the current time) +-metadata string=string add metadata +-dframes number set the number of data frames to record +-timelimit limit set max runtime in seconds +-target type specify target file type ("vcd", "svcd", "dvd", "dv", "dv50", "pal-vcd", "ntsc-svcd", ...) +-xerror exit on error +-frames number set the number of frames to record +-tag fourcc/tag force codec tag/fourcc +-filter filter_list set stream filterchain +-stats print progress report during encoding +-attach filename add an attachment to the output file +-dump_attachment filename extract an attachment into a file +-bsf bitstream_filters A comma-separated list of bitstream filters +-dcodec codec force data codec ('copy' to copy stream) + +Advanced options: +-map file.stream[:syncfile.syncstream] set input stream mapping +-map_channel file.stream.channel[:syncfile.syncstream] map an audio channel from one stream to another +-map_meta_data outfile[,metadata]:infile[,metadata] DEPRECATED set meta data information of outfile from infile +-map_metadata outfile[,metadata]:infile[,metadata] set metadata information of outfile from infile +-map_chapters input_file_index set chapters mapping +-benchmark add timings for benchmarking +-dump dump each input packet +-hex when dumping packets, also dump the payload +-re read input at native frame rate +-loop_input deprecated, use -loop +-loop_output deprecated, use -loop +-vsync video sync method +-async audio sync method +-adrift_threshold threshold audio drift threshold +-copyts copy timestamps +-copytb source copy input stream time base when stream copying +-shortest finish encoding within shortest input +-dts_delta_threshold threshold timestamp discontinuity delta threshold +-copyinkf copy initial non-keyframes +-q q use fixed quality scale (VBR) +-qscale q use fixed quality scale (VBR) +-streamid streamIndex:value set the value of an outfile streamid +-muxdelay seconds set the maximum demux-decode delay +-muxpreload seconds set the initial demux-decode delay +-fpre filename set options from indicated preset file + +Video options: +-vframes number set the number of video frames to record +-r rate set frame rate (Hz value, fraction or abbreviation) +-s size set frame size (WxH or abbreviation) +-aspect aspect set aspect ratio (4:3, 16:9 or 1.3333, 1.7777) +-bits_per_raw_sample number set the number of bits per raw sample +-croptop size Removed, use the crop filter instead +-cropbottom size Removed, use the crop filter instead +-cropleft size Removed, use the crop filter instead +-cropright size Removed, use the crop filter instead +-padtop size Removed, use the pad filter instead +-padbottom size Removed, use the pad filter instead +-padleft size Removed, use the pad filter instead +-padright size Removed, use the pad filter instead +-padcolor color Removed, use the pad filter instead +-vn disable video +-vcodec codec force video codec ('copy' to copy stream) +-sameq use same quantizer as source (implies VBR) +-same_quant use same quantizer as source (implies VBR) +-pass n select the pass number (1 or 2) +-passlogfile prefix select two pass log file name prefix +-vf filter list video filters +-b bitrate video bitrate (please use -b:v) +-dn disable data + +Advanced Video options: +-pix_fmt format set pixel format +-intra use only intra frames +-vdt n discard threshold +-rc_override override rate control override for specific intervals +-deinterlace deinterlace pictures +-psnr calculate PSNR of compressed frames +-vstats dump video coding statistics to file +-vstats_file file dump video coding statistics to file +-intra_matrix matrix specify intra matrix coeffs +-inter_matrix matrix specify inter matrix coeffs +-top top=1/bottom=0/auto=-1 field first +-dc precision intra_dc_precision +-vtag fourcc/tag force video tag/fourcc +-qphist show QP histogram +-force_fps force the selected framerate, disable the best supported framerate selection +-force_key_frames timestamps force key frames at specified timestamps +-vbsf video bitstream_filters deprecated +-vpre preset set the video options to the indicated preset + +Audio options: +-aframes number set the number of audio frames to record +-aq quality set audio quality (codec-specific) +-ar rate set audio sampling rate (in Hz) +-ac channels set number of audio channels +-an disable audio +-acodec codec force audio codec ('copy' to copy stream) +-vol volume change audio volume (256=normal) +-rmvol volume rematrix volume (as factor) + + */ + +/* + * //./ffmpeg -y -i test.mp4 -vframes 999999 -vf 'redact=blurbox.txt [out] [d], [d]nullsink' -acodec copy outputa.mp4 + + //ffmpeg -v 10 -y -i /sdcard/org.witness.sscvideoproto/videocapture1042744151.mp4 -vcodec libx264 + //-b 3000k -s 720x480 -r 30 -acodec copy -f mp4 -vf 'redact=/data/data/org.witness.sscvideoproto/redact_unsort.txt' + ///sdcard/org.witness.sscvideoproto/new.mp4 + + //"-vf" , "redact=" + Environment.getExternalStorageDirectory().getPath() + "/" + PACKAGENAME + "/redact_unsort.txt", + + + // Need to make sure this will create a legitimate mp4 file + //"-acodec", "ac3", "-ac", "1", "-ar", "16000", "-ab", "32k", + + + String[] ffmpegCommand = {"/data/data/"+PACKAGENAME+"/ffmpeg", "-v", "10", "-y", "-i", recordingFile.getPath(), + "-vcodec", "libx264", "-b", "3000k", "-vpre", "baseline", "-s", "720x480", "-r", "30", + //"-vf", "drawbox=10:20:200:60:red@0.5", + "-vf" , "\"movie="+ overlayImage.getPath() +" [logo];[in][logo] overlay=0:0 [out]\"", + "-acodec", "copy", + "-f", "mp4", savePath.getPath()+"/output.mp4"}; + + + + +//ffmpeg -i source-video.avi -s 480x320 -vcodec mpeg4 -acodec aac -ac 1 -ar 16000 -r 13 -ab 32000 -aspect 3:2 output-video.mp4/ + + + */ + + +/* concat doesn't seem to work +cmd.add("-i"); + +StringBuffer concat = new StringBuffer(); + +for (int i = 0; i < videos.size(); i++) +{ + if (i > 0) + concat.append("|"); + + concat.append(out.path + '.' + i + ".wav"); + +} + +cmd.add("concat:\"" + concat.toString() + "\""); +*/ + diff --git a/app/src/main/java/org/ffmpeg/android/ShellUtils.java b/app/src/main/java/org/ffmpeg/android/ShellUtils.java new file mode 100644 index 0000000..c2ffebd --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/ShellUtils.java @@ -0,0 +1,234 @@ +/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */ +/* See LICENSE for licensing information */ +package org.ffmpeg.android; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.StringTokenizer; + + +import android.util.Log; + +public class ShellUtils { + + //various console cmds + public final static String SHELL_CMD_CHMOD = "chmod"; + public final static String SHELL_CMD_KILL = "kill -9"; + public final static String SHELL_CMD_RM = "rm"; + public final static String SHELL_CMD_PS = "ps"; + public final static String SHELL_CMD_PIDOF = "pidof"; + + public final static String CHMOD_EXE_VALUE = "700"; + + public static boolean isRootPossible() { + + StringBuilder log = new StringBuilder(); + + try { + + // Check if Superuser.apk exists + File fileSU = new File("/system/app/Superuser.apk"); + if (fileSU.exists()) + return true; + + fileSU = new File("/system/bin/su"); + if (fileSU.exists()) + return true; + + //Check for 'su' binary + String[] cmd = {"which su"}; + int exitCode = ShellUtils.doShellCommand(null, cmd, new ShellCallback() { + + @Override + public void shellOut(String msg) { + + //System.out.print(msg); + + } + + @Override + public void processComplete(int exitValue) { + // TODO Auto-generated method stub + + } + + }, false, true).exitValue(); + + if (exitCode == 0) { + logMessage("Can acquire root permissions"); + return true; + + } + + } catch (IOException e) { + //this means that there is no root to be had (normally) so we won't log anything + logException("Error checking for root access", e); + + } catch (Exception e) { + logException("Error checking for root access", e); + //this means that there is no root to be had (normally) + } + + logMessage("Could not acquire root permissions"); + + + return false; + } + + + public static int findProcessId(String command) { + int procId = -1; + + try { + procId = findProcessIdWithPidOf(command); + + if (procId == -1) + procId = findProcessIdWithPS(command); + } catch (Exception e) { + try { + procId = findProcessIdWithPS(command); + } catch (Exception e2) { + logException("Unable to get proc id for: " + command, e2); + } + } + + return procId; + } + + //use 'pidof' command + public static int findProcessIdWithPidOf(String command) throws Exception { + + int procId = -1; + + Runtime r = Runtime.getRuntime(); + + Process procPs = null; + + String baseName = new File(command).getName(); + //fix contributed my mikos on 2010.12.10 + procPs = r.exec(new String[]{SHELL_CMD_PIDOF, baseName}); + //procPs = r.exec(SHELL_CMD_PIDOF); + + BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream())); + String line = null; + + while ((line = reader.readLine()) != null) { + + try { + //this line should just be the process id + procId = Integer.parseInt(line.trim()); + break; + } catch (NumberFormatException e) { + logException("unable to parse process pid: " + line, e); + } + } + + + return procId; + + } + + //use 'ps' command + public static int findProcessIdWithPS(String command) throws Exception { + + int procId = -1; + + Runtime r = Runtime.getRuntime(); + + Process procPs = null; + + procPs = r.exec(SHELL_CMD_PS); + + BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream())); + String line = null; + + while ((line = reader.readLine()) != null) { + if (line.indexOf(' ' + command) != -1) { + + StringTokenizer st = new StringTokenizer(line, " "); + st.nextToken(); //proc owner + + procId = Integer.parseInt(st.nextToken().trim()); + + break; + } + } + + + return procId; + + } + + public static int doShellCommand(String[] cmds, ShellCallback sc, boolean runAsRoot, boolean waitFor) throws Exception { + return doShellCommand(null, cmds, sc, runAsRoot, waitFor).exitValue(); + + } + + public static Process doShellCommand(Process proc, String[] cmds, ShellCallback sc, boolean runAsRoot, boolean waitFor) throws Exception { + + + if (proc == null) { + if (runAsRoot) + proc = Runtime.getRuntime().exec("su"); + else + proc = Runtime.getRuntime().exec("sh"); + } + + OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream()); + + for (int i = 0; i < cmds.length; i++) { + logMessage("executing shell cmd: " + cmds[i] + "; runAsRoot=" + runAsRoot + ";waitFor=" + waitFor); + + out.write(cmds[i]); + out.write("\n"); + } + + out.flush(); + out.write("exit\n"); + out.flush(); + + if (waitFor) { + + final char buf[] = new char[20]; + + // Consume the "stdout" + InputStreamReader reader = new InputStreamReader(proc.getInputStream()); + int read = 0; + while ((read = reader.read(buf)) != -1) { + if (sc != null) sc.shellOut(new String(buf)); + } + + // Consume the "stderr" + reader = new InputStreamReader(proc.getErrorStream()); + read = 0; + while ((read = reader.read(buf)) != -1) { + if (sc != null) sc.shellOut(new String(buf)); + } + + proc.waitFor(); + + } + + sc.processComplete(proc.exitValue()); + + return proc; + + } + + public static void logMessage(String msg) { + + } + + public static void logException(String msg, Exception e) { + + } + + public interface ShellCallback { + public void shellOut(String shellLine); + + public void processComplete(int exitValue); + } +} diff --git a/app/src/main/java/org/ffmpeg/android/filters/CropVideoFilter.java b/app/src/main/java/org/ffmpeg/android/filters/CropVideoFilter.java new file mode 100644 index 0000000..a64c306 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/filters/CropVideoFilter.java @@ -0,0 +1,133 @@ +package org.ffmpeg.android.filters; + +public class CropVideoFilter extends VideoFilter { + + private String mOutWidth; + private String mOutHeight; + private String mX; + private String mY; + + public CropVideoFilter(String width, String height, String x, String y) { + mOutWidth = width; + mOutHeight = height; + mX = x; + mY = y; + } + + @Override + public String getFilterString() { + + StringBuffer result = new StringBuffer(); + + result.append("crop="); + + if (mOutWidth != null) + result.append(mOutWidth).append(":"); + + if (mOutHeight != null) + result.append(mOutHeight).append(":"); + + if (mX != null) + result.append(mX).append(":"); + + if (mY != null) + result.append(mY).append(":"); + + result.deleteCharAt(result.length() - 1); //remove the last semicolon! + + return result.toString(); + } + +} + +/* +Crop the input video to out_w:out_h:x:y:keep_aspect + +The keep_aspect parameter is optional, if specified and set to a non-zero value will force the output display aspect ratio to be the same of the input, by changing the output sample aspect ratio. + +The out_w, out_h, x, y parameters are expressions containing the following constants: + +‘x, y’ +the computed values for x and y. They are evaluated for each new frame. + +‘in_w, in_h’ +the input width and height + +‘iw, ih’ +same as in_w and in_h + +‘out_w, out_h’ +the output (cropped) width and height + +‘ow, oh’ +same as out_w and out_h + +‘a’ +same as iw / ih + +‘sar’ +input sample aspect ratio + +‘dar’ +input display aspect ratio, it is the same as (iw / ih) * sar + +‘hsub, vsub’ +horizontal and vertical chroma subsample values. For example for the pixel format "yuv422p" hsub is 2 and vsub is 1. + +‘n’ +the number of input frame, starting from 0 + +‘pos’ +the position in the file of the input frame, NAN if unknown + +‘t’ +timestamp expressed in seconds, NAN if the input timestamp is unknown + +The out_w and out_h parameters specify the expressions for the width and height of the output (cropped) video. They are evaluated just at the configuration of the filter. + +The default value of out_w is "in_w", and the default value of out_h is "in_h". + +The expression for out_w may depend on the value of out_h, and the expression for out_h may depend on out_w, but they cannot depend on x and y, as x and y are evaluated after out_w and out_h. + +The x and y parameters specify the expressions for the position of the top-left corner of the output (non-cropped) area. They are evaluated for each frame. If the evaluated value is not valid, it is approximated to the nearest valid value. + +The default value of x is "(in_w-out_w)/2", and the default value for y is "(in_h-out_h)/2", which set the cropped area at the center of the input image. + +The expression for x may depend on y, and the expression for y may depend on x. + +Follow some examples: + + +# crop the central input area with size 100x100 +crop=100:100 + +# crop the central input area with size 2/3 of the input video +"crop=2/3*in_w:2/3*in_h" + +# crop the input video central square +crop=in_h + +# delimit the rectangle with the top-left corner placed at position +# 100:100 and the right-bottom corner corresponding to the right-bottom +# corner of the input image. +crop=in_w-100:in_h-100:100:100 + +# crop 10 pixels from the left and right borders, and 20 pixels from +# the top and bottom borders +"crop=in_w-2*10:in_h-2*20" + +# keep only the bottom right quarter of the input image +"crop=in_w/2:in_h/2:in_w/2:in_h/2" + +# crop height for getting Greek harmony +"crop=in_w:1/PHI*in_w" + +# trembling effect +"crop=in_w/2:in_h/2:(in_w-out_w)/2+((in_w-out_w)/2)*sin(n/10):(in_h-out_h)/2 +((in_h-out_h)/2)*sin(n/7)" + +# erratic camera effect depending on timestamp +"crop=in_w/2:in_h/2:(in_w-out_w)/2+((in_w-out_w)/2)*sin(t*10):(in_h-out_h)/2 +((in_h-out_h)/2)*sin(t*13)" + +# set x depending on the value of y +"crop=in_w/2:in_h/2:y:10+10*sin(n/10)" +*/ \ No newline at end of file diff --git a/app/src/main/java/org/ffmpeg/android/filters/DrawBoxVideoFilter.java b/app/src/main/java/org/ffmpeg/android/filters/DrawBoxVideoFilter.java new file mode 100644 index 0000000..ad81e4f --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/filters/DrawBoxVideoFilter.java @@ -0,0 +1,52 @@ +package org.ffmpeg.android.filters; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; + +public class DrawBoxVideoFilter extends OverlayVideoFilter { + + public int x; + public int y; + public int width; + public int height; + public String color; + + public DrawBoxVideoFilter(int x, int y, int width, int height, int alpha, String color, File tmpDir) throws Exception { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.color = color; + + if (alpha < 0 || alpha > 255) { + throw new IllegalArgumentException("Alpha must be an integer betweeen 0 and 255"); + } + Paint paint = new Paint(); + paint.setAlpha(alpha); + + + Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); + bitmap.eraseColor(Color.parseColor(color)); + + Bitmap temp_box = Bitmap.createBitmap(width, height, Config.ARGB_8888); + Canvas canvas = new Canvas(temp_box); + canvas.drawBitmap(bitmap, 0, 0, paint); + + File outputFile; + outputFile = File.createTempFile("box_" + width + height + color, ".png", tmpDir); + FileOutputStream os = new FileOutputStream(outputFile); + temp_box.compress(Bitmap.CompressFormat.PNG, 100, os); + overlayFile = outputFile; + xParam = Integer.toString(x); + yParam = Integer.toString(y); + + } +} diff --git a/app/src/main/java/org/ffmpeg/android/filters/DrawTextVideoFilter.java b/app/src/main/java/org/ffmpeg/android/filters/DrawTextVideoFilter.java new file mode 100644 index 0000000..147d73b --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/filters/DrawTextVideoFilter.java @@ -0,0 +1,124 @@ +package org.ffmpeg.android.filters; + +import java.io.File; + +public class DrawTextVideoFilter extends VideoFilter { + + private String mX; + private String mY; + private String mText; + private String mFontColor; + private int mFontSize; + private File mFileFont; + private int mBox; + private String mBoxColor; + + public final static String X_CENTERED = "(w-text_w)/2"; + public final static String Y_CENTERED = "(h-text_h-line_h)/2"; + + public final static String X_LEFT = "0"; + public final static String Y_BOTTOM = "(h-text_h-line_h)"; + + public DrawTextVideoFilter(String text) { + mX = X_CENTERED; + mY = Y_CENTERED; + + mText = text; + mFontColor = "white"; + mFontSize = 36; + mFileFont = new File("/system/fonts/Roboto-Regular.ttf"); + if (!mFileFont.exists()) + mFileFont = new File("/system/fonts/DroidSerif-Regular.ttf"); + + mBox = 1; + mBoxColor = "black@0.5";//0x00000000@1 + + } + + public DrawTextVideoFilter(String text, String x, String y, String fontColor, int fontSize, File fontFile, boolean showBox, String boxColor, String boxOpacity) { + mX = x; + mY = y; + + mText = text; + mFontColor = fontColor; + mFontSize = fontSize; + + mFileFont = fontFile; + + mBox = showBox ? 1 : 0; + mBoxColor = boxColor + '@' + boxOpacity; + + } + + @Override + public String getFilterString() { + + StringBuffer result = new StringBuffer(); + result.append("drawtext="); + result.append("fontfile='").append(mFileFont.getAbsolutePath()).append("':"); + result.append("text='").append(mText).append("':"); + result.append("x=").append(mX).append(":"); + result.append("y=").append(mY).append(":"); + result.append("fontcolor=").append(mFontColor).append(":"); + result.append("fontsize=").append(mFontSize).append(":"); + result.append("box=").append(mBox).append(":"); + result.append("boxcolor=").append(mBoxColor); + + return result.toString(); + } + +} + +/* + * //mdout.videoFilter = "drawtext=fontfile=/system/fonts/DroidSans.ttf: text='this is awesome':x=(w-text_w)/2:y=H-60 :fontcolor=white :box=1:boxcolor=0x00000000@1"; + + File fontFile = new File("/system/fonts/Roboto-Regular.ttf"); + if (!fontFile.exists()) + fontFile = new File("/system/fonts/DroidSans.ttf"); + + mdout.videoFilter = "drawtext=fontfile='" + fontFile.getAbsolutePath() + "':text='this is awesome':x=(main_w-text_w)/2:y=50:fontsize=24:fontcolor=white"; + */ + +/** + * /system/fonts + *

+ * AndroidClock.ttf + * AndroidClock_Highlight.ttf + * AndroidClock_Solid.ttf + * AndroidEmoji.ttf + * AnjaliNewLipi-light.ttf + * Clockopia.ttf + * DroidNaskh-Regular-SystemUI.ttf + * DroidNaskh-Regular.ttf + * DroidSans-Bold.ttf + * DroidSans.ttf + * DroidSansArmenian.ttf + * DroidSansDevanagari-Regular.ttf + * DroidSansEthiopic-Regular.ttf + * DroidSansFallback.ttf + * DroidSansGeorgian.ttf + * DroidSansHebrew-Bold.ttf + * DroidSansHebrew-Regular.ttf + * DroidSansMono.ttf + * DroidSansTamil-Bold.ttf + * DroidSansTamil-Regular.ttf + * DroidSansThai.ttf + * DroidSerif-Bold.ttf + * DroidSerif-BoldItalic.ttf + * DroidSerif-Italic.ttf + * DroidSerif-Regular.ttf + * Lohit-Bengali.ttf + * Lohit-Kannada.ttf + * Lohit-Telugu.ttf + * MTLmr3m.ttf + * Roboto-Bold.ttf + * Roboto-BoldItalic.ttf + * Roboto-Italic.ttf + * Roboto-Light.ttf + * Roboto-LightItalic.ttf + * Roboto-Regular.ttf + * RobotoCondensed-Bold.ttf + * RobotoCondensed-BoldItalic.ttf + * RobotoCondensed-Italic.ttf + * RobotoCondensed-Regular.ttf + */ \ No newline at end of file diff --git a/app/src/main/java/org/ffmpeg/android/filters/FadeVideoFilter.java b/app/src/main/java/org/ffmpeg/android/filters/FadeVideoFilter.java new file mode 100644 index 0000000..094e5b6 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/filters/FadeVideoFilter.java @@ -0,0 +1,28 @@ +package org.ffmpeg.android.filters; + +public class FadeVideoFilter extends VideoFilter { + + private String mAction; //in our out + private int mStart; + private int mLength; + + public FadeVideoFilter(String action, int start, int length) { + mAction = action; + mStart = start; + mLength = length; + } + + @Override + public String getFilterString() { + + StringBuffer result = new StringBuffer(); + result.append("fade="); + result.append(mAction).append(':').append(mStart).append(':').append(mLength); + + return result.toString(); + } + +} + +///fade=in:0:25, fade=out:975:25 + diff --git a/app/src/main/java/org/ffmpeg/android/filters/OverlayVideoFilter.java b/app/src/main/java/org/ffmpeg/android/filters/OverlayVideoFilter.java new file mode 100644 index 0000000..9cdab2d --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/filters/OverlayVideoFilter.java @@ -0,0 +1,65 @@ +package org.ffmpeg.android.filters; + +import java.io.File; + +/** + * @class overlay overlay one image or video on top of another + * @desc x is the x coordinate of the overlayed video on the main video, + * y is the y coordinate. The parameters are expressions containing + * the following parameters: + *

+ *      	main_w, main_h
+ *          main input width and height
+ *
+ *       W, H
+ *           same as main_w and main_h
+ *
+ *       overlay_w, overlay_h
+ *           overlay input width and height
+ *
+ *       w, h
+ *           same as overlay_w and overlay_h
+ * 
+ * @examples
draw the overlay at 10 pixels from the bottom right
+ * corner of the main video.
+ * 		main_w-overlay_w-10
+ * 		main_h-overlay_h-10
+ * draw the overlay in the bottom left corner of the input
+ *  10
+ *  main_h-overlay_h-10 [out]
+ */ +public class OverlayVideoFilter extends VideoFilter { + + public File overlayFile; + public String xParam, yParam; + + public OverlayVideoFilter() { + + } + + public OverlayVideoFilter(File fileMovieOverlay, int x, int y) { + this.overlayFile = fileMovieOverlay; + this.xParam = Integer.toString(x); + this.yParam = Integer.toString(y); + } + + public OverlayVideoFilter(File fileMovieOverlay, String xExpression, String yExpression) { + this.overlayFile = fileMovieOverlay; + this.xParam = xExpression; + this.yParam = yExpression; + } + + public String getFilterString() { + if (overlayFile != null) + return "movie=" + + overlayFile.getAbsolutePath() + + " [logo];[in][logo] " + + "overlay=" + xParam + ":" + yParam + + " [out]"; + else + return ""; + + } +} + +//"\"movie="+ overlayImage.getPath() +" [logo];[in][logo] overlay=0:0 [out]\"", \ No newline at end of file diff --git a/app/src/main/java/org/ffmpeg/android/filters/RedactVideoFilter.java b/app/src/main/java/org/ffmpeg/android/filters/RedactVideoFilter.java new file mode 100644 index 0000000..b971c53 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/filters/RedactVideoFilter.java @@ -0,0 +1,23 @@ +package org.ffmpeg.android.filters; + +import java.io.File; + +public class RedactVideoFilter extends VideoFilter { + + private File fileRedactList; + + public RedactVideoFilter(File fileRedactList) { + this.fileRedactList = fileRedactList; + } + + public String getFilterString() { + if (fileRedactList != null) + return "redact=" + fileRedactList.getAbsolutePath(); + else + return ""; + + } +} + +//redact=blurbox.txt [out] [d], [d]nullsink +//"redact=" + Environment.getExternalStorageDirectory().getPath() + "/" + PACKAGENAME + "/redact_unsort.txt", \ No newline at end of file diff --git a/app/src/main/java/org/ffmpeg/android/filters/TransposeVideoFilter.java b/app/src/main/java/org/ffmpeg/android/filters/TransposeVideoFilter.java new file mode 100644 index 0000000..b02264f --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/filters/TransposeVideoFilter.java @@ -0,0 +1,29 @@ +package org.ffmpeg.android.filters; + +/* + * works for video and images + * 0 = 90CounterCLockwise and Vertical Flip (default) +1 = 90Clockwise +2 = 90CounterClockwise +3 = 90Clockwise and Vertical Flip + */ +public class TransposeVideoFilter extends VideoFilter { + + private int mTranspose = -1; + + public final static int NINETY_COUNTER_CLOCKWISE_AND_VERTICAL_FLIP = 0; + public final static int NINETY_CLOCKWISE = 1; + public final static int NINETY_COUNTER_CLOCKWISE = 2; + public final static int NINETY_CLOCKWISE_AND_VERTICAL_FLIP = 3; + + public TransposeVideoFilter(int transpose) { + mTranspose = transpose; + } + + @Override + public String getFilterString() { + + return "transpose=" + mTranspose; + } + +} diff --git a/app/src/main/java/org/ffmpeg/android/filters/VideoFilter.java b/app/src/main/java/org/ffmpeg/android/filters/VideoFilter.java new file mode 100644 index 0000000..5251689 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/filters/VideoFilter.java @@ -0,0 +1,25 @@ +package org.ffmpeg.android.filters; + +import java.util.ArrayList; +import java.util.Iterator; + +public abstract class VideoFilter { + + public abstract String getFilterString(); + + public static String format(ArrayList listFilters) { + StringBuffer result = new StringBuffer(); + + Iterator it = listFilters.iterator(); + VideoFilter vf; + + while (it.hasNext()) { + vf = it.next(); + result.append(vf.getFilterString()); + + if (it.hasNext()) + result.append(","); + } + return result.toString(); + } +} diff --git a/app/src/main/java/org/ffmpeg/android/test/ConcatTest.java b/app/src/main/java/org/ffmpeg/android/test/ConcatTest.java new file mode 100644 index 0000000..19e178c --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/test/ConcatTest.java @@ -0,0 +1,62 @@ +package org.ffmpeg.android.test; + + +import java.io.File; +import java.util.ArrayList; +import java.util.Locale; + +import net.sourceforge.sox.SoxController; + +import org.ffmpeg.android.Clip; +import org.ffmpeg.android.FfmpegController; +import org.ffmpeg.android.ShellUtils; + +public class ConcatTest { + + public static void test(String videoRoot, String fileTmpPath, String fileOut, double fadeLen) throws Exception { + File fileTmp = new File(fileTmpPath); + File fileAppRoot = new File(""); + File fileVideoRoot = new File(videoRoot); + + FfmpegController fc = new FfmpegController(null, fileTmp); + SoxController sxCon = new SoxController(null, fileAppRoot, null); + + ArrayList listVideos = new ArrayList(); + + String[] fileList = fileVideoRoot.list(); + for (String fileVideo : fileList) { + if (fileVideo.endsWith("mp4")) { + Clip clip = new Clip(); + clip.path = new File(fileVideoRoot, fileVideo).getCanonicalPath(); + + fc.getInfo(clip); + + clip.duration = clip.duration - fadeLen; + listVideos.add(clip); + + + } + } + + Clip clipOut = new Clip(); + clipOut.path = new File(fileOut).getCanonicalPath(); + + fc.concatAndTrimFilesMP4Stream(listVideos, clipOut, false, false, new ShellUtils.ShellCallback() { + + @Override + public void shellOut(String shellLine) { + + System.out.println("fc>" + shellLine); + } + + @Override + public void processComplete(int exitValue) { + + if (exitValue < 0) + System.err.println("concat non-zero exit: " + exitValue); + } + }); + + + } +} diff --git a/app/src/main/java/org/ffmpeg/android/test/ConvertTest.java b/app/src/main/java/org/ffmpeg/android/test/ConvertTest.java new file mode 100644 index 0000000..a1d6178 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/test/ConvertTest.java @@ -0,0 +1,5 @@ +package org.ffmpeg.android.test; + +public class ConvertTest { + +} diff --git a/app/src/main/java/org/ffmpeg/android/test/CrossfadeTest.java b/app/src/main/java/org/ffmpeg/android/test/CrossfadeTest.java new file mode 100644 index 0000000..da73756 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/test/CrossfadeTest.java @@ -0,0 +1,188 @@ +package org.ffmpeg.android.test; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Locale; + +import net.sourceforge.sox.CrossfadeCat; +import net.sourceforge.sox.SoxController; + +import org.ffmpeg.android.Clip; +import org.ffmpeg.android.FfmpegController; +import org.ffmpeg.android.ShellUtils; + +public class CrossfadeTest { + + + public static void test(String videoRoot, String fileTmpPath, String clipOutPath, double fadeLen) throws Exception { + File fileTmp = new File(fileTmpPath); + File fileAppRoot = new File(""); + File fileVideoRoot = new File(videoRoot); + + String fadeType = "l"; + int sampleRate = 22050; + int channels = 1; + + FfmpegController ffmpegc = new FfmpegController(null, fileTmp); + + Clip clipOut = new Clip(); + clipOut.path = clipOutPath; + clipOut.audioCodec = "aac"; + clipOut.audioBitrate = 56; + + + ArrayList listVideos = new ArrayList(); + + String[] fileList = fileVideoRoot.list(); + for (String fileVideo : fileList) { + if (fileVideo.endsWith("mp4")) { + Clip clip = new Clip(); + clip.path = new File(fileVideoRoot, fileVideo).getCanonicalPath(); + //clip.startTime = "00:00:03"; + //clip.duration = "00:00:02"; + + ffmpegc.getInfo(clip); + + //System.out.println("clip " + fileVideo + " duration=" + clip.duration); + + listVideos.add(clip); + + } + } + + //now add 1 second cross fade to each audio file and cat them together + SoxController sxCon = new SoxController(null, fileAppRoot, new ShellUtils.ShellCallback() { + + @Override + public void shellOut(String shellLine) { + + // System.out.println("sxCon> " + shellLine); + + } + + @Override + public void processComplete(int exitValue) { + + + if (exitValue != 0) { + System.err.println("sxCon> EXIT=" + exitValue); + + RuntimeException re = new RuntimeException("non-zero exit: " + exitValue); + re.printStackTrace(); + throw re; + } + + } + }); + + ArrayList alAudio = new ArrayList(); + + //convert each input file to a WAV so we can use Sox to process + int wavIdx = 0; + + for (Clip mediaIn : listVideos) { + if (new File(mediaIn.path).exists()) { + + if (mediaIn.audioCodec == null) { + //there is no audio track so let's generate silence + + + } else { + Clip audioOut = ffmpegc.convertToWaveAudio(mediaIn, new File(fileTmp, wavIdx + ".wav").getCanonicalPath(), sampleRate, channels, new ShellUtils.ShellCallback() { + + @Override + public void shellOut(String shellLine) { + + // System.out.println("convertToWav> " + shellLine); + + } + + @Override + public void processComplete(int exitValue) { + + if (exitValue != 0) { + + System.err.println("convertToWav> EXIT=" + exitValue); + + RuntimeException re = new RuntimeException("non-zero exit: " + exitValue); + re.printStackTrace(); + throw re; + } + } + }); + + alAudio.add(audioOut); + + /* + float duration = (float) sxCon.getLength(new File(audioOut.path).getCanonicalPath()); + + if (mediaIn.duration == null) + { + mediaIn.duration = String.format(Locale.US, "%f", duration); + }*/ + ffmpegc.getInfo(mediaIn); + + + wavIdx++; + } + } else { + throw new FileNotFoundException(mediaIn.path); + } + } + + if (alAudio.size() > 0) { + String fileOut = alAudio.get(0).path; + + System.out.println("mix length=" + sxCon.getLength(fileOut)); + + for (int i = 1; i < alAudio.size(); i++) { + + File fileAdd = new File(alAudio.get(i).path); + + CrossfadeCat xCat = new CrossfadeCat(sxCon, fileOut, fileAdd.getCanonicalPath(), fadeLen, fileOut); + xCat.start(); + + fileAdd.deleteOnExit(); + + System.out.println("mix length=" + sxCon.getLength(fileOut)); + + } + + + //1 second fade in and fade out, t = triangle or linear + //String fadeLenStr = sxCon.formatTimePeriod(fadeLen); + + + String fadeFileOut = sxCon.fadeAudio(fileOut, fadeType, fadeLen, sxCon.getLength(fileOut) - fadeLen, fadeLen); + + //now export the final file to our requested output format mOut.mimeType = AppConstants.MimeTypes.MP4_AUDIO; + + Clip mdFinalIn = new Clip(); + mdFinalIn.path = fadeFileOut; + + + System.out.println("final duration: " + sxCon.getLength(fadeFileOut)); + + Clip exportOut = ffmpegc.convertTo3GPAudio(mdFinalIn, clipOut, new ShellUtils.ShellCallback() { + + @Override + public void shellOut(String shellLine) { + + //System.out.println("convertTo3gp> " + shellLine); + } + + @Override + public void processComplete(int exitValue) { + + if (exitValue < 0) { + RuntimeException re = new RuntimeException("non-zero exit: " + exitValue); + re.printStackTrace(); + throw re; + } + } + }); + } + } + +} diff --git a/app/src/main/java/org/ffmpeg/android/test/FilterTest.java b/app/src/main/java/org/ffmpeg/android/test/FilterTest.java new file mode 100644 index 0000000..0426df6 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/test/FilterTest.java @@ -0,0 +1,65 @@ +package org.ffmpeg.android.test; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +import org.ffmpeg.android.filters.CropVideoFilter; +import org.ffmpeg.android.filters.DrawBoxVideoFilter; +import org.ffmpeg.android.filters.DrawTextVideoFilter; +import org.ffmpeg.android.filters.FadeVideoFilter; +import org.ffmpeg.android.filters.TransposeVideoFilter; +import org.ffmpeg.android.filters.VideoFilter; + +import android.app.Activity; +import android.content.Context; + +public class FilterTest { + + + public static void test(String title, String textColor, File fileFont, String boxColor, String opacity) throws Exception { + ArrayList listFilters = new ArrayList(); + + File fileDir = new File("tmp"); + fileDir.mkdir(); + + int height = 480; + int width = 720; + int lowerThird = height / 3; + DrawBoxVideoFilter vf = new DrawBoxVideoFilter(0, height - lowerThird, width, lowerThird, 100, "blue", fileDir); + + DrawTextVideoFilter vfTitle = + new DrawTextVideoFilter(title, + DrawTextVideoFilter.X_CENTERED, DrawTextVideoFilter.Y_CENTERED, + textColor, + 38, + fileFont, + true, + boxColor, + opacity); + + float fps = 29.97f; + int fadeTime = (int) (fps * 3); + //fades in first 3 seconds + FadeVideoFilter vfFadeIn = new FadeVideoFilter("in", 0, fadeTime); + + //fades out last 50 frames + int totalFrames = (int) (14.37 * 29.97); + FadeVideoFilter vfFadeOut = new FadeVideoFilter("out", totalFrames - fadeTime, fadeTime); + + //crops video in 100 pixels on each side + CropVideoFilter vfCrop = new CropVideoFilter("in_w-100", "in_h-100", "100", "100"); + + //rotates video 90 degress clockwise + TransposeVideoFilter vfTranspose = new TransposeVideoFilter(TransposeVideoFilter.NINETY_CLOCKWISE); + + listFilters.add(vfTranspose); + listFilters.add(vfCrop); + listFilters.add(vfTitle); + listFilters.add(vfFadeIn); + listFilters.add(vfFadeOut); + + + fileDir.deleteOnExit(); + } +} diff --git a/app/src/main/java/org/ffmpeg/android/test/MixTest.java b/app/src/main/java/org/ffmpeg/android/test/MixTest.java new file mode 100644 index 0000000..2d82595 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/test/MixTest.java @@ -0,0 +1,325 @@ +package org.ffmpeg.android.test; + +import android.content.Context; +import android.util.Log; + +import com.coremedia.iso.boxes.Container; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder; +import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; +import com.googlecode.mp4parser.authoring.tracks.AppendTrack; + +import org.ffmpeg.android.Clip; +import org.ffmpeg.android.FfmpegController; +import org.ffmpeg.android.ShellUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.util.LinkedList; +import java.util.List; + +public class MixTest { + + public static void test(String fileTmpPath, String videoClipPath, String audioClipPath, final Clip clipOut, Context context) throws Exception { + File fileTmp = new File(fileTmpPath); + File fileAppRoot = new File(""); + + final FfmpegController fc = new FfmpegController(context, fileTmp); + + final Clip clipVideo = new Clip(videoClipPath); + //fc.getInfo(clipVideo); + clipVideo.videoCodec = "mp4"; + final Clip clipAudio = new Clip(audioClipPath); + //fc.getInfo(clipAudio); + clipAudio.audioCodec = "aac"; + final Clip clipTemp = new Clip("/storage/emulated/0/.mofunshow/movies/90331/test_temp.mp4"); + final Clip clipPic = new Clip("/storage/emulated/0/.mofunshow/movies/90331/1.png"); +// fc.removeVideoAudio(clipVideo, clipTemp, new ShellUtils.ShellCallback() { +// @Override +// public void shellOut(String shellLine) { +// System.out.println("removeVideoAudio> " + shellLine); +// } +// +// @Override +// public void processComplete(int exitValue) { +// if (exitValue != 0) +// System.err.println("removeVideoAudio concat non-zero exit: " + exitValue); +// try { +// fc.combineAudioAndVideo(clipTemp, clipAudio, clipOut, new ShellUtils.ShellCallback() { +// +// @Override +// public void shellOut(String shellLine) { +// System.out.println("MIX> " + shellLine); +// } +// +// @Override +// public void processComplete(int exitValue) { +// +// if (exitValue != 0) +// System.err.println("combineAudioAndVideo concat non-zero exit: " + exitValue); +// } +// }); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } +// }); +// fc.combineAudioAndVideo(clipVideo, clipAudio, clipOut, new ShellUtils.ShellCallback() { +// +// @Override +// public void shellOut(String shellLine) { +// System.out.println("MIX> " + shellLine); +// } +// +// @Override +// public void processComplete(int exitValue) { +// +// if (exitValue != 0) +// System.err.println("combineAudioAndVideo concat non-zero exit: " + exitValue); +// } +// }); +// fc.convertImageToMP4(clipPic, 3, "/storage/emulated/0/.mofunshow/movies/90331/test_temp.mp4", new ShellUtils.ShellCallback() { +// @Override +// public void shellOut(String shellLine) { +// System.out.println("convertImageToMP4> " + shellLine); +// } +// +// @Override +// public void processComplete(int exitValue) { +// System.out.println("convertImageToMP4> complete"); +// } +// }); + + + } + + public static void testJpegToMp4(final String mp4FilePath, String fileTmpPath, List picFiles, final String clipOut, Context context) throws Exception { + File fileTmp = new File(fileTmpPath); + final FfmpegController fc = new FfmpegController(context, fileTmp); + final String clipTemp = "/storage/emulated/0/.mofunshow/movies/90331/test_temp.mp4"; + String clipLogo = "/storage/emulated/0/.mofunshow/movies/90331/logo.png"; + final Clip clipPic = new Clip(picFiles.get(0)); + clipPic.height = 450; + clipPic.width = 800; + clipPic.videoBitrate = 256; + + Clip logo = new Clip(clipLogo); + + fc.convertImageToMP4(clipPic, logo, 2, clipOut, new ShellUtils.ShellCallback() { + @Override + public void shellOut(String shellLine) { + System.out.println("convertImageToMP4> " + shellLine); + } + + @Override + public void processComplete(int exitValue) { + System.out.println("convertImageToMP4> complete"); +// List fileList = new ArrayList(); +// fileList.add(mp4FilePath); +//// fileList.add(clipOut); +// fileList.add(clipOut); +// newClipMethod(fileList,clipTemp); +// try { +// fc.filterMp4Complex(new Clip(clipOut), clipPic,clipTemp, new ShellUtils.ShellCallback() { +// @Override +// public void shellOut(String shellLine) { +// System.out.println("filterMp4Complex> " + shellLine); +// } +// +// @Override +// public void processComplete(int exitValue) { +// System.out.println("filterMp4Complex> complete"); +// } +// }); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// try { +// fc.mergMp4(new Clip(mp4FilePath), new Clip(clipOut), clipTemp, new ShellUtils.ShellCallback() { +// @Override +// public void shellOut(String shellLine) { +// System.out.println("mergMp4> " + shellLine); +// } +// +// @Override +// public void processComplete(int exitValue) { +// System.out.println("mergMp4> complete"); +// } +// }); +// } catch (Exception e) { +// e.printStackTrace(); +// } + } + }); + + + } + + public static void testMergeMp4(final String mp4FilePath, final String filterPath, String fileTmpPath, final String clipOut, Context context) throws Exception { + File fileTmp = new File(fileTmpPath); + + String content = "file '" + mp4FilePath + "'\r\n" + + "file '" + filterPath + "'"; + + final FfmpegController fc = new FfmpegController(context, fileTmp); + fc.mergMp4(saveConfig(content, fileTmpPath), clipOut, new ShellUtils.ShellCallback() { + @Override + public void shellOut(String shellLine) { + System.out.println("mergMp4> " + shellLine); + } + + @Override + public void processComplete(int exitValue) { + System.out.println("mergMp4> complete"); + } + }); + + + } + + private static String saveConfig(String content, String tempPath) { + String textName = "config.txt"; + writeTxtToFile(content, tempPath, textName); + return tempPath + "/" + textName; + + } + + // 将字符串写入到文本文件中 + public static void writeTxtToFile(String strcontent, String filePath, String fileName) { + //生成文件夹之后,再生成文件,不然会出错 + makeFilePath(filePath, fileName); + + String strFilePath = filePath + fileName; + // 每次写入时,都换行写 + String strContent = strcontent + "\r\n"; + try { + File file = new File(strFilePath); + if (!file.exists()) { + Log.d("TestFile", "Create the file:" + strFilePath); + file.getParentFile().mkdirs(); + file.createNewFile(); + } else { + file.delete(); + } + RandomAccessFile raf = new RandomAccessFile(file, "rwd"); + raf.seek(file.length()); + raf.write(strContent.getBytes()); + raf.close(); + } catch (Exception e) { + Log.e("TestFile", "Error on write File:" + e); + } + } + + // 生成文件 + public static File makeFilePath(String filePath, String fileName) { + File file = null; + makeRootDirectory(filePath); + try { + file = new File(filePath + fileName); + if (!file.exists()) { + file.createNewFile(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return file; + } + + // 生成文件夹 + public static void makeRootDirectory(String filePath) { + File file = null; + try { + file = new File(filePath); + if (!file.exists()) { + file.mkdir(); + } + } catch (Exception e) { + Log.i("error:", e + ""); + } + + } + + public static void newClipMethod(List fileList, String output) { + List moviesList = new LinkedList(); + try { + for (String file : fileList) { + moviesList.add(MovieCreator.build(file)); + } + } catch (IOException e) { + e.printStackTrace(); + } + + List videoTracks = new LinkedList(); + List audioTracks = new LinkedList(); + for (Movie m : moviesList) { + for (Track t : m.getTracks()) { + if (t.getHandler().equals("soun")) { + audioTracks.add(t); + } + if (t.getHandler().equals("vide")) { + videoTracks.add(t); + } + } + } + + Movie result = new Movie(); + + try { + if (audioTracks.size() > 0) { + result.addTrack(audioTracks.get(0)); + } + if (videoTracks.size() > 0) { + result.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()]))); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + Container out = new DefaultMp4Builder().build(result); + + try { + FileChannel fc = new FileOutputStream(new File(output)).getChannel(); + out.writeContainer(fc); + fc.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + moviesList.clear(); + fileList.clear(); + + } + + public static void testMakeLastFrameFilter(String lastFrame, long timeLength, final String mp4FilePath, final String fileTmpPath, final String clipOut, final Context context) throws Exception { + File fileTmp = new File(fileTmpPath); + float timeSecond = timeLength / 1000f; + final FfmpegController fc = new FfmpegController(context, fileTmp); + final String clipLogo = "/storage/emulated/0/.mofunshow/movies/90331/logo2.png"; + final String tempMp4 = fileTmpPath + "/" + "temp_filter.mp4"; + Clip logo = new Clip(clipLogo); + + fc.makeLastFrameFilter2(lastFrame, timeSecond - 0.7f, new Clip(mp4FilePath), logo, 5, tempMp4, new ShellUtils.ShellCallback() { + @Override + public void shellOut(String shellLine) { + System.out.println("makeLastFrameFilter> " + shellLine); + } + + @Override + public void processComplete(int exitValue) { + System.out.println("makeLastFrameFilter> complete"); + try { + testMergeMp4(mp4FilePath, tempMp4, fileTmpPath, clipOut, context); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + + } +} diff --git a/app/src/main/java/org/ffmpeg/android/test/Tests.java b/app/src/main/java/org/ffmpeg/android/test/Tests.java new file mode 100644 index 0000000..65f09cb --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/test/Tests.java @@ -0,0 +1,75 @@ +package org.ffmpeg.android.test; + +import org.ffmpeg.android.Clip; + +import java.io.File; + +public class Tests { + + /** + * @param args + */ + public static void main(String[] args) throws Exception { + + + String[] testpaths = { + // "/home/n8fr8/Desktop/smcampmovie", + // "/home/n8fr8/Desktop/smcampmovie2", + "/home/n8fr8/Desktop/sm3" + + }; + + int idx = -1; + + double fadeLen = 1; + + for (String testpath : testpaths) { + idx++; + + System.out.println("************************************"); + System.out.println("CONCAT TEST: " + testpath); + + File fileVideoOutput = new File("/tmp/test" + idx + ".mp4"); + fileVideoOutput.delete(); + + ConcatTest.test(testpath, "/tmp", fileVideoOutput.getCanonicalPath(), fadeLen); + + if (!fileVideoOutput.exists()) { + System.out.println("FAIL!! > output file did not get created: " + fileVideoOutput.getCanonicalPath()); + continue; + } else + System.out.println("SUCCESS!! > " + fileVideoOutput.getCanonicalPath()); + + System.out.println("************************************"); + System.out.println("CROSSFADE TEST: " + testpath); + + File fileAudioOutput = new File("/tmp/test" + idx + ".3gp"); + fileAudioOutput.delete(); + CrossfadeTest.test(testpath, "/tmp", fileAudioOutput.getCanonicalPath(), fadeLen); + if (!fileAudioOutput.exists()) { + System.out.println("FAIL!! > output file did not get created: " + fileAudioOutput.getCanonicalPath()); + continue; + } else + System.out.println("SUCCESS!! > " + fileAudioOutput.getCanonicalPath()); + + System.out.println("************************************"); + System.out.println("MIX TEST: " + testpath); + + File fileMix = new File("/tmp/test" + idx + "mix.mp4"); + fileMix.delete(); + Clip clipMixOut = new Clip(fileMix.getCanonicalPath()); +// MixTest.test("/tmp", fileVideoOutput.getCanonicalPath(), fileAudioOutput.getCanonicalPath(), clipMixOut); + if (!fileMix.exists()) + System.out.println("FAIL!! > output file did not get created: " + fileMix.getCanonicalPath()); + else + System.out.println("SUCCESS!! > " + fileMix.getCanonicalPath()); + + + } + + System.out.println("**********************"); + System.out.println("*******FIN**********"); + + } + +} diff --git a/app/src/main/res/drawable-hdpi/ic_action_search.png b/app/src/main/res/drawable-hdpi/ic_action_search.png new file mode 100644 index 0000000000000000000000000000000000000000..67de12decbd60613f6716de113daae46dc7d6ff9 GIT binary patch literal 3120 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 E59N2f#Q*>R literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..72a445d04e643f4dd80a3deed313a8be95d02fd5 GIT binary patch literal 3065 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 Hu0mjfi6q`C literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_action_search.png b/app/src/main/res/drawable-xhdpi/ic_action_search.png new file mode 100644 index 0000000000000000000000000000000000000000..d699c6b37e0dcb1d636ebdd6733ab6d576d97ab5 GIT binary patch literal 3199 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 ReC-u>pr>W5S)<|b@_%^e3*P_$ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..002e7b0522a42546be339a0358584324fdc3e71a GIT binary patch literal 6679 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml new file mode 100644 index 0000000..1b8f8f0 --- /dev/null +++ b/app/src/main/res/layout/main.xml @@ -0,0 +1,16 @@ + + + +