diff --git a/README.md b/README.md index 7ad2b0c..4f3a9fa 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ Android applications. It depends on the [android-ffmpeg](https://github.com/guardianproject/android-ffmpeg) project to provide the ffmpeg binary. +New ffmpeg binary has be provided from [android-ffmpeg](http://writingminds.github.io/ffmpeg-android/) & [ffmpeg-android-java](https://github.com/WritingMinds/ffmpeg-android-java) +FFmpeg version was update to n2.4.2. + + For ease of developer use, we've included the FFMPEG and Sox binaries in the project, however, we strongly recommend you compile them yourselves using the steps below. 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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9f70036 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/code/Config.java b/app/src/main/java/com/code/Config.java new file mode 100644 index 0000000..ff6d88e --- /dev/null +++ b/app/src/main/java/com/code/Config.java @@ -0,0 +1,13 @@ +package com.code; + +/** + * @author Chad + * @title com.code + * @description + * @modifier + * @date + * @since 16/6/1 下午9:19 + **/ +public class Config { + public static String SYSTEM_DEFAULT_FONT_PATH = "/system/fonts/DroidSansFallback.ttf"; +} 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..9a07143 --- /dev/null +++ b/app/src/main/java/com/code/MainActivity.java @@ -0,0 +1,144 @@ +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/20160529171913002801000325.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(); + File[] files = new File("/system/fonts/").listFiles(); + for (File file : files) { + String fileName = file.getName().toLowerCase(); + if (fileName.contains("miui")) { + if (fileName.toLowerCase().equals("miui-regular")) { + Config.SYSTEM_DEFAULT_FONT_PATH = file.getAbsolutePath(); + break; + } + Config.SYSTEM_DEFAULT_FONT_PATH = file.getAbsolutePath(); + break; + } + } + 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(); + } + + } + }); + 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..6ffa2c1 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/Clip.java @@ -0,0 +1,68 @@ +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 float frameRate = 25; + + 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..a4bd16e --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/FfmpegController.java @@ -0,0 +1,1819 @@ +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, boolean overwrite) throws FileNotFoundException, IOException { + mFileTemp = fileTemp; + installBinaries(context, overwrite); + } + + 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 (upgrade) { + 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 CODEC_VIDEO = "-c:v"; + + 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 static final String THREAD = "-threads"; + + public static final String PROFILE = "-profile:v"; + + public static final String PRESET = "-preset"; + public static final String PIX_FORMAT = "-pix_fmt"; + public static final String MAP = "-map"; + public static final String FILTER_COMPLEX = "-filter_complex"; + + + } + + 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, float duration, String outPath, ShellCallback sc) throws Exception { + Clip result = new Clip(); + ArrayList cmd = new ArrayList(); + cmd.add(mFfmpegBin); + cmd.add("-y"); +// +// cmd.add(Argument.THREAD); +// cmd.add("4"); + + cmd.add(Argument.STARTTIME); + cmd.add(String.valueOf(startTime)); + + cmd.add(Argument.DURATION); + cmd.add(String.valueOf(duration)); + + cmd.add(Argument.FILE_INPUT); + cmd.add(new File(mediaIn.path).getCanonicalPath()); + + cmd.add(Argument.FILE_INPUT); + cmd.add(new File(mediaLogo.path).getCanonicalPath()); + + cmd.add(Argument.FILE_INPUT); + cmd.add(new File(lastFrame).getCanonicalPath()); + + cmd.add(Argument.FRAMERATE); + cmd.add(String.valueOf(mediaIn.frameRate)); + + cmd.add(Argument.PRESET); + cmd.add("medium"); + + cmd.add(Argument.CODEC_VIDEO); + cmd.add("libx264"); + + cmd.add(Argument.PIX_FORMAT); + cmd.add("yuv420p"); + + cmd.add(Argument.MAP); + cmd.add("0:0"); + + cmd.add(Argument.FILTER_COMPLEX); + String filter = "geq=lum='if(lte(T,0.5), 255*T*(1/0.5),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="+ Config.SYSTEM_DEFAULT_FONT_PATH+":text=主演:猪八戒香肠嘴™:fontsize=23:fontcolor=white@1.0:x=main_w/2-20:y=main_h/2+20[text];" + + "[lay][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); + } + + if (mediaIn.videoBitrate != -1) { + cmd.add(Argument.BITRATE_VIDEO); + cmd.add(mediaIn.videoBitrate + "k"); + } + + cmd.add(Argument.PROFILE); + 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..eb9d4d8 --- /dev/null +++ b/app/src/main/java/org/ffmpeg/android/test/MixTest.java @@ -0,0 +1,383 @@ +package org.ffmpeg.android.test; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.os.Environment; +import android.text.TextPaint; +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.R; +import org.ffmpeg.android.ShellUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +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(FfmpegController fc, 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 + "'"; + + if (fc == null) { + 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, false); + final String tempMp4 = fileTmpPath + "/" + "temp_filter.mp4"; + + Bitmap bitmap = BitmapFactory.decodeFile(lastFrame); + Clip logo = new Clip(createLogoWall("主演:香肠嘴猪八戒™の개암Athens❄️冰.", bitmap.getWidth(), context)); + + Clip mp4ClipOrg = new Clip(mp4FilePath); + mp4ClipOrg.width = bitmap.getWidth(); + mp4ClipOrg.height = bitmap.getHeight(); + mp4ClipOrg.frameRate = 24; + fc.makeLastFrameFilter2(lastFrame, timeSecond - 2f, mp4ClipOrg, logo, 0.7f, 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(fc, mp4FilePath, tempMp4, fileTmpPath, clipOut, context); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + + } + + public static Bitmap readBitmap(int resid, Context context) { + BitmapFactory.Options opt = new BitmapFactory.Options(); + opt.inPreferredConfig = Bitmap.Config.ARGB_8888; + opt.inPurgeable = true; + opt.inInputShareable = true; +//获取资源图片 + InputStream input = context.getResources().openRawResource(resid); + return BitmapFactory.decodeStream(input, null, opt); + } + + private static String createLogoWall(String text, int fixWidth, Context context) { + String outPath = Environment.getExternalStorageDirectory() + "/logo_user.png"; + new File(outPath).delete(); + Bitmap bitmap = getDrawBitMap(readBitmap(R.raw.end_wallpaper_eng, context), text, fixWidth); + FileOutputStream fos = null; + try { + fos = new FileOutputStream(outPath); + bitmap.compress(Bitmap.CompressFormat.PNG, 80, fos); + fos.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return outPath; + } + + public static Bitmap getDrawBitMap(Bitmap bmp, String text, int fixWidth) { + int width = bmp.getWidth(); + int height = bmp.getHeight(); + int newWidth = fixWidth; + Bitmap newBitmap = Bitmap.createBitmap(newWidth, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(newBitmap); + canvas.drawBitmap(bmp, (newWidth - width) / 2, 0, null); + TextPaint textPaint = new TextPaint(); + textPaint.setAntiAlias(true); + textPaint.setColor(Color.WHITE); + textPaint.setTextSize(21.0F); +// StaticLayout sl = new StaticLayout(text, textPaint, newBitmap.getWidth() - 8, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); +// canvas.translate(width / 2 + 30, height / 2 + 15); +// sl.draw(canvas); + canvas.drawText(text, newWidth / 2 - 30, height / 2 + 30, textPaint); + return newBitmap; + } +} 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 0000000..67de12d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..fba1ff0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_search.png b/app/src/main/res/drawable-mdpi/ic_action_search.png new file mode 100644 index 0000000..134d549 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_search.png differ 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 0000000..72a445d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ 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 0000000..d699c6b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_search.png differ 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 0000000..002e7b0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ 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 @@ + + + +