From 8e60e1e206884ff26d866bd2bb5f5a1fd5ddc2e1 Mon Sep 17 00:00:00 2001 From: qqchenjian318 <1694455148@qq.com> Date: Sun, 8 Apr 2018 15:34:07 +0800 Subject: [PATCH 01/10] Create README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d87ec40 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# VideoEditor-For-Android +一个Android的视频编辑器,包括了视频录制、剪切、增加bgm、美白、加滤镜、加水印等多种功能 + +基于android硬编码的视频编辑器,不支持4.3以下系统,通过android的api完成视频采集,通过OpenGL,完成视频数据帧的处理,通过android的硬编码器MeidaCodec +对采集到的视频流进行硬编码。 +利用OpenGL完成视频的美白、加滤镜、加水印等功能。利用MediaCodec完成音视频的分离和音频的一些混音处理 From 71f264d5df30f88f7cc54e218eb3e750e2ffff78 Mon Sep 17 00:00:00 2001 From: chenjian Date: Tue, 24 Apr 2018 10:40:21 +0800 Subject: [PATCH 02/10] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=8B=BC=E6=8E=A5=E7=9A=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 15 +- .idea/modules.xml | 2 +- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 5 + app/src/main/cpp/native-lib.cpp | 14 + .../example/cj/videoeditor/CustomBgView.java | 52 -- .../example/cj/videoeditor/MyApplication.java | 4 + .../example/cj/videoeditor/MyClassLoader.java | 17 + .../activity/AudioEditorActivity.java | 2 + .../cj/videoeditor/activity/MainActivity.java | 135 +++ .../activity/MediaSelectVideoActivity.java | 147 ++++ .../videoeditor/activity/PreviewActivity.java | 2 + .../activity/VideoConnectActivity.java | 192 +++++ .../activity/VideoSelectActivity.java | 2 +- .../adapter/VideoSelectAdapter.java | 152 ++++ .../cj/videoeditor/bean/AudioSettingInfo.java | 13 + .../example/cj/videoeditor/bean/CutBean.java | 56 ++ .../cj/videoeditor/bean/MediaDecode.java | 18 + .../cj/videoeditor/drawer/TextureRender.java | 396 +++++++++ .../cj/videoeditor/drawer/VideoDrawer.java | 213 +++-- .../cj/videoeditor/jni/AudioJniUtils.java | 4 + .../videoeditor/media/MediaPlayerWrapper.java | 4 +- .../cj/videoeditor/mediacodec/AudioCodec.java | 7 +- .../videoeditor/mediacodec/AudioRunnable.java | 784 ++++++++++++++++++ .../mediacodec/MediaMuxerRunnable.java | 202 +++++ .../videoeditor/mediacodec/OutputSurface.java | 41 +- .../videoeditor/mediacodec/VideoClipper.java | 150 ++-- .../videoeditor/mediacodec/VideoRunnable.java | 388 +++++++++ .../cj/videoeditor/widget/TitleView.java | 307 +++++++ .../videoeditor/widget/VideoPreviewView.java | 12 + .../main/res/drawable/app_back_selector.xml | 8 + .../main/res/drawable/bg_circle_yellow.xml | 8 + .../drawable/selector_btn_image_choose.xml | 6 + .../res/drawable/selector_title_bar_btn.xml | 11 + .../drawable/setting_text_color_selector.xml | 7 + app/src/main/res/layout/activity_connect.xml | 42 + app/src/main/res/layout/activity_main.xml | 13 +- .../layout/activity_media_select_video.xml | 22 + app/src/main/res/layout/item_media_video.xml | 29 + app/src/main/res/layout/item_video_select.xml | 8 + app/src/main/res/layout/view_titlebar.xml | 92 ++ app/src/main/res/mipmap-xxhdpi/btn_back_n.png | Bin 0 -> 1799 bytes app/src/main/res/mipmap-xxhdpi/btn_back_p.png | Bin 0 -> 1773 bytes .../res/mipmap-xxhdpi/icon_choice_nor.png | Bin 0 -> 3504 bytes .../mipmap-xxhdpi/icon_choice_selected.png | Bin 0 -> 2689 bytes app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 10 + build.gradle | 8 + 49 files changed, 3385 insertions(+), 221 deletions(-) delete mode 100644 app/src/main/java/com/example/cj/videoeditor/CustomBgView.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/MyClassLoader.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/activity/MediaSelectVideoActivity.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/activity/VideoConnectActivity.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/adapter/VideoSelectAdapter.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/bean/AudioSettingInfo.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/bean/CutBean.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/bean/MediaDecode.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/drawer/TextureRender.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/mediacodec/AudioRunnable.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/mediacodec/MediaMuxerRunnable.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/mediacodec/VideoRunnable.java create mode 100644 app/src/main/java/com/example/cj/videoeditor/widget/TitleView.java create mode 100644 app/src/main/res/drawable/app_back_selector.xml create mode 100644 app/src/main/res/drawable/bg_circle_yellow.xml create mode 100644 app/src/main/res/drawable/selector_btn_image_choose.xml create mode 100644 app/src/main/res/drawable/selector_title_bar_btn.xml create mode 100644 app/src/main/res/drawable/setting_text_color_selector.xml create mode 100644 app/src/main/res/layout/activity_connect.xml create mode 100644 app/src/main/res/layout/activity_media_select_video.xml create mode 100644 app/src/main/res/layout/item_media_video.xml create mode 100644 app/src/main/res/layout/view_titlebar.xml create mode 100644 app/src/main/res/mipmap-xxhdpi/btn_back_n.png create mode 100644 app/src/main/res/mipmap-xxhdpi/btn_back_p.png create mode 100644 app/src/main/res/mipmap-xxhdpi/icon_choice_nor.png create mode 100644 app/src/main/res/mipmap-xxhdpi/icon_choice_selected.png diff --git a/.idea/misc.xml b/.idea/misc.xml index 5d19981..ba7052b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,8 +1,5 @@ - - - - - - - - - - - - - - + diff --git a/.idea/modules.xml b/.idea/modules.xml index 25d2a93..3a305ae 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 28209dc..417617e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,6 +15,7 @@ android { cppFlags "" } } + } buildTypes { release { @@ -37,4 +38,5 @@ dependencies { compile 'com.android.support:appcompat-v7:26.0.0-alpha1' testCompile 'junit:junit:4.12' compile 'com.github.bumptech.glide:glide:3.7.0' + compile 'com.android.support:cardview-v7:27.0.2' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6a41725..2e4dcab 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index b0d2ee8..dfe8aba 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -72,3 +72,17 @@ Java_com_example_cj_videoeditor_jni_AudioJniUtils_audioMix(JNIEnv *env, jclass t } +extern "C" +JNIEXPORT jstring JNICALL +Java_com_example_cj_videoeditor_jni_AudioJniUtils_putString(JNIEnv *env, jclass type, + jstring info_) { + const char *info = env->GetStringUTFChars(info_, 0); + char buf[128]; + if (info == NULL) + return NULL; + sprintf(buf,"From C %s ",info); + + env->ReleaseStringUTFChars(info_, info); + + return env->NewStringUTF(buf); +} \ No newline at end of file diff --git a/app/src/main/java/com/example/cj/videoeditor/CustomBgView.java b/app/src/main/java/com/example/cj/videoeditor/CustomBgView.java deleted file mode 100644 index e184317..0000000 --- a/app/src/main/java/com/example/cj/videoeditor/CustomBgView.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.cj.videoeditor; - -import android.content.Context; -import android.graphics.BlurMaskFilter; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.support.annotation.Nullable; -import android.util.AttributeSet; -import android.util.Log; -import android.widget.LinearLayout; - -/** - * Created by qqche_000 on 2017/12/10. - * 固定背景的view - */ - -public class CustomBgView extends LinearLayout{ - - private Paint paint; - - public CustomBgView(Context context) { - this(context,null); - } - - public CustomBgView(Context context, @Nullable AttributeSet attrs) { - this(context, attrs,0); - } - - public CustomBgView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - private void init() { - setLayerType(LAYER_TYPE_SOFTWARE,null); - paint = new Paint(); - paint.setColor(Color.parseColor("#aaaaaa")); - paint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.SOLID)); - - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - Log.e("hero","---"+getWidth()+"---"+getHeight()); - Rect rect = new Rect(0,0,getWidth(),getHeight()-50); - - canvas.drawRect(rect,paint); - } -} diff --git a/app/src/main/java/com/example/cj/videoeditor/MyApplication.java b/app/src/main/java/com/example/cj/videoeditor/MyApplication.java index 85928bc..3961688 100644 --- a/app/src/main/java/com/example/cj/videoeditor/MyApplication.java +++ b/app/src/main/java/com/example/cj/videoeditor/MyApplication.java @@ -4,8 +4,10 @@ import android.content.Context; import android.util.DisplayMetrics; + /** * Created by qqche_000 on 2017/8/6. + * */ public class MyApplication extends Application{ @@ -22,6 +24,8 @@ public void onCreate() { .getDisplayMetrics(); screenWidth = mDisplayMetrics.widthPixels; screenHeight = mDisplayMetrics.heightPixels; + + } public static Context getContext() { diff --git a/app/src/main/java/com/example/cj/videoeditor/MyClassLoader.java b/app/src/main/java/com/example/cj/videoeditor/MyClassLoader.java new file mode 100644 index 0000000..b8a7784 --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/MyClassLoader.java @@ -0,0 +1,17 @@ +package com.example.cj.videoeditor; + +/** + * Created by cj on 2018/1/9. + * desc + */ + +public class MyClassLoader extends ClassLoader{ + public MyClassLoader(){ + super(); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + return super.findClass(name); + } +} diff --git a/app/src/main/java/com/example/cj/videoeditor/activity/AudioEditorActivity.java b/app/src/main/java/com/example/cj/videoeditor/activity/AudioEditorActivity.java index 3cece9f..e6e239b 100644 --- a/app/src/main/java/com/example/cj/videoeditor/activity/AudioEditorActivity.java +++ b/app/src/main/java/com/example/cj/videoeditor/activity/AudioEditorActivity.java @@ -2,6 +2,7 @@ import android.content.Intent; import android.os.Bundle; +import android.os.Handler; import android.support.annotation.Nullable; import android.view.View; import android.widget.Toast; @@ -26,6 +27,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { findViewById(R.id.audio_select).setOnClickListener(this); findViewById(R.id.pcm_to_audio).setOnClickListener(this); findViewById(R.id.audio_mix).setOnClickListener(this); + } @Override diff --git a/app/src/main/java/com/example/cj/videoeditor/activity/MainActivity.java b/app/src/main/java/com/example/cj/videoeditor/activity/MainActivity.java index 7e0c80c..fe3eba7 100644 --- a/app/src/main/java/com/example/cj/videoeditor/activity/MainActivity.java +++ b/app/src/main/java/com/example/cj/videoeditor/activity/MainActivity.java @@ -1,11 +1,28 @@ package com.example.cj.videoeditor.activity; +import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.v7.widget.CardView; +import android.util.Log; +import android.util.SparseIntArray; import android.view.View; import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; import com.example.cj.videoeditor.R; +import com.example.cj.videoeditor.jni.AudioJniUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import dalvik.system.DexClassLoader; public class MainActivity extends BaseActivity implements View.OnClickListener { @@ -17,13 +34,115 @@ protected void onCreate(Bundle savedInstanceState) { Button recordBtn = (Button) findViewById(R.id.record_activity); Button selectBtn = (Button) findViewById(R.id.select_activity); Button audioBtn = (Button) findViewById(R.id.audio_activity); + Button videoBtn = (Button) findViewById(R.id.video_connect); + recordBtn.setOnClickListener(this); selectBtn.setOnClickListener(this); audioBtn.setOnClickListener(this); + videoBtn.setOnClickListener(this); +// test(new int[]{2,7,11,15},9); + new Thread(new Runnable() { + @Override + public void run() { +// test(); + } + + + }).start(); + + final int max = Integer.MAX_VALUE >>> 4; + Random random = new Random(System.currentTimeMillis()); + + for (int i = 0; i < 200; i++) { + int hash = random.nextInt(max); +// testHash(hash); + } + StringBuilder sb = new StringBuilder(); + StringBuilder sb2 = new StringBuilder(); + for (int i = 0; i < 16; i++) { + sb.append(" ").append(indexOne[i]); + sb2.append(" ").append(indexTwo[i]); + } + Log.e("hero",sb.toString()); + Log.e("hero",sb2.toString()); + HashMap map = new HashMap(); + + testJNI(); + } + + private void testJNI() { + Log.e("hero","---"+ AudioJniUtils.putString(" hello JNI")); + } + + //测试两种不同hash的效果 + private int[] indexOne = new int[20]; + private int[] indexTwo = new int[20]; + public void testHash(Object key){ + int hash1 = singleWordWangJenkinsHash(key); + int hash2 = hashNew(key); + int index1 = hash1 & 15; + int index2 = hash2 & 15; + Log.e("hero",toBinaryString(hash1)+" ---------->>>> "+toBinaryString(hash2)+"----::"+index1+":::"+index2); + indexOne[index1] = indexOne[index1] + 1; + indexTwo[index2] = indexTwo[index2] + 1; + } + public int singleWordWangJenkinsHash(Object k) { + int h = k.hashCode(); + + h += (h << 15) ^ 0xffffcd7d;//左移 15位 相当于h 乘以2~15次方 再异或0xffffcd7d ?为啥要异或这个值 + h ^= (h >>> 10);//这行代码可以翻译为 h = h ^ (h >>> 10),即h 异或 h无符号右移10位的值 + h += (h << 3);//h = h + (h << 3),即h = h 加上 h左移3位 + h ^= (h >>> 6);//h = h ^ (h >>> 6) ,即 h 异或 h无符号右移6位的值 + h += (h << 2) + (h << 14); + return h ^ (h >>> 16); + } + public int hashNew(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } + final static char[] digits = {'0','1'}; + + static String toBinaryString(int i) { + char[] buf = new char[32]; + int pos = 32; + int mask = 1; + do { + buf[--pos] = digits[i & mask]; + i >>>= 1; + } while (pos > 0); + return new String(buf, pos, 32); + } + public void test(Object key1,Object key2){ + int h; + int i; + int s = (key1 == null) ? 0 : (h = key1.hashCode()) ^ (h >>> 16); + int ss = (key2 == null) ? 0 : (i = key2.hashCode()) ^ (i >>> 16); + + Log.e("hero","模拟计算key--"+(s & 15)+"---"+(ss & 15)); + } + public void test(Object k1,Object k2,Object k3){ + Log.e("hero","模拟计算key--"+(k1.hashCode() % 16)+"---"+(k2.hashCode() % 16)+"---"+(k3.hashCode() % 16)); } + private static AtomicInteger nextHashCode = + new AtomicInteger(); + private void test() { + int HASH_INCREMENT = 0x61c88647; + + List ss = new ArrayList<>(); + for (int i = 0; i < 32; i++) { + int andAdd = nextHashCode.getAndAdd(HASH_INCREMENT); + ss.add(andAdd); + } + + for (int i = 0; i < 16; i++) { + Integer integer = ss.get(i * 2); + int i1 = integer & 15; +// Log.e("hero","-----递增的值--==="+integer+"---计算后的值==="+i1); + } + } @Override public void onClick(View v) { switch (v.getId()){ @@ -36,6 +155,22 @@ public void onClick(View v) { case R.id.audio_activity: startActivity(new Intent(MainActivity.this , AudioEditorActivity.class)); break; + case R.id.video_connect: + startActivity(new Intent(MainActivity.this , MediaSelectVideoActivity.class)); + break; + } + } + + public void test(int[] arrs,int target){ + SparseIntArray da = new SparseIntArray(); + for (int i = 0; i < arrs.length; i++) { + da.append(arrs[i],i); + } + for (int i = 0; i < arrs.length; i++) { + int count = target - arrs[i]; + if (da.get(count) >= 0 && da.get(count) > i){ + Log.e("hero","---第一个值是:"+i+"----第二个值是:"+da.get(count)); + } } } } diff --git a/app/src/main/java/com/example/cj/videoeditor/activity/MediaSelectVideoActivity.java b/app/src/main/java/com/example/cj/videoeditor/activity/MediaSelectVideoActivity.java new file mode 100644 index 0000000..a32f28e --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/activity/MediaSelectVideoActivity.java @@ -0,0 +1,147 @@ +package com.example.cj.videoeditor.activity; + +import android.app.Activity; +import android.app.LoaderManager; +import android.content.CursorLoader; +import android.content.Intent; +import android.content.Loader; +import android.database.Cursor; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.view.View; +import android.widget.GridView; +import android.widget.TextView; +import android.widget.Toast; + +import com.bumptech.glide.Glide; +import com.example.cj.videoeditor.R; +import com.example.cj.videoeditor.adapter.VideoSelectAdapter; +import com.example.cj.videoeditor.bean.CutBean; +import com.example.cj.videoeditor.widget.TitleView; + +import java.util.ArrayList; +import java.util.List; + +/** + * 选择视频 + */ +public class MediaSelectVideoActivity extends Activity implements LoaderManager.LoaderCallbacks, VideoSelectAdapter.videoOnSelectChangedListener, View.OnClickListener { + GridView gridview; + TitleView title; + + TextView tv_title_bar_right_text; // 右边文字 + + VideoSelectAdapter mMediaAdapter; + + public static final String PROJECT_VIDEO = MediaStore.MediaColumns._ID; + + + int max_size = -1;// 最大size + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_media_select_video); + gridview = (GridView) findViewById(R.id.gridview_media_video); + title = (TitleView) findViewById(R.id.title_media_video); + tv_title_bar_right_text = (TextView) findViewById(R.id.tv_title_bar_right_text); + tv_title_bar_right_text.setOnClickListener(this); + initView(); + initData(); + + } + + private void initView() { + tv_title_bar_right_text.setTextColor(getResources().getColor(R.color.iTextColor5)); + } + + private void initData() { + max_size = getIntent().getIntExtra("max_size", -1); + + getLoaderManager().initLoader(0, null, this); + title.setBtnLeftOnClick(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + Uri videoUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + String order = MediaStore.MediaColumns.DATE_ADDED + " DESC"; + return new CursorLoader(getApplicationContext(), videoUri, new String[]{MediaStore.Video.Media.DATA, PROJECT_VIDEO}, null, null, order); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + + if (data == null || data.getCount() <= 0) { + return; + } + if (mMediaAdapter == null) { + + mMediaAdapter = new VideoSelectAdapter(getApplicationContext(), data); + mMediaAdapter.setMediaSelectVideoActivity(this); + mMediaAdapter.setOnSelectChangedListener(this); + mMediaAdapter.setMaxSize(max_size); + } else { + mMediaAdapter.swapCursor(data); + } + + + if (gridview.getAdapter() == null) { + gridview.setAdapter(mMediaAdapter); + } + mMediaAdapter.notifyDataSetChanged(); + } + + @Override + public void onLoaderReset(Loader loader) { + if (mMediaAdapter != null) + mMediaAdapter.swapCursor(null); + } + + @Override + protected void onDestroy() { + getLoaderManager().destroyLoader(0); + Glide.get(this).clearMemory(); + super.onDestroy(); + } + + List pathList;//临时保存 + List coverList; + + @Override + public void changed(List pathList, List coverList) { + if (pathList.isEmpty() && coverList.isEmpty()) { + tv_title_bar_right_text.setClickable(false); + tv_title_bar_right_text.setTextColor(getResources().getColor(R.color.iTextColor5)); + title.setTvRightNumVisibile(View.INVISIBLE); + } else { + tv_title_bar_right_text.setClickable(true); + tv_title_bar_right_text.setTextColor(getResources().getColor(R.color.iTextColor6)); + this.pathList = pathList; + this.coverList = coverList; + title.setTvRightNum(pathList.size() + ""); + } + + } + + // 提交 + @Override + public void onClick(View v) { + if ((pathList == null || pathList.isEmpty()) && (coverList == null || coverList.isEmpty())) { + Toast.makeText(this, "不选视频是要怎样", Toast.LENGTH_SHORT).show(); + return; + } + + Intent intent = new Intent(this, VideoConnectActivity.class); + intent.putStringArrayListExtra("path", (ArrayList) pathList); + startActivity(intent); + finish(); + } +} diff --git a/app/src/main/java/com/example/cj/videoeditor/activity/PreviewActivity.java b/app/src/main/java/com/example/cj/videoeditor/activity/PreviewActivity.java index dd8f5a8..41ab851 100644 --- a/app/src/main/java/com/example/cj/videoeditor/activity/PreviewActivity.java +++ b/app/src/main/java/com/example/cj/videoeditor/activity/PreviewActivity.java @@ -6,6 +6,7 @@ import android.os.Handler; import android.os.Message; import android.support.annotation.Nullable; +import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; @@ -186,6 +187,7 @@ public void onFinish() { } }); try { + Log.e("hero","-----PreviewActivity---clipVideo"); clipper.clipVideo(0,mVideoView.getVideoDuration()*1000); } catch (IOException e) { e.printStackTrace(); diff --git a/app/src/main/java/com/example/cj/videoeditor/activity/VideoConnectActivity.java b/app/src/main/java/com/example/cj/videoeditor/activity/VideoConnectActivity.java new file mode 100644 index 0000000..258fc72 --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/activity/VideoConnectActivity.java @@ -0,0 +1,192 @@ +package com.example.cj.videoeditor.activity; + +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.example.cj.videoeditor.Constants; +import com.example.cj.videoeditor.R; +import com.example.cj.videoeditor.bean.AudioSettingInfo; +import com.example.cj.videoeditor.bean.CutBean; +import com.example.cj.videoeditor.gpufilter.SlideGpuFilterGroup; +import com.example.cj.videoeditor.gpufilter.helper.MagicFilterType; +import com.example.cj.videoeditor.media.MediaPlayerWrapper; +import com.example.cj.videoeditor.media.VideoInfo; +import com.example.cj.videoeditor.mediacodec.MediaMuxerRunnable; +import com.example.cj.videoeditor.utils.TimeFormatUtils; +import com.example.cj.videoeditor.widget.VideoPreviewView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Created by cj on 2018/1/2. + * desc + */ + +public class VideoConnectActivity extends BaseActivity implements View.OnClickListener, MediaPlayerWrapper.IMediaCallback, SlideGpuFilterGroup.OnFilterChangeListener { + private VideoPreviewView mPreviewView; + List inputPath; + ExecutorService pool; + AudioSettingInfo settingInfo; + + static final int ENCODE_START = 0; + static final int ENCODE_FINISH = 1; + static final int UPDATE_SEEKBAR = 2; + static final int VIDEO_PREPARE = 3; + static final int VIDEO_START = 4; + static final int VIDEO_PAUSE = 5; + static final int VIDEO_CHANGED = 6; + static final int VIDEO_COMPLETION = 7; + + private String outputPath; + + private MagicFilterType currentFilterType; + + Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case ENCODE_START: + break; + case ENCODE_FINISH: + endLoading(); + Toast.makeText(VideoConnectActivity.this, "Finish Encode path=" + outputPath, Toast.LENGTH_SHORT).show(); + break; + case UPDATE_SEEKBAR: + break; + case VIDEO_PREPARE: + + break; + case VIDEO_START: + break; + case VIDEO_PAUSE: + break; + case VIDEO_CHANGED: + break; + case VIDEO_COMPLETION: + break; + } + } + }; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_connect); + initView(); + initData(); + } + + private void initData() { + inputPath = getIntent().getStringArrayListExtra("path"); + + mPreviewView.setVideoPath(inputPath); + mPreviewView.setIMediaCallback(this); + mPreviewView.setOnFilterChangeListener(this); + mPreviewView.clearWaterMark();//设置不显示水印 + + pool = Executors.newCachedThreadPool(); + settingInfo = new AudioSettingInfo(); + settingInfo.volFirst = 1; + settingInfo.volSecond = 1; + } + + private void initView() { + mPreviewView = (VideoPreviewView) findViewById(R.id.connect_video_view); + findViewById(R.id.iv_back).setOnClickListener(this); + findViewById(R.id.iv_confirm).setOnClickListener(this); + } + + + @Override + protected void onPause() { + super.onPause(); + mPreviewView.pause(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mPreviewView.onDestroy(); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.iv_back: + finish(); + break; + case R.id.iv_confirm: + mPreviewView.pause(); + showLoading("视频编辑中"); + + pool.execute(new Runnable() { + @Override + public void run() { + outputPath = Constants.getPath("output/", System.currentTimeMillis() + ".mp4"); + final long startTime = System.currentTimeMillis(); + MediaMuxerRunnable instance = new MediaMuxerRunnable(); + instance.setVideoInfo(mPreviewView.getVideoInfo(), outputPath); + instance.setAudioSetting(settingInfo); + instance.setFilterType(currentFilterType); + instance.addMuxerListener(new MediaMuxerRunnable.MuxerListener() { + @Override + public void onStart() { + Log.e("hero", "===muxer onStart===="); + } + + @Override + public void onFinish() { + Log.e("hero", "===muxer onFinish===="); + long endTime = System.currentTimeMillis(); + Log.e("timee", "---视频编辑消耗的时间===" + (endTime - startTime)); + mHandler.sendEmptyMessage(1); + } + }); + instance.start(); + } + }); + break; + } + } + + @Override + public void onVideoPrepare() { + + } + + @Override + public void onVideoStart() { + + } + + @Override + public void onVideoPause() { + + } + + @Override + public void onCompletion(MediaPlayer mp) { + + } + + @Override + public void onVideoChanged(VideoInfo info) { + + } + + @Override + public void onFilterChange(MagicFilterType type) { + currentFilterType = type; + } +} diff --git a/app/src/main/java/com/example/cj/videoeditor/activity/VideoSelectActivity.java b/app/src/main/java/com/example/cj/videoeditor/activity/VideoSelectActivity.java index b80dd04..3caf8dc 100644 --- a/app/src/main/java/com/example/cj/videoeditor/activity/VideoSelectActivity.java +++ b/app/src/main/java/com/example/cj/videoeditor/activity/VideoSelectActivity.java @@ -143,7 +143,7 @@ public void onSelect(final String path, String cover) { mDialog.setPositiveButton("加滤镜", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - //跳转预览界面 TODO + //跳转预览界面 if(!TextUtils.isEmpty(path)){ Intent intent=new Intent(VideoSelectActivity.this,PreviewActivity.class); intent.putExtra("path",path); diff --git a/app/src/main/java/com/example/cj/videoeditor/adapter/VideoSelectAdapter.java b/app/src/main/java/com/example/cj/videoeditor/adapter/VideoSelectAdapter.java new file mode 100644 index 0000000..295a0c1 --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/adapter/VideoSelectAdapter.java @@ -0,0 +1,152 @@ +package com.example.cj.videoeditor.adapter; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ImageView; +import android.widget.Toast; + +import com.bumptech.glide.Glide; +import com.example.cj.videoeditor.MyApplication; +import com.example.cj.videoeditor.R; +import com.example.cj.videoeditor.activity.MediaSelectVideoActivity; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * 本地视频列表 + */ +public class VideoSelectAdapter extends CursorAdapter { + MediaSelectVideoActivity activity; + + List coverList=new ArrayList(); + List pathList=new ArrayList(); + + int maxSize = -1; // 最大size + + public VideoSelectAdapter(Context context, Cursor c) { + super(context, c); + } + + public VideoSelectAdapter(Context context, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + } + + public VideoSelectAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + } + + public void setMediaSelectVideoActivity(MediaSelectVideoActivity activity) { + this.activity = activity; + } + + public void setMaxSize(int maxSize) { + this.maxSize = maxSize; + } + + + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + ViewHolder holder = new ViewHolder(); + View inflate = View.inflate(context, R.layout.item_media_video, null); + holder.pic = (ImageView) inflate.findViewById(R.id.iv_media_video); + holder.is_true = (ImageView) inflate.findViewById(R.id.is_true); + inflate.setTag(holder); + return inflate; + } + + @Override + public void bindView(View view, final Context context, Cursor cursor) { + final ViewHolder holder = (ViewHolder) view.getTag(); + final Uri uri = getUri(cursor); + final String path = cursor.getString(cursor + .getColumnIndex(MediaStore.Video.Media.DATA)); + + holder.is_true.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 判断是否超过大小 (粉丝发的大小有限制) + if (isToBigger(path)) { + Toast.makeText(MyApplication.getContext(),"选择的视频不能超过" + maxSize + "M~",Toast.LENGTH_SHORT).show(); + return; + } + + if (coverList.contains(uri.toString()) && pathList.contains(path)) { + coverList.remove(uri.toString()); + pathList.remove(path); + holder.is_true.setImageResource(R.mipmap.icon_choice_nor); + } else { +// if (TextUtils.isEmpty(tempCover) && TextUtils.isEmpty(tempPath)) { +// tempCover = uri.toString(); +// tempPath = path; +// holder.is_true.setImageResource(R.mipmap.icon_choice_selected); +// } else { +// Toast.makeText(context,context.getString(R.string.video_select),Toast.LENGTH_SHORT).show(); +// } + coverList.add(uri.toString()); + pathList.add(path); + holder.is_true.setImageResource(R.mipmap.icon_choice_selected); + } + if (listener != null) { + listener.changed(pathList, coverList); + } + } + }); + holder.is_true.setImageResource(pathList.contains(path) ? R.mipmap.icon_choice_selected : R.mipmap.icon_choice_nor); + + Glide.with(context) + .load(uri) + .placeholder(R.mipmap.editor_img_def_video) + .error(R.mipmap.editor_img_def_video) + .crossFade() + .into(holder.pic); + } + + public Uri getUri(Cursor cursor) { + String id = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); + return Uri.withAppendedPath(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id); + } + @Override + public Object getItem(int position) { + return super.getItem(position); + } + + class ViewHolder { + public ImageView pic; + public ImageView is_true; + } + + public void setOnSelectChangedListener(videoOnSelectChangedListener listener) { + this.listener = listener; + } + + videoOnSelectChangedListener listener; + + public interface videoOnSelectChangedListener { + void changed(List pathList, List coverList); + } + + + private boolean isToBigger(String path) { + if (maxSize == -1) { //没有限制 + return false; + } + try { + File file = new File(path); + long length = file.length(); + return length > maxSize * 1024 * 1024; + } catch (Exception e) { + return false; + } + + + } + +} diff --git a/app/src/main/java/com/example/cj/videoeditor/bean/AudioSettingInfo.java b/app/src/main/java/com/example/cj/videoeditor/bean/AudioSettingInfo.java new file mode 100644 index 0000000..40b3a4a --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/bean/AudioSettingInfo.java @@ -0,0 +1,13 @@ +package com.example.cj.videoeditor.bean; + +/** + * Created by cj on 2017/6/28. + * desc + */ + +public class AudioSettingInfo { + public boolean isSet;//是否设置过 + public String filePath;//bgm的文件路径 + public int volFirst;//原生音量大小 + public int volSecond;//bgm大小 +} diff --git a/app/src/main/java/com/example/cj/videoeditor/bean/CutBean.java b/app/src/main/java/com/example/cj/videoeditor/bean/CutBean.java new file mode 100644 index 0000000..9cee221 --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/bean/CutBean.java @@ -0,0 +1,56 @@ +package com.example.cj.videoeditor.bean; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Created by cj on 2017/7/10. + * desc 以video的path和position为关键key + * 记录 当前视频剪切的选择参数 + * 便于后面剪切视频 + */ + +public class CutBean implements Parcelable { + public int position;//这个选择参数 在当前视频集里的位置 + public String videoPath; + public long startPoint;//开始剪切的时间点 + public long cutDuration;//剪切的时长 + public long videoDuration;//video的总长度 + public CutBean(){ + + } + + protected CutBean(Parcel in) { + position = in.readInt(); + videoPath = in.readString(); + startPoint = in.readLong(); + cutDuration = in.readLong(); + videoDuration = in.readLong(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public CutBean createFromParcel(Parcel in) { + return new CutBean(in); + } + + @Override + public CutBean[] newArray(int size) { + return new CutBean[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(position); + dest.writeString(videoPath); + dest.writeLong(startPoint); + dest.writeLong(cutDuration); + dest.writeLong(videoDuration); + } +} diff --git a/app/src/main/java/com/example/cj/videoeditor/bean/MediaDecode.java b/app/src/main/java/com/example/cj/videoeditor/bean/MediaDecode.java new file mode 100644 index 0000000..bc4266c --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/bean/MediaDecode.java @@ -0,0 +1,18 @@ +package com.example.cj.videoeditor.bean; + +import android.media.MediaExtractor; + +/** + * Created by cj on 2017/7/11. + * desc 音频解码的info类 包含了音频path 音频的MediaExtractor + * 和本段音频的截取点cutPoint + * 以及剪切时长 cutDuration + */ + +public class MediaDecode { + public String path; + public MediaExtractor extractor; + public int cutPoint; + public int cutDuration; + public int duration; +} diff --git a/app/src/main/java/com/example/cj/videoeditor/drawer/TextureRender.java b/app/src/main/java/com/example/cj/videoeditor/drawer/TextureRender.java new file mode 100644 index 0000000..03da2bf --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/drawer/TextureRender.java @@ -0,0 +1,396 @@ +package com.example.cj.videoeditor.drawer; + +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.res.Resources; +import android.graphics.BitmapFactory; +import android.graphics.SurfaceTexture; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.Matrix; +import android.util.Log; + + +import com.example.cj.videoeditor.MyApplication; +import com.example.cj.videoeditor.R; +import com.example.cj.videoeditor.filter.AFilter; +import com.example.cj.videoeditor.filter.GroupFilter; +import com.example.cj.videoeditor.filter.NoFilter; +import com.example.cj.videoeditor.filter.RotationOESFilter; +import com.example.cj.videoeditor.filter.WaterMarkFilter; +import com.example.cj.videoeditor.gpufilter.basefilter.GPUImageFilter; +import com.example.cj.videoeditor.media.VideoInfo; +import com.example.cj.videoeditor.utils.EasyGlUtils; +import com.example.cj.videoeditor.utils.MatrixUtils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +/** + * Code for rendering a texture onto a surface using OpenGL ES 2.0. + */ +public class TextureRender { + private static final String TAG = "TextureRender"; + private static final int FLOAT_SIZE_BYTES = 4; + private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES; + private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; + private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; + // private final float[] mTriangleVerticesData = {//原来的纹理坐标体系是错误的 +// // X, Y, Z, U, V +// -1.0f, -1.0f, 0, 0.f, 0.f, +// 1.0f, -1.0f, 0, 1.f, 0.f, +// -1.0f, 1.0f, 0, 0.f, 1.f, +// 1.0f, 1.0f, 0, 1.f, 1.f, +// }; + private final float[] mTriangleVerticesData = { + // X, Y, Z, U, V + -1.0f, -1.0f, 0, 0.f, 1.f, + 1.0f, -1.0f, 0, 1.f, 1.f, + -1.0f, 1.0f, 0, 0.f, 0.f, + 1.0f, 1.0f, 0, 1.f, 0.f, + }; + private FloatBuffer mTriangleVertices; + private static final String VERTEX_SHADER = + "uniform mat4 uMVPMatrix;\n" + + "uniform mat4 uSTMatrix;\n" + + "attribute vec4 aPosition;\n" + + "attribute vec4 aTextureCoord;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " gl_Position = uMVPMatrix * aPosition;\n" + + " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" + + "}\n"; + private static final String FRAGMENT_SHADER = + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + // highp here doesn't seem to matter + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + + "}\n"; + private float[] mMVPMatrix = new float[16]; + private float[] mSTMatrix = new float[16]; + private int mProgram; + private int mTextureID = -12345; + private int muMVPMatrixHandle; + private int muSTMatrixHandle; + private int maPositionHandle; + private int maTextureHandle; + + //======================clip======================== + float[] SM = new float[16]; //用于显示的变换矩阵 + boolean isClipMode; + int clipViewWidth; + int clipViewHeight; + int clipEncodeWidth; + int clipEncodeHeight; + + + //======================zoom======================== + //创建帧缓冲区 + private int[] fFrame = new int[1]; + private int[] fTexture = new int[2]; + AFilter mShow; + RotationOESFilter rotationFilter; + GPUImageFilter mGpuFilter; + GroupFilter mBeFilter; + //第一段视频宽高(旋转后) + int viewWidth; + int viewHeight; + //当前视频宽高(旋转后) + int videoWidth; + int videoHeight; + //最终显示的宽高 + int width; + int height; + int x; + int y; + boolean videoChanged = false; + //第一段视频信息 + VideoInfo info; + /** + * 用于后台绘制的变换矩阵 + */ + private float[] OM; + + public TextureRender(VideoInfo info) { + this.info=info; + mTriangleVertices = ByteBuffer.allocateDirect( + mTriangleVerticesData.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + mTriangleVertices.put(mTriangleVerticesData).position(0); + Matrix.setIdentityM(mSTMatrix, 0); + Resources resources = MyApplication.getContext().getResources(); + mShow = new NoFilter(resources); + mShow.setMatrix(MatrixUtils.flip(MatrixUtils.getOriginalMatrix(), false, true)); + rotationFilter = new RotationOESFilter(resources); + mBeFilter = new GroupFilter(resources); + //默认加上水印 可以取消掉 + WaterMarkFilter waterMarkFilter = new WaterMarkFilter(resources); + waterMarkFilter.setWaterMark(BitmapFactory.decodeResource(resources, R.mipmap.watermark)); + + waterMarkFilter.setPosition(0, 70, 0, 0); + mBeFilter.addFilter(waterMarkFilter); + OM = MatrixUtils.getOriginalMatrix(); + MatrixUtils.flip(OM, false, false);//矩阵上下翻转 + mBeFilter.setMatrix(OM); + } + + public int getTextureId() { + return mTextureID; + } + + public void drawFrame(SurfaceTexture st) { + zoomDraw(st); + } + + public void zoomDraw(SurfaceTexture st){ + EasyGlUtils.bindFrameTexture(fFrame[0], fTexture[0]); + GLES20.glViewport(0, 0, viewWidth, viewHeight); + rotationFilter.draw(); + EasyGlUtils.unBindFrameBuffer(); + + mBeFilter.setTextureId(fTexture[0]); + mBeFilter.draw(); + + if (mGpuFilter != null) { + EasyGlUtils.bindFrameTexture(fFrame[0], fTexture[1]); + mGpuFilter.onDrawFrame(mBeFilter.getOutputTexture()); + EasyGlUtils.unBindFrameBuffer(); + } + + if (videoChanged) { + GLES20.glViewport(x, y, width, height); + } + + mShow.setTextureId(fTexture[mGpuFilter == null ? 0 : 1]); + mShow.draw(); + GLES20.glFinish(); + } + + public void preDraw(SurfaceTexture st) { + checkGlError("onDrawFrame start"); +// st.getTransformMatrix(mSTMatrix);//当视频角度不为0时,这里会导致视频方向不对 + GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glUseProgram(mProgram); + checkGlError("glUseProgram"); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); + mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); + GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); + checkGlError("glVertexAttribPointer maPosition"); + GLES20.glEnableVertexAttribArray(maPositionHandle); + checkGlError("glEnableVertexAttribArray maPositionHandle"); + mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); + GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); + checkGlError("glVertexAttribPointer maTextureHandle"); + GLES20.glEnableVertexAttribArray(maTextureHandle); + checkGlError("glEnableVertexAttribArray maTextureHandle"); + Matrix.setIdentityM(mMVPMatrix, 0); + GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0); + GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkGlError("glDrawArrays"); + } + + /** + * Initializes GL state. Call this after the EGL surface has been created and made current. + */ + public void surfaceCreated() { + mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER); + if (mProgram == 0) { + throw new RuntimeException("failed creating program"); + } + maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); + checkGlError("glGetAttribLocation aPosition"); + if (maPositionHandle == -1) { + throw new RuntimeException("Could not get attrib location for aPosition"); + } + maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord"); + checkGlError("glGetAttribLocation aTextureCoord"); + if (maTextureHandle == -1) { + throw new RuntimeException("Could not get attrib location for aTextureCoord"); + } + muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); + checkGlError("glGetUniformLocation uMVPMatrix"); + if (muMVPMatrixHandle == -1) { + throw new RuntimeException("Could not get attrib location for uMVPMatrix"); + } + muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix"); + checkGlError("glGetUniformLocation uSTMatrix"); + if (muSTMatrixHandle == -1) { + throw new RuntimeException("Could not get attrib location for uSTMatrix"); + } + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + mTextureID = textures[0]; + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); + checkGlError("glBindTexture mTextureID"); + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_LINEAR);//改为线性过滤,是画面更加平滑(抗锯齿) + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + checkGlError("glTexParameter"); + + mShow.create(); + rotationFilter.create(); + rotationFilter.setTextureId(mTextureID); + + mBeFilter.create(); + GLES20.glGenFramebuffers(1, fFrame, 0); + + if (info.rotation == 0 || info.rotation == 180) { + EasyGlUtils.genTexturesWithParameter(2, fTexture, 0, GLES20.GL_RGBA, info.width, info.height); + viewWidth = info.width; + viewHeight = info.height; + } else { + EasyGlUtils.genTexturesWithParameter(2, fTexture, 0, GLES20.GL_RGBA, info.height, info.width); + viewWidth = info.height; + viewHeight = info.width; + } + + rotationFilter.setRotation(info.rotation); + } + + /** + * Replaces the fragment shader. + */ + public void changeFragmentShader(String fragmentShader) { + GLES20.glDeleteProgram(mProgram); + mProgram = createProgram(VERTEX_SHADER, fragmentShader); + if (mProgram == 0) { + throw new RuntimeException("failed creating program"); + } + } + + private int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + checkGlError("glCreateShader type=" + shaderType); + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + Log.e(TAG, "Could not compile shader " + shaderType + ":"); + Log.e(TAG, " " + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + } + return shader; + } + + private int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + return 0; + } + int program = GLES20.glCreateProgram(); + checkGlError("glCreateProgram"); + if (program == 0) { + Log.e(TAG, "Could not create program"); + } + GLES20.glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + Log.e(TAG, "Could not link program: "); + Log.e(TAG, GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + program = 0; + } + return program; + } + + public void checkGlError(String op) { + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + Log.e(TAG, op + ": glError " + error); + throw new RuntimeException(op + ": glError " + error); + } + } + + public void addGpuFilter(GPUImageFilter filter) { + if (mGpuFilter != null) { + mGpuFilter.destroy(); + } + mGpuFilter = filter; + if (filter != null) { + mGpuFilter.init(); + mGpuFilter.onDisplaySizeChanged(info.width, info.height); + mGpuFilter.onInputSizeChanged(info.width, info.height); + } + } + + public void onVideoSizeChanged(VideoInfo info) { + + setVideoWidthAndHeight(info); + adjustVideoPosition(); + videoChanged = true; + mBeFilter.setSize(viewWidth, viewHeight); + } + + public void setVideoWidthAndHeight(VideoInfo info) { + rotationFilter.setRotation(info.rotation); + if (info.rotation == 0 || info.rotation == 180) { + this.videoWidth = info.width; + this.videoHeight = info.height; + } else { + this.videoWidth = info.height; + this.videoHeight = info.width; + } + } + + private void adjustVideoPosition() { + float w = (float) viewWidth / videoWidth; + float h = (float) viewHeight / videoHeight; + if (w < h) { + width = viewWidth; + height = (int) ((float) videoHeight * w); + } else { + width = (int) ((float) videoWidth * h); + height = viewHeight; + } + x = (viewWidth - width) / 2; + y = (viewHeight - height) / 2; + } + + /** + * 清除掉水印 + * */ + public void clearWaterMark(){ + if (mBeFilter != null){ + mBeFilter.clearAll(); + mShow.setMatrix(OM); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/cj/videoeditor/drawer/VideoDrawer.java b/app/src/main/java/com/example/cj/videoeditor/drawer/VideoDrawer.java index 275e6d3..3f27716 100644 --- a/app/src/main/java/com/example/cj/videoeditor/drawer/VideoDrawer.java +++ b/app/src/main/java/com/example/cj/videoeditor/drawer/VideoDrawer.java @@ -7,6 +7,7 @@ import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.opengl.GLSurfaceView; +import android.util.Log; import android.view.MotionEvent; import com.example.cj.videoeditor.R; @@ -32,64 +33,106 @@ */ public class VideoDrawer implements GLSurfaceView.Renderer { - /**用于后台绘制的变换矩阵*/ + /** + * 用于后台绘制的变换矩阵 + */ private float[] OM; - /**用于显示的变换矩阵*/ + /** + * 用于显示的变换矩阵 + */ private float[] SM = new float[16]; private SurfaceTexture surfaceTexture; - /**可选择画面的滤镜*/ + /** + * 可选择画面的滤镜 + */ private RotationOESFilter mPreFilter; - /**显示的滤镜*/ + /** + * 显示的滤镜 + */ private AFilter mShow; - /**美白的filter*/ + /** + * 美白的filter + */ private MagicBeautyFilter mBeautyFilter; private AFilter mProcessFilter; - /**绘制水印的滤镜*/ + /** + * 绘制水印的滤镜 + */ private final GroupFilter mBeFilter; - /**多种滤镜切换*/ + /** + * 多种滤镜切换 + */ private SlideGpuFilterGroup mSlideFilterGroup; - /**绘制其他样式的滤镜*/ + /** + * 绘制其他样式的滤镜 + */ private GPUImageFilter mGroupFilter; - /**控件的长宽*/ + /** + * 控件的长宽 + */ private int viewWidth; private int viewHeight; + /** + * 视频的长宽 + */ + private int videoWidth; + private int videoHeight; + /** + * 第一个视频的宽高 + */ + private int firstVideoRealWidth=-1; + private int firstVideoRealHeight=-1; - /**创建离屏buffer*/ + /** + * 投影的位置和大小 + * */ + private int x; + private int y; + private int width; + private int height; + + /** + * 创建离屏buffer + */ private int[] fFrame = new int[1]; private int[] fTexture = new int[1]; - /**用于视频旋转的参数*/ + /** + * 用于视频旋转的参数 + */ private int rotation; - /**是否开启美颜*/ + /** + * 是否开启美颜 + */ private boolean isBeauty = false; - public VideoDrawer(Context context,Resources res){ + public VideoDrawer(Context context, Resources res) { mPreFilter = new RotationOESFilter(res);//旋转相机操作 mShow = new NoFilter(res); mBeFilter = new GroupFilter(res); mBeautyFilter = new MagicBeautyFilter(); - mProcessFilter=new ProcessFilter(res); + mProcessFilter = new ProcessFilter(res); mSlideFilterGroup = new SlideGpuFilterGroup(); OM = MatrixUtils.getOriginalMatrix(); - MatrixUtils.flip(OM,false,true);//矩阵上下翻转 + MatrixUtils.flip(OM, false, true);//矩阵上下翻转 // mShow.setMatrix(OM); WaterMarkFilter waterMarkFilter = new WaterMarkFilter(res); waterMarkFilter.setWaterMark(BitmapFactory.decodeResource(res, R.mipmap.watermark)); - waterMarkFilter.setPosition(0,70,0,0); + waterMarkFilter.setPosition(0, 70, 0, 0); mBeFilter.addFilter(waterMarkFilter); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { - int texture[]=new int[1]; - GLES20.glGenTextures(1,texture,0); - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES ,texture[0]); + int texture[] = new int[1]; + GLES20.glGenTextures(1, texture, 0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, @@ -105,102 +148,140 @@ public void onSurfaceCreated(GL10 gl, EGLConfig config) { mBeautyFilter.setBeautyLevel(3);//默认设置3级的美颜 mSlideFilterGroup.init(); } - public void onVideoChanged(VideoInfo info){ + + public void onVideoChanged(VideoInfo info) { setRotation(info.rotation); - if(info.rotation==0||info.rotation==180){ - MatrixUtils.getShowMatrix(SM,info.width,info.height,viewWidth,viewHeight); - }else{ - MatrixUtils.getShowMatrix(SM,info.height,info.width,viewWidth,viewHeight); + if (info.rotation == 0 || info.rotation == 180) { + this.videoWidth = info.width; + this.videoHeight = info.height; + } else { + this.videoWidth = videoHeight; + this.videoHeight = videoWidth; } + adjustVideoPosition(); + } - mPreFilter.setMatrix(SM); + private void adjustVideoPosition() { + if (firstVideoRealWidth == -1 && firstVideoRealHeight == -1) { + float w = (float) viewWidth / videoWidth; + float h = (float) viewHeight / videoHeight; + if (w < h) { + width = viewWidth; + height = (int) ((float) videoHeight * w); + } else { + width = (int) ((float) videoWidth * h); + height = viewHeight; + } + x = (viewWidth - width) / 2; + y = (viewHeight - height) / 2; + firstVideoRealWidth = width; + firstVideoRealHeight = height; + } else { + float w = (float) firstVideoRealWidth / videoWidth; + float h = (float) firstVideoRealHeight / videoHeight; + if (w < h) { + width = firstVideoRealWidth; + height = (int) ((float) videoHeight * w); + } else { + width = (int) ((float) videoWidth * h); + height = firstVideoRealHeight; + } + x = (viewWidth - firstVideoRealWidth) / 2 + (firstVideoRealWidth - width) / 2; + y = (viewHeight - firstVideoRealHeight) / 2 + (firstVideoRealHeight - height) / 2; + } } + @Override public void onSurfaceChanged(GL10 gl, int width, int height) { - viewWidth=width; - viewHeight=height; + viewWidth = width; + viewHeight = height; GLES20.glDeleteFramebuffers(1, fFrame, 0); GLES20.glDeleteTextures(1, fTexture, 0); - GLES20.glGenFramebuffers(1,fFrame,0); - EasyGlUtils.genTexturesWithParameter(1,fTexture,0, GLES20.GL_RGBA,viewWidth,viewHeight); + GLES20.glGenFramebuffers(1, fFrame, 0); + EasyGlUtils.genTexturesWithParameter(1, fTexture, 0, GLES20.GL_RGBA, viewWidth, viewHeight); - mBeFilter.setSize(viewWidth,viewHeight); - mProcessFilter.setSize(viewWidth,viewHeight); - mBeautyFilter.onDisplaySizeChanged(viewWidth,viewHeight); - mBeautyFilter.onInputSizeChanged(viewWidth,viewHeight); - mSlideFilterGroup.onSizeChanged(viewWidth,viewHeight); + mBeFilter.setSize(viewWidth, viewHeight); + mProcessFilter.setSize(viewWidth, viewHeight); + mBeautyFilter.onDisplaySizeChanged(viewWidth, viewHeight); + mBeautyFilter.onInputSizeChanged(viewWidth, viewHeight); + mSlideFilterGroup.onSizeChanged(viewWidth, viewHeight); } @Override public void onDrawFrame(GL10 gl) { surfaceTexture.updateTexImage(); - EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]); - GLES20.glViewport(0,0,viewWidth,viewHeight); + EasyGlUtils.bindFrameTexture(fFrame[0], fTexture[0]); + GLES20.glViewport(0, 0, viewWidth, viewHeight); mPreFilter.draw(); EasyGlUtils.unBindFrameBuffer(); mBeFilter.setTextureId(fTexture[0]); mBeFilter.draw(); - if (mBeautyFilter != null && isBeauty && mBeautyFilter.getBeautyLevel() != 0){ - EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]); - GLES20.glViewport(0,0,viewWidth,viewHeight); + if (mBeautyFilter != null && isBeauty && mBeautyFilter.getBeautyLevel() != 0) { + EasyGlUtils.bindFrameTexture(fFrame[0], fTexture[0]); + GLES20.glViewport(0, 0, viewWidth, viewHeight); mBeautyFilter.onDrawFrame(mBeFilter.getOutputTexture()); EasyGlUtils.unBindFrameBuffer(); mProcessFilter.setTextureId(fTexture[0]); - }else { + } else { mProcessFilter.setTextureId(mBeFilter.getOutputTexture()); } mProcessFilter.draw(); mSlideFilterGroup.onDrawFrame(mProcessFilter.getOutputTexture()); - if (mGroupFilter != null){ - EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]); - GLES20.glViewport(0,0,viewWidth,viewHeight); + if (mGroupFilter != null) { + EasyGlUtils.bindFrameTexture(fFrame[0], fTexture[0]); + GLES20.glViewport(0, 0, viewWidth, viewHeight); mGroupFilter.onDrawFrame(mSlideFilterGroup.getOutputTexture()); EasyGlUtils.unBindFrameBuffer(); mProcessFilter.setTextureId(fTexture[0]); - }else { + } else { mProcessFilter.setTextureId(mSlideFilterGroup.getOutputTexture()); } mProcessFilter.draw(); - - GLES20.glViewport(0,0,viewWidth,viewHeight); - + GLES20.glViewport(x, y, width, height); mShow.setTextureId(mProcessFilter.getOutputTexture()); mShow.draw(); } - public SurfaceTexture getSurfaceTexture(){ + + public SurfaceTexture getSurfaceTexture() { return surfaceTexture; } - public void setRotation(int rotation){ - this.rotation=rotation; - if(mPreFilter!=null){ + public void setRotation(int rotation) { + this.rotation = rotation; + if (mPreFilter != null) { mPreFilter.setRotation(this.rotation); } } - /**切换开启美白效果*/ - public void switchBeauty(){ + + /** + * 切换开启美白效果 + */ + public void switchBeauty() { isBeauty = !isBeauty; } + /** * 是否开启美颜功能 - * */ - public void isOpenBeauty(boolean isBeauty){ + */ + public void isOpenBeauty(boolean isBeauty) { this.isBeauty = isBeauty; } + /** * 触摸事件监听 - * */ - public void onTouch(MotionEvent event){ + */ + public void onTouch(MotionEvent event) { mSlideFilterGroup.onTouchEvent(event); } + /** * 滤镜切换的监听 - * */ - public void setOnFilterChangeListener(SlideGpuFilterGroup.OnFilterChangeListener listener){ + */ + public void setOnFilterChangeListener(SlideGpuFilterGroup.OnFilterChangeListener listener) { mSlideFilterGroup.setOnFilterChangeListener(listener); } @@ -212,12 +293,20 @@ public void checkGlError(String s) { } public void setGpuFilter(GPUImageFilter filter) { - if (filter != null){ + if (filter != null) { mGroupFilter = filter; mGroupFilter.init(); mGroupFilter.onDisplaySizeChanged(viewWidth, viewWidth); - mGroupFilter.onInputSizeChanged(viewWidth,viewHeight); + mGroupFilter.onInputSizeChanged(viewWidth, viewHeight); + } + } + /** + * 清除掉水印 + * */ + public void clearWaterMark(){ + if (mBeFilter != null){ + mBeFilter.clearAll(); + mShow.setMatrix(OM); } - } } diff --git a/app/src/main/java/com/example/cj/videoeditor/jni/AudioJniUtils.java b/app/src/main/java/com/example/cj/videoeditor/jni/AudioJniUtils.java index 519db80..946329e 100644 --- a/app/src/main/java/com/example/cj/videoeditor/jni/AudioJniUtils.java +++ b/app/src/main/java/com/example/cj/videoeditor/jni/AudioJniUtils.java @@ -12,4 +12,8 @@ public class AudioJniUtils { System.loadLibrary("native-lib"); } public static native byte[] audioMix(byte[] sourceA,byte[] sourceB,byte[] dst,float firstVol , float secondVol); + + public static native String putString(String info); + + } diff --git a/app/src/main/java/com/example/cj/videoeditor/media/MediaPlayerWrapper.java b/app/src/main/java/com/example/cj/videoeditor/media/MediaPlayerWrapper.java index 4ef7c59..7a22da2 100644 --- a/app/src/main/java/com/example/cj/videoeditor/media/MediaPlayerWrapper.java +++ b/app/src/main/java/com/example/cj/videoeditor/media/MediaPlayerWrapper.java @@ -3,6 +3,7 @@ import android.media.AudioManager; import android.media.MediaMetadataRetriever; import android.media.MediaPlayer; +import android.util.Log; import android.view.Surface; import java.io.IOException; @@ -36,7 +37,7 @@ public void setOnCompletionListener(IMediaCallback callback) { /** * get video info and store * - * @param dataSource + * @param dataSource 视频播放的源文件 */ public void setDataSource(List dataSource) { this.mSrcList = dataSource; @@ -55,7 +56,6 @@ public void setDataSource(List dataSource) { info.width = Integer.parseInt(width); info.height = Integer.parseInt(height); info.duration = Integer.parseInt(duration); - mInfoList.add(info); } } diff --git a/app/src/main/java/com/example/cj/videoeditor/mediacodec/AudioCodec.java b/app/src/main/java/com/example/cj/videoeditor/mediacodec/AudioCodec.java index b2f8994..a36b73f 100644 --- a/app/src/main/java/com/example/cj/videoeditor/mediacodec/AudioCodec.java +++ b/app/src/main/java/com/example/cj/videoeditor/mediacodec/AudioCodec.java @@ -458,7 +458,12 @@ public static void PCM2AAC(String encodeType, String outputFile) throws IOExcept outBitSize=encodeBufferInfo.size; outPacketSize=outBitSize+7;//7为ADTS头部的大小 - outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer + if (Build.VERSION.SDK_INT >= 21){ + outputBuffer = mediaEncode.getOutputBuffer(outputIndex);//拿到输出Buffer + }else { + outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer + } + outputBuffer.position(encodeBufferInfo.offset); outputBuffer.limit(encodeBufferInfo.offset + outBitSize); chunkAudio = new byte[outPacketSize]; diff --git a/app/src/main/java/com/example/cj/videoeditor/mediacodec/AudioRunnable.java b/app/src/main/java/com/example/cj/videoeditor/mediacodec/AudioRunnable.java new file mode 100644 index 0000000..2e80da5 --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/mediacodec/AudioRunnable.java @@ -0,0 +1,784 @@ +package com.example.cj.videoeditor.mediacodec; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.media.MediaMetadataRetriever; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + + +import com.example.cj.videoeditor.Constants; +import com.example.cj.videoeditor.bean.AudioSettingInfo; +import com.example.cj.videoeditor.bean.MediaDecode; +import com.example.cj.videoeditor.media.VideoInfo; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by cj on 2017/6/30. + * desc 音频编解码线程 + */ +@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) +public class AudioRunnable extends Thread { + final int TIMEOUT_USEC = 0; + private AudioSettingInfo mSettingInfo;//混音设置 + private int audioTrackIndex = -1; + private MediaFormat audioMediaFormat; + private MediaMuxerRunnable mMediaMuxer; + private MediaDecode mAudioDecode; + private boolean mIsBgmLong; + + private List mVideoInfos;//多个音频的合并 + private List mTrackIndex = new ArrayList<>();//用于记录不同分离器的audio信道的index + private List mAudioDecodes = new ArrayList<>(); + + + public AudioRunnable(List inputFiles, AudioSettingInfo settingInfo, MediaMuxerRunnable mediaMuxer) { + this.mMediaMuxer = mediaMuxer; + mSettingInfo = settingInfo; + mVideoInfos = inputFiles; + } + + @Override + public void run() { + try { + prepare(); + if (mSettingInfo != null && !TextUtils.isEmpty(mSettingInfo.filePath)) { + //说明有混音操作 + mixAudioEditor(); + } else { + simpleAudioMix(); + } + + } catch (IOException e) { + e.printStackTrace(); + } + + } + + private void prepare() throws IOException { + if (mSettingInfo != null && !TextUtils.isEmpty(mSettingInfo.filePath)) {//判断是否bgm更长 + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + long audioDuration = 0; + for (int i = 0; i < mVideoInfos.size(); i++) { + retriever.setDataSource(mVideoInfos.get(i).path); + String videoDuration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + audioDuration += Long.parseLong(videoDuration); + } + + retriever.setDataSource(mSettingInfo.filePath); + String bgmDuration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + mIsBgmLong = audioDuration < Long.parseLong(bgmDuration); + } + for (int i = 0; i < mVideoInfos.size(); i++) { + //给每一个视频文件都创建一个MediaExtractor + MediaExtractor temp = new MediaExtractor(); + VideoInfo videoInfo = mVideoInfos.get(i); + temp.setDataSource(videoInfo.path); + + MediaDecode decode = new MediaDecode();//音频解码info + decode.extractor = temp; + decode.path = videoInfo.path; + decode.cutPoint = videoInfo.cutPoint; + decode.cutDuration = videoInfo.cutDuration; + decode.duration = videoInfo.duration; + mAudioDecodes.add(decode); + } + + for (int i = 0; i < mAudioDecodes.size(); i++) {//所有音频Extractor选择信道,并且记录信道 + MediaExtractor extractor = mAudioDecodes.get(i).extractor; + int trackCount = extractor.getTrackCount(); + for (int j = 0; j < trackCount; j++) { + MediaFormat trackFormat = extractor.getTrackFormat(j); + String mime = trackFormat.getString(MediaFormat.KEY_MIME); + + if (mime.startsWith("audio/")) { + extractor.selectTrack(j); + mTrackIndex.add(j); + break; + } + } + + } + } + + /** + * 音频—》PCM —》读取pcm文件,进行混音 + * 如果是多个音频和一个bgm进行混合 + * 1、将多个音频写入同一个pcm临时文件 + * 2、从音频的pcm文件和bgm的pcm文件读取数据 进行混音 + *

+ * 有一些可以优化的点 + * 1、根据bgm的长度来决定解码音频的长度 + * 2、bgm没有到达的点 就不进行解码 而选择直接写入 + */ + private void mixAudioEditor() { + //bgm初始化 + MediaExtractor extractor = new MediaExtractor(); + int audioTrack = -1; + boolean hasAudio = false; + try { + extractor.setDataSource(mSettingInfo.filePath); + for (int i = 0; i < extractor.getTrackCount(); i++) { + MediaFormat trackFormat = extractor.getTrackFormat(i); + String mime = trackFormat.getString(MediaFormat.KEY_MIME); + if (mime.startsWith("audio/")) { + audioTrack = i; + hasAudio = true; + break; + } + } + if (hasAudio) { + extractor.selectTrack(audioTrack); + //分别用两个线程去解码数据,然后解码出来的数据,用一个线程去编码数据,用两个list去保存解码后的数据,如果 + String path1 = Constants.getPath("video/temp/", "audio.pcm");//视频音乐 + String path2 = Constants.getPath("video/temp/", "bgm.pcm");//背景音 + final boolean[] audioDecodeIsOver = {false}; + final boolean[] bgmDecodeIsOver = {false}; + + //原始音频解码 + new Thread(new AudioRunnable.AudioDecodeRunnable(mAudioDecodes, mTrackIndex, path1, false, new AudioRunnable.DecodeOverListener() { + @Override + public void decodeIsOver() { + audioDecodeIsOver[0] = true; + } + })).start(); + + //bgm解码 + List bgmDecode = new ArrayList<>(); + MediaDecode decode = new MediaDecode(); + decode.extractor = extractor; + bgmDecode.add(decode); + + List bgmTrack = new ArrayList<>(); + bgmTrack.add(audioTrack); + + new Thread(new AudioRunnable.AudioDecodeRunnable(bgmDecode, bgmTrack, path2, true, new AudioRunnable.DecodeOverListener() { + @Override + public void decodeIsOver() { + bgmDecodeIsOver[0] = true; + } + })).start(); + + //开启一个编码线程,将解码后的数据进行混合编码之后,写入到MediaMuxer中 + while (true) { + Thread.sleep(50); + if (audioDecodeIsOver[0] && bgmDecodeIsOver[0]) { + break; + } + } + new Thread(new AudioRunnable.AudioMixAndEncodeRunnable(new String[]{path1, path2}, new AudioRunnable.EncodeListener() { + @Override + public void encodeIsOver() { + mMediaMuxer.audioIsOver(); + } + })).start(); + + + } else { + Log.e("hero", " select audio file has no auido track"); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + + //执行解码音频的代码块 + private AtomicBoolean bgmDecodeOver = new AtomicBoolean(false);//bgm解码完成 + private AtomicBoolean audioDecodeOver = new AtomicBoolean(false);//原声解码完成 + private AtomicInteger bgmCount = new AtomicInteger(0);//背景音pcm文件大小 byte + private AtomicInteger audioCount = new AtomicInteger(0);//原声pcm文件大小 byte + + private long default_time = 23219; + private List totalTime = new ArrayList<>();//每段音频的时长 + private List eachTime = new ArrayList<>();//每段音频的时间间隔(每帧之间的时间戳间隔) + private List frameCount = new ArrayList<>();//每段音频的帧数 + + private int which = 0;//当前是第几段 + private long currentTime = 0;//当前的音频时长 用于记录总时长 + + private class AudioMixAndEncodeRunnable implements Runnable { + private String[] mixFiles; + private AudioRunnable.EncodeListener mListener; + private int muxerCount = 0; + + public AudioMixAndEncodeRunnable(String[] files, AudioRunnable.EncodeListener listener) { + mixFiles = files; + mListener = listener; + } + + @Override + public void run() { + try { + int inputIndex; + ByteBuffer inputBuffer; + int outputIndex; + byte[] chunkPCM; + long start = System.currentTimeMillis(); + //初始化编码器 + MediaFormat encodeFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 2);//mime type 采样率 声道数 + encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);//比特率 + encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, android.media.MediaCodecInfo.CodecProfileLevel.AACObjectLC); + encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 500 * 1024); + + MediaCodec mediaEncode = MediaCodec.createEncoderByType("audio/mp4a-latm"); + mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + mediaEncode.start(); + + ByteBuffer[] encodeInputBuffers = mediaEncode.getInputBuffers(); + ByteBuffer[] encodeOutputBuffers = mediaEncode.getOutputBuffers(); + MediaCodec.BufferInfo encodeBufferInfo = new MediaCodec.BufferInfo(); + + + //初始化两个io + int fileNum = mixFiles.length; + FileInputStream[] audioFileStreams = new FileInputStream[fileNum]; + String audioFile; + for (int fileIndex = 0; fileIndex < fileNum; ++fileIndex) { + audioFile = mixFiles[fileIndex]; + audioFileStreams[fileIndex] = new FileInputStream(audioFile); + } + + byte[] readBytes = new byte[8 * 1024];//读取的临时数组 + FileInputStream inputStream; + byte[][] allAudioBytes = new byte[fileNum][];//用于承载读出的数据 + boolean[] streamDoneArray = new boolean[fileNum];//记录两个输入流是否已经读完 + long timeUs = 0; + boolean isFirstOutputFrame = true;//是否是输出的第一帧 + boolean encodeDone = false; + currentTime = totalTime.get(which);//当前段时长 + default_time = eachTime.get(which);//每帧时间戳间隔 + while (!encodeDone) { + if (!streamDoneArray[0] || !streamDoneArray[1]) {//数据没有读完 + inputIndex = mediaEncode.dequeueInputBuffer(TIMEOUT_USEC); + if (inputIndex >= 0) { + inputBuffer = encodeInputBuffers[inputIndex]; + inputBuffer.clear();//同解码器 + //从两个文件中读取数据 + for (int j = 0; j < fileNum; j++) { + inputStream = audioFileStreams[j]; + if (!streamDoneArray[j] && inputStream.read(readBytes) != -1) { + allAudioBytes[j] = Arrays.copyOf(readBytes, readBytes.length); + } else { + //说明某个文件读取完成了 + if (mIsBgmLong) { + //说明bgm比原声长, + if (j == 0) { + //说明是原声读取完毕了,那么bgm也不再读取 + streamDoneArray[j] = true; + streamDoneArray[1] = true;//bgm也不再读取 + //并且跳出去,就不再读数据了 + mediaEncode.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + for(FileInputStream temp:audioFileStreams){ + temp.close(); + } + break; + } + } else { + //说明原声比bgm长 + if (j == 1) { + //说明是bgm文件读取完了 + inputStream.skip(0); + int tempSize = inputStream.read(readBytes); + if (tempSize > 0) { + allAudioBytes[j] = Arrays.copyOf(readBytes, readBytes.length); + } else { + allAudioBytes[j] = new byte[8 * 1024];//那么bgm的部分就用0替代 + } + } else { + //说明是原声读取完了 + streamDoneArray[j] = true; + streamDoneArray[1] = true;//bgm也不再读取 + //结束读取数据和写入数据 + mediaEncode.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + for(FileInputStream temp:audioFileStreams){ + temp.close(); + } + break; + } + } + } + } + if (!streamDoneArray[0] || !streamDoneArray[1]) { + //对两个byte数组进行合并 +// chunkPCM = AudioHardcodeHandler.normalizationMix(allAudioBytes, mSettingInfo.volFirst, mSettingInfo.volSecond); + chunkPCM = AudioCodec.nativeAudioMix(allAudioBytes, mSettingInfo.volFirst, mSettingInfo.volSecond); + if (chunkPCM == null) { + break; + } + inputBuffer.put(chunkPCM);//PCM数据填充给inputBuffer + mediaEncode.queueInputBuffer(inputIndex, 0, chunkPCM.length, 0, 0);//通知编码器 编码 + } + } + } + + boolean outputDone = false; + while (!outputDone) {//同解码器 + outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, TIMEOUT_USEC);//同解码器 + if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { + outputDone = true; + } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + MediaFormat newFormat = mediaEncode.getOutputFormat(); + mMediaMuxer.addMeidaFormat(MediaMuxerRunnable.MEDIA_TRACK_AUDIO, newFormat); + } else if (outputIndex < 0) { + } else { + if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + encodeDone = true; + break; + } + if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + mediaEncode.releaseOutputBuffer(outputIndex, false); + break; + } + ByteBuffer outputBuffer; + if (Build.VERSION.SDK_INT >= 21) { + outputBuffer = mediaEncode.getOutputBuffer(outputIndex);//拿到输出Buffer + } else { + outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer + } + if (isFirstOutputFrame) { + timeUs = 0; + isFirstOutputFrame = false; + } else { + timeUs += default_time; + } + if (timeUs >= currentTime * 1000) {//说明音频切换了,要用下一段音频的平均帧间隔了 + which++; + + if (which >= totalTime.size()) { + which = 0; + } + currentTime += totalTime.get(which); + //说明更换视频了 + default_time = eachTime.get(which); + } + encodeBufferInfo.presentationTimeUs = timeUs; + muxerCount++; + mMediaMuxer.addMuxerData(MediaMuxerRunnable.MEDIA_TRACK_AUDIO, outputBuffer, encodeBufferInfo); + + mediaEncode.releaseOutputBuffer(outputIndex, false); + } + } + } + mediaEncode.stop(); + mediaEncode.release(); + long end = System.currentTimeMillis(); + //删除掉传入的pcm文件 + for (int i = 0; i < mixFiles.length; i++) { + File file = new File(mixFiles[i]); + if (file.exists()) { + file.delete(); + } + } + if (mListener != null) { + mListener.encodeIsOver(); + } + } catch (IOException e) { + Log.e("hero", " init encoder error "); + } + } + } + + private class AudioDecodeRunnable implements Runnable { + private boolean isBgm; + private String tempPcmFile; + private AudioRunnable.DecodeOverListener mListener; + private List mMediaCodecInfos;//将多个分离器中的音频数据 写入一个pcm文件中 + private List mAudioCodec; + private List audioTrackList;//各个分离器中track的index + private MediaCodec mMediaCodec; + private MediaDecode mMediaCodecInfo; + + public AudioDecodeRunnable(List extractors, List trackList, String pcmFile, boolean isBgm, AudioRunnable.DecodeOverListener listener) { + mMediaCodecInfos = extractors; + audioTrackList = trackList; + this.isBgm = isBgm; + tempPcmFile = pcmFile; + mListener = listener; + } + + @Override + public void run() { + //初始化多个视频源的原声解码器, + mAudioCodec = new ArrayList<>(); + /** + * 1、初始化多个解码器 + * */ + for (int i = 0; i < mMediaCodecInfos.size(); i++) { + MediaExtractor extractor = mMediaCodecInfos.get(i).extractor; + MediaFormat trackFormat = extractor.getTrackFormat(audioTrackList.get(i)); + try { + MediaCodec temp = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME)); + temp.configure(trackFormat, null, null, 0); + mAudioCodec.add(temp); + + } catch (IOException e) { + e.printStackTrace(); + } + } + String s = null; + long ss = 0; + + if (!isBgm) { + + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + + for (int i = 0; i < mMediaCodecInfos.size(); i++) { + retriever.setDataSource(mMediaCodecInfos.get(i).path); + s = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + ss += Long.parseLong(s); + totalTime.add(Long.parseLong(s));//添加每段音频的时长 + } + } + + + /** + * 2、选中第一个解码器 + * */ + mMediaCodec = mAudioCodec.get(0); + mMediaCodec.start(); + mMediaCodecInfo = mMediaCodecInfos.get(0); + if (mAudioDecodes.get(0).cutDuration > 0 && mAudioDecodes.get(0).cutPoint + mAudioDecodes.get(0).cutDuration <= mAudioDecodes.get(0).duration) { + mMediaCodecInfo.extractor.seekTo(mAudioDecodes.get(0).cutPoint * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + } + /** + * 3、初始化当前解码器的相关参数 + * */ + ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); + ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers(); + MediaCodec.BufferInfo decodeBufferInfo = new MediaCodec.BufferInfo(); + MediaCodec.BufferInfo inputInfo = new MediaCodec.BufferInfo(); + boolean codeOver = false;//所有音频是否全部解码 + int curIndex = 0;//当前是输入的第几个音频 + boolean isNewStep = false;//音频段落切换标志 + boolean isFirstFrame = true; + long samptime = 0;//记录上一帧音频时间戳,以便于取差值 + boolean inputDone = false;//整体输入结束标志 + int decode = 0;//用以记录每段输入了多少帧音频 + try { + FileOutputStream fos = new FileOutputStream(tempPcmFile); + long stratTime = System.currentTimeMillis(); + while (!codeOver) { + if (!inputDone) { + for (int i = 0; i < inputBuffers.length; i++) { + //遍历所以的编码器 然后将数据传入之后 再去输出端取数据 + int inputIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC); + if (inputIndex >= 0) { + /** + * 从分离器中拿到数据 写入解码器 + * */ + ByteBuffer inputBuffer = inputBuffers[inputIndex];//拿到inputBuffer + inputBuffer.clear();//清空之前传入inputBuffer内的数据 + int sampleSize = mMediaCodecInfo.extractor.readSampleData(inputBuffer, 0);//MediaExtractor读取数据到inputBuffer中 + + if (sampleSize < 0) { + mMediaCodec.queueInputBuffer(inputIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + inputDone = true; + } else { + /**音频对解码前的数据 进行抛弃*/ + long sampleTime = mMediaCodecInfo.extractor.getSampleTime(); + boolean isWrite = true; + if (mMediaCodecInfo.cutDuration > 0 && sampleTime / 1000 < mMediaCodecInfo.cutPoint) { + //说明还没有到剪切点 + isWrite = false; + mMediaCodec.queueInputBuffer(inputIndex, 0, 0, 0, 0);//释放inputBuffer + } + if (mMediaCodecInfo.cutDuration > 0 && sampleTime / 1000 > (mMediaCodecInfo.cutDuration + mMediaCodecInfo.cutPoint)) { + isWrite = false; + mMediaCodec.queueInputBuffer(inputIndex, 0, 0, 0, 0);//虽然跳过了 但是需要释放当前的inputBuffer + } + + if (isWrite) {//当前读取的数据可用,且在裁剪范围内 + inputInfo.offset = 0; + inputInfo.size = sampleSize; + inputInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME; + + if (isNewStep) {//切换到了下一段音频 + if (!isBgm) { + frameCount.add(decode);//保存之前一段的帧数 + decode = 0; + } + inputInfo.presentationTimeUs += 23219; + isNewStep = false; + } else { + if (isFirstFrame) {//当为第一个音频的第一帧时 + inputInfo.presentationTimeUs = 0; + isFirstFrame = false; + } else { + inputInfo.presentationTimeUs += mMediaCodecInfo.extractor.getSampleTime() - samptime; + } + } + decode++; + mMediaCodec.queueInputBuffer(inputIndex, inputInfo.offset, sampleSize, inputInfo.presentationTimeUs, 0);//通知MediaDecode解码刚刚传入的数据 + } + samptime = mMediaCodecInfo.extractor.getSampleTime(); + mMediaCodecInfo.extractor.advance();//MediaExtractor移动到下一取样处 + } + } + } + } + + boolean decodeOutputDone = false; + byte[] chunkPCM; + while (!decodeOutputDone) { + int outputIndex = mMediaCodec.dequeueOutputBuffer(decodeBufferInfo, TIMEOUT_USEC); + if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { + /**没有可用的解码器output*/ + decodeOutputDone = true; + } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + outputBuffers = mMediaCodec.getOutputBuffers(); + } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + MediaFormat newFormat = mMediaCodec.getOutputFormat(); + } else if (outputIndex < 0) { + } else { + ByteBuffer outputBuffer; + if (Build.VERSION.SDK_INT >= 21) { + outputBuffer = mMediaCodec.getOutputBuffer(outputIndex); + } else { + outputBuffer = outputBuffers[outputIndex]; + } + boolean isUseful = true; + + if (isUseful) { + chunkPCM = new byte[decodeBufferInfo.size]; + outputBuffer.get(chunkPCM); + outputBuffer.clear(); + byte[] result = chunkPCM; + if (chunkPCM.length < 4096) {//这里看起来应该是16位单声道转16位双声道 + //说明是单声道的,需要转换一下 + byte[] stereoBytes = new byte[decodeBufferInfo.size * 2]; + for (int i = 0; i < chunkPCM.length; i += 2) { + stereoBytes[i * 2 + 0] = chunkPCM[i]; + stereoBytes[i * 2 + 1] = chunkPCM[i + 1]; + stereoBytes[i * 2 + 2] = chunkPCM[i]; + stereoBytes[i * 2 + 3] = chunkPCM[i + 1]; + } + result = stereoBytes; + } + if (isBgm) { + int i = bgmCount.get() + result.length;// + bgmCount.set(i); + } else { + int i = audioCount.get() + result.length; + audioCount.set(i); + } + fos.write(result);//数据写入文件中 + fos.flush(); + } + mMediaCodec.releaseOutputBuffer(outputIndex, false); + if ((decodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + curIndex++; + if (curIndex >= mMediaCodecInfos.size()) { + codeOver = true; + inputDone = true; + if (!isBgm) { + frameCount.add(decode);//加入最后一段音频帧数 + } + } else { + /** + * 更换分离器 + * 更换解码器 + * 开始解码下一个音频 + * */ + mMediaCodecInfo.extractor.release(); + mMediaCodecInfo = mMediaCodecInfos.get(curIndex); + if (mMediaCodecInfo.cutDuration > 0) { + mMediaCodecInfo.extractor.seekTo(mMediaCodecInfo.cutPoint * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + } + mMediaCodec.stop(); + mMediaCodec.release(); + mMediaCodec = mAudioCodec.get(curIndex); + mMediaCodec.start(); + isNewStep = true; + codeOver = false; + inputBuffers = mMediaCodec.getInputBuffers(); + outputBuffers = mMediaCodec.getOutputBuffers(); + inputDone = false; + } + } + } + } + + if (isBgm && mIsBgmLong && audioDecodeOver.get()) { + //如果当前是bgm解码线程、并且是bgm比较长、并且原声解码完成了,就需要判定一下 + //当前bgm解码个数是否大于等于audioCount解码个数,如果大于 就退出线程 + if (bgmCount.get() >= audioCount.get()) { + break; + } + } + }//大循环结束 + + if (isBgm) { + //bgm解码完成 + bgmDecodeOver.set(true); + } else { + audioDecodeOver.set(true); + } + if (!isBgm) { + for (int i = 0; i < frameCount.size(); i++) { + long l = totalTime.get(i) * 1000 / frameCount.get(i);//计算每段音频的帧平均间隔 + eachTime.add(l); + } + } + fos.close();//输出流释放 + //释放最后一个decoder + mMediaCodec.stop(); + mMediaCodec.release(); + if (mListener != null) { + mListener.decodeIsOver(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void log(boolean isBgm, String log) { + if (!isBgm) { + Log.e("audioo", log); + } else { + Log.e("bgm", log); + } + } + + + /** + * 只是将原视频中的音频分离出来,然后进行添加 + * 不需要编解码等操作 + * 支持多个视频文件的音频连续写入 + * 暂时不考虑视频中没有音频的情况 + * 不考虑部分视频的音频有问题 导致音频速度加快或者减慢的情况(主要是因为立体声和单声道引起的 + * stereo和mono) + */ + private void simpleAudioMix() { + MediaExtractor mExtractor = mAudioDecodes.get(0).extractor;//拿到第一个分离器 + mAudioDecode = mAudioDecodes.get(0);//拿到第一个视频的信息 + int trackCount = mExtractor.getTrackCount(); + for (int i = 0; i < trackCount; i++) {//初始化第一段视频的音频format + MediaFormat trackFormat = mExtractor.getTrackFormat(i); + String mime = trackFormat.getString(MediaFormat.KEY_MIME); + if (mime.startsWith("audio/")) { + audioTrackIndex = i; + audioMediaFormat = trackFormat; + break; + } + } + //将第一个视频的audioFormat作为整体音频的audioFormat + mMediaMuxer.addMeidaFormat(MediaMuxerRunnable.MEDIA_TRACK_AUDIO, audioMediaFormat); + + ByteBuffer buffer = ByteBuffer.allocate(50 * 1024); + + //分离音频 + int curIndex = 0;//当前是第几段视频,从第0段开始, + boolean isNextStep = false; + boolean isFirstFrame = true; + boolean isFirstSeek = false; + long samptime = 0; + long lastTime = 0;//上一帧的时间戳 + if (audioTrackIndex != -1) { + if (mAudioDecode.cutDuration > 0 && (mAudioDecode.cutPoint + mAudioDecode.cutDuration) <= mAudioDecode.duration) { + //说明是需要剪切的 跳转到剪切点 现在只是跳了第一个视频 + mAudioDecode.extractor.seekTo(mAudioDecode.cutPoint * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + isFirstSeek = true; + } + while (true) { + int readSampleData = mAudioDecode.extractor.readSampleData(buffer, 0); + if (readSampleData < 0) { + //说明 本地读取完毕了 + curIndex++; + if (curIndex < mAudioDecodes.size()) { + //说明还有新的音频要添加 + mAudioDecode.extractor.release();//老的释放掉 + mExtractor = mAudioDecodes.get(curIndex).extractor;//更换extractor + mAudioDecode = mAudioDecodes.get(curIndex);//更换音频的那个啥 + if (mAudioDecode.cutDuration > 0 && (mAudioDecode.cutPoint + mAudioDecode.cutDuration) <= mAudioDecode.duration) { + //说明是需要剪切的 跳转到剪切点 现在只是跳了第一个视频 + mAudioDecode.extractor.seekTo(mAudioDecode.cutPoint * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + } + isNextStep = true; + } else { + //说明已经没有其他的音频了 就break掉 + break; + } + } else { + long sampleTime = mAudioDecode.extractor.getSampleTime();//当前的时间戳 + //如果这个时间戳以及大于了 就break掉 + if (mAudioDecode.cutDuration > 0 && (mAudioDecode.cutPoint + mAudioDecode.cutDuration) <= sampleTime / 1000) { + /** + * 说明本地视频到了剪切点了,要做以下几件事情 + * 1、跳出本次写入 + * 2、更换源文件 + * */ + curIndex++; + if (curIndex < mAudioDecodes.size()) { + //说明还有源文件 + mAudioDecode.extractor.release(); + mAudioDecode = mAudioDecodes.get(curIndex); + if (mAudioDecode.cutDuration > 0 && (mAudioDecode.cutPoint + mAudioDecode.cutDuration) <= mAudioDecode.duration) { + //说明是需要剪切的 跳转到剪切点 现在只是跳了第一个视频 + mAudioDecode.extractor.seekTo(mAudioDecode.cutPoint * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + } + isNextStep = true; + continue; + } else { + //说明已经没有了 + break; + } + } + + MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); + info.offset = 0; + info.size = readSampleData; + info.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME; + if (isNextStep) { + //说明是新的一段音频 + info.presentationTimeUs = lastTime + 23219; + isNextStep = false; + } else { + //当前帧的时间戳 减去上次帧的时间戳 加上 上次数据帧的时间 就是本次帧的时间戳 + if (isFirstSeek) { + //说明第一个视频跳帧过 + info.presentationTimeUs = 0; + isFirstSeek = false; + } else { + if (isFirstFrame) { + info.presentationTimeUs = 0; + isFirstFrame = false; + } else { + info.presentationTimeUs = lastTime + (mAudioDecode.extractor.getSampleTime() - samptime); + } + } + } + lastTime = info.presentationTimeUs; + samptime = mAudioDecode.extractor.getSampleTime(); + mMediaMuxer.addMuxerData(MediaMuxerRunnable.MEDIA_TRACK_AUDIO, buffer, info); + mAudioDecode.extractor.advance(); + } + } + mAudioDecode.extractor.release(); + mMediaMuxer.audioIsOver(); + } + } + + interface DecodeOverListener { + void decodeIsOver(); + } + + interface EncodeListener { + void encodeIsOver(); + } +} diff --git a/app/src/main/java/com/example/cj/videoeditor/mediacodec/MediaMuxerRunnable.java b/app/src/main/java/com/example/cj/videoeditor/mediacodec/MediaMuxerRunnable.java new file mode 100644 index 0000000..525fa58 --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/mediacodec/MediaMuxerRunnable.java @@ -0,0 +1,202 @@ +package com.example.cj.videoeditor.mediacodec; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.media.MediaMuxer; +import android.text.TextUtils; +import android.util.Log; + + +import com.example.cj.videoeditor.bean.AudioSettingInfo; +import com.example.cj.videoeditor.gpufilter.helper.MagicFilterType; +import com.example.cj.videoeditor.media.VideoInfo; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Vector; + +/** + * Created by cj on 2017/6/30. + * desc 音视频混合器,只要是将音视频的混合分成两条子线程进行处理 + * 音频线程AudioRunnable 进行音频的编解码 以及向混合器中写入数据 + * 视频线程VideoRunnable 进行视频的编解码 以及向混合器中写入数据 + */ + +public class MediaMuxerRunnable extends Thread { + public static final int MEDIA_TRACK_AUDIO = 1; + public static final int MEDIA_TRACK_VIDEO = 2; + + private AudioRunnable mAudioRunnable; + private VideoRunnable mVideoRunnable; + private final Object lock = new Object(); + + + private String outputFilePath; + private MediaMuxer mMediaMuxer; + + private int mVideoTrack; + private int mAudioTrack; + private boolean isVideoAdd = false; + private boolean isAudioAdd = false; + private boolean isVideoOver = false; + private boolean isAudioOver = false; + private boolean isMuxerStarted = false; + + private static AudioSettingInfo mAudioSettingInfo; + private static MagicFilterType mFilterType; + private MuxerListener mListener; + private List mVideoInfos;//视频文件的信息 + + public MediaMuxerRunnable() { + + } + + + //设置要合并的视频信息和输出文件path + public void setVideoInfo(List inputVideoInfo, String outputFile) { + mVideoInfos = inputVideoInfo; + outputFilePath = outputFile; + } + + public void addMuxerListener(MuxerListener listener) { + mListener = listener; + } + + public void setAudioSetting(AudioSettingInfo info) { + mAudioSettingInfo = info; + } + + public void setOrientationHint(int i) { + mMediaMuxer.setOrientationHint(i); + } + + public void setFilterType(MagicFilterType filterType) { + mFilterType = filterType; + } + + /** + * 如果当前没有数据的话,会lock一直在wait,如果解码完成 + */ + //音频解码完成 + public void audioIsOver() { + isAudioOver = true; + editorFinish(); + } + + //视频解码编辑完成 + public void videoIsOver() { + isVideoOver = true; + editorFinish(); + } + public void editorFinish(){ + synchronized (lock){ + if(isAudioOver&&isVideoOver){ + stopMediaMuxer(); + if (mListener != null) { + mListener.onFinish(); + } + } + } + } + + /** + * @param trackIndex audio_track == 1 video_track == 2 + * @param format MediaFormat + */ + public synchronized void addMeidaFormat(int trackIndex, MediaFormat format) { + synchronized (lock) { + if (mMediaMuxer == null) { + return; + } + if (trackIndex == MEDIA_TRACK_AUDIO) { + mAudioTrack = mMediaMuxer.addTrack(format); + isAudioAdd = true; + + } else if (trackIndex == MEDIA_TRACK_VIDEO) { + mVideoTrack = mMediaMuxer.addTrack(format); + isVideoAdd = true; + } + if (isAudioAdd && isVideoAdd) { + mMediaMuxer.start(); + isMuxerStarted = true; + lock.notify(); + Log.e("muxer", "start media muxer waiting for data..."); + } + } + } + + public void addMuxerData(int trackIndex, ByteBuffer buffer, MediaCodec.BufferInfo info){ + if (!isMuxerStarted) { + synchronized (lock) { + if (!isMuxerStarted) { + try { + lock.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + if (trackIndex == MEDIA_TRACK_AUDIO) { + mMediaMuxer.writeSampleData(mAudioTrack, buffer, info); + } else if (trackIndex == MEDIA_TRACK_VIDEO) { + mMediaMuxer.writeSampleData(mVideoTrack, buffer, info); + } + } + @Override + public void run() { + // + if (mListener != null) { + mListener.onStart(); + } + initMuxer(); + mAudioRunnable = new AudioRunnable(mVideoInfos, mAudioSettingInfo,this); + mVideoRunnable = new VideoRunnable(mVideoInfos, mFilterType, this); + + mAudioRunnable.start(); + mVideoRunnable.start(); + } + + //初始化混合器,编解码线程 + private void initMuxer() { + checkUseful(); + + try { + mMediaMuxer = new MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + //检查各项参数是否正确 + private void checkUseful() { + if (mVideoInfos == null || mVideoInfos.size() == 0) { + throw new IllegalStateException(" 必须先设置要处理的视频"); + } + if (TextUtils.isEmpty(outputFilePath)) { + throw new IllegalStateException(" 必须设置视频输出路径"); + } + } + + /** + * 停止MediaMuxer + */ + private void stopMediaMuxer() { + if (mMediaMuxer != null) { + mMediaMuxer.stop(); + mMediaMuxer.release(); + isAudioAdd = false; + isVideoAdd = false; + mMediaMuxer = null; + } + } + + public interface MuxerListener { + void onStart(); + + void onFinish(); + } +} diff --git a/app/src/main/java/com/example/cj/videoeditor/mediacodec/OutputSurface.java b/app/src/main/java/com/example/cj/videoeditor/mediacodec/OutputSurface.java index 24446f6..27e8445 100644 --- a/app/src/main/java/com/example/cj/videoeditor/mediacodec/OutputSurface.java +++ b/app/src/main/java/com/example/cj/videoeditor/mediacodec/OutputSurface.java @@ -22,7 +22,7 @@ import com.example.cj.videoeditor.MyApplication; -import com.example.cj.videoeditor.drawer.VideoDrawer; +import com.example.cj.videoeditor.drawer.TextureRender; import com.example.cj.videoeditor.gpufilter.basefilter.GPUImageFilter; import com.example.cj.videoeditor.media.VideoInfo; @@ -59,8 +59,8 @@ class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { private Surface mSurface; private Object mFrameSyncObject = new Object(); // guards mFrameAvailable private boolean mFrameAvailable; -// private TextureRender mTextureRender; - private VideoDrawer mDrawer; + private TextureRender mTextureRender; +// private VideoDrawer mDrawer; /** * Creates an OutputSurface using the current EGL context. Creates a Surface that can be @@ -81,9 +81,20 @@ private void setup(VideoInfo info) { // mTextureRender = new TextureRender(info); // mTextureRender.surfaceCreated(); - mDrawer = new VideoDrawer(MyApplication.getContext(),MyApplication.getContext().getResources()); + /* mDrawer = new VideoDrawer(MyApplication.getContext(),MyApplication.getContext().getResources()); + mDrawer.onSurfaceCreated(null,null); - mDrawer.onSurfaceChanged(null,info.width,info.height); + if (info.rotation == 0 || info.rotation == 180){ + mDrawer.onSurfaceChanged(null,info.width,info.height); + }else { + mDrawer.onSurfaceChanged(null,info.height,info.width); + } + mDrawer.onVideoChanged(info);*/ + + mTextureRender = new TextureRender(info); + mTextureRender.surfaceCreated(); + mTextureRender.onVideoSizeChanged(info); + mSurfaceTexture = new SurfaceTexture(mTextureRender.getTextureId()); // Even if we don't access the SurfaceTexture after the constructor returns, we // still need to keep a reference to it. The Surface doesn't retain a reference @@ -91,7 +102,7 @@ private void setup(VideoInfo info) { // causes the native finalizer to run. // if (VERBOSE) Log.d(TAG, "textureID=" + mTextureRender.getTextureId()); // mSurfaceTexture = new SurfaceTexture(mTextureRender.getTextureId()); - mSurfaceTexture = mDrawer.getSurfaceTexture(); +// mSurfaceTexture = mDrawer.getSurfaceTexture(); // This doesn't work if OutputSurface is created on the thread that CTS started for // these test cases. @@ -198,8 +209,8 @@ public void release() { mEGLContext = null; mEGLSurface = null; mEGL = null; -// mTextureRender = null; - mDrawer = null; + mTextureRender = null; +// mDrawer = null; mSurface = null; mSurfaceTexture = null; } @@ -258,15 +269,15 @@ public void awaitNewImage() { // Latch the data. // mTextureRender.checkGlError("before updateTexImage"); // mDrawer.checkGlError("before updateTexImage"); -// mSurfaceTexture.updateTexImage(); + mSurfaceTexture.updateTexImage(); } /** * Draws the data from SurfaceTexture onto the current EGL surface. */ public void drawImage() { -// mTextureRender.drawFrame(mSurfaceTexture); - mDrawer.onDrawFrame(null); + mTextureRender.drawFrame(mSurfaceTexture); +// mDrawer.onDrawFrame(null); } @Override @@ -297,13 +308,15 @@ private void checkEglError(String msg) { } } public void addGpuFilter(GPUImageFilter filter){ - mDrawer.setGpuFilter(filter); +// mDrawer.setGpuFilter(filter); + mTextureRender.addGpuFilter(filter); } public void isBeauty(boolean isBeauty){ - mDrawer.isOpenBeauty(isBeauty); +// mDrawer.isOpenBeauty(isBeauty); +// mTextureRender.isOpenBeauty(isBeauty); } public void onVideoSizeChanged(VideoInfo info){ -// mTextureRender.onVideoSizeChanged(info); + mTextureRender.onVideoSizeChanged(info); } } \ No newline at end of file diff --git a/app/src/main/java/com/example/cj/videoeditor/mediacodec/VideoClipper.java b/app/src/main/java/com/example/cj/videoeditor/mediacodec/VideoClipper.java index 252c2d1..cf354ec 100644 --- a/app/src/main/java/com/example/cj/videoeditor/mediacodec/VideoClipper.java +++ b/app/src/main/java/com/example/cj/videoeditor/mediacodec/VideoClipper.java @@ -32,38 +32,39 @@ public class VideoClipper { private String mInputVideoPath; private String mOutputVideoPath; - MediaCodec videoDecoder; - MediaCodec videoEncoder; - MediaCodec audioDecoder; - MediaCodec audioEncoder; - - MediaExtractor mVideoExtractor; - MediaExtractor mAudioExtractor; - MediaMuxer mMediaMuxer; - static ExecutorService executorService = Executors.newFixedThreadPool(4); - int muxVideoTrack = -1; - int muxAudioTrack = -1; - int videoTrackIndex = -1; - int audioTrackIndex = -1; - long startPosition; - long clipDur; - int videoWidth; - int videoHeight; - int videoRotation; - OutputSurface outputSurface = null; - InputSurface inputSurface = null; - MediaFormat videoFormat; - MediaFormat audioFormat; - GPUImageFilter mFilter; - boolean isOpenBeauty; - boolean videoFinish = false; - boolean audioFinish = false; - boolean released = false; - long before; - long after; - Object lock = new Object(); - boolean muxStarted = false; - OnVideoCutFinishListener listener; + private MediaCodec videoDecoder; + private MediaCodec videoDecoder2; + private MediaCodec videoEncoder; + private MediaCodec audioDecoder; + private MediaCodec audioEncoder; + + private MediaExtractor mVideoExtractor; + private MediaExtractor mAudioExtractor; + private MediaMuxer mMediaMuxer; + private static ExecutorService executorService = Executors.newFixedThreadPool(4); + private int muxVideoTrack = -1; + private int muxAudioTrack = -1; + private int videoTrackIndex = -1; + private int audioTrackIndex = -1; + private long startPosition; + private long clipDur; + private int videoWidth; + private int videoHeight; + private int videoRotation; + private OutputSurface outputSurface = null; + private InputSurface inputSurface = null; + private MediaFormat videoFormat; + private MediaFormat audioFormat; + private GPUImageFilter mFilter; + private boolean isOpenBeauty; + private boolean videoFinish = false; + private boolean audioFinish = false; + private boolean released = false; + private long before; + private long after; + private Object lock = new Object(); + private boolean muxStarted = false; + private OnVideoCutFinishListener listener; //初始化音视频解码器和编码器 public VideoClipper() { @@ -90,16 +91,18 @@ public void setOutputVideoPath(String outputPath) { public void setOnVideoCutFinishListener(OnVideoCutFinishListener listener) { this.listener = listener; } + /** * 设置滤镜 - * */ + */ public void setFilter(GPUImageFilter filter) { - if (filter == null ) { + if (filter == null) { mFilter = null; return; } mFilter = filter; } + public void setFilterType(MagicFilterType type) { if (type == null || type == MagicFilterType.NONE) { mFilter = null; @@ -110,8 +113,8 @@ public void setFilterType(MagicFilterType type) { /** * 开启美颜 - * */ - public void showBeauty(){ + */ + public void showBeauty() { isOpenBeauty = true; } @@ -157,7 +160,7 @@ public void run() { long firstVideoTime = mVideoExtractor.getSampleTime(); mVideoExtractor.seekTo(firstVideoTime + startPosition, SEEK_TO_PREVIOUS_SYNC); - + Log.e("hero","_____videoCliper------run"); initVideoCodec();//暂时统一处理,为音频转换采样率做准备 startVideoCodec(videoDecoder, videoEncoder, mVideoExtractor, inputSurface, outputSurface, firstVideoTime, startPosition, clipDur); @@ -210,10 +213,10 @@ private void startAudioCodec(MediaCodec decoder, MediaCodec encoder, MediaExtrac boolean inputDone = false; boolean decodeDone = false; extractor.seekTo(firstSampleTime + startPosition, SEEK_TO_PREVIOUS_SYNC); - int decodeinput=0; - int encodeinput=0; - int encodeoutput=0; - long lastEncodeOutputTimeStamp=-1; + int decodeinput = 0; + int encodeinput = 0; + int encodeoutput = 0; + long lastEncodeOutputTimeStamp = -1; while (!done) { if (!inputDone) { int inputIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC); @@ -225,7 +228,7 @@ private void startAudioCodec(MediaCodec decoder, MediaCodec encoder, MediaExtrac if ((dur < duration) && readSampleData > 0) { decoder.queueInputBuffer(inputIndex, 0, readSampleData, extractor.getSampleTime(), 0); decodeinput++; - System.out.println("videoCliper audio decodeinput"+decodeinput+" dataSize"+readSampleData+" sampeTime"+extractor.getSampleTime()); + System.out.println("videoCliper audio decodeinput" + decodeinput + " dataSize" + readSampleData + " sampeTime" + extractor.getSampleTime()); extractor.advance(); } else { decoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); @@ -247,11 +250,16 @@ private void startAudioCodec(MediaCodec decoder, MediaCodec encoder, MediaExtrac } else { boolean canEncode = (info.size != 0 && info.presentationTimeUs - firstSampleTime > startPosition); boolean endOfStream = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; - if (canEncode&&!endOfStream) { - ByteBuffer decoderOutputBuffer = decoderOutputBuffers[index]; + if (canEncode && !endOfStream) { + ByteBuffer decoderOutputBuffer; + if (Build.VERSION.SDK_INT >= 21){ + decoderOutputBuffer = decoder.getOutputBuffer(index); + }else { + decoderOutputBuffer = decoderOutputBuffers[index]; + } int encodeInputIndex = encoder.dequeueInputBuffer(TIMEOUT_USEC); - if(encodeInputIndex>=0){ + if (encodeInputIndex >= 0) { ByteBuffer encoderInputBuffer = encoderInputBuffers[encodeInputIndex]; encoderInputBuffer.clear(); if (info.size < 4096) {//这里看起来应该是16位单声道转16位双声道 @@ -269,16 +277,16 @@ private void startAudioCodec(MediaCodec decoder, MediaCodec encoder, MediaExtrac encoderInputBuffer.put(stereoBytes); encoder.queueInputBuffer(encodeInputIndex, 0, stereoBytes.length, info.presentationTimeUs, 0); encodeinput++; - System.out.println("videoCliper audio encodeInput"+encodeinput+" dataSize"+info.size+" sampeTime"+info.presentationTimeUs); - }else{ + System.out.println("videoCliper audio encodeInput" + encodeinput + " dataSize" + info.size + " sampeTime" + info.presentationTimeUs); + } else { encoderInputBuffer.put(decoderOutputBuffer); encoder.queueInputBuffer(encodeInputIndex, info.offset, info.size, info.presentationTimeUs, 0); encodeinput++; - System.out.println("videoCliper audio encodeInput"+encodeinput+" dataSize"+info.size+" sampeTime"+info.presentationTimeUs); + System.out.println("videoCliper audio encodeInput" + encodeinput + " dataSize" + info.size + " sampeTime" + info.presentationTimeUs); } } } - if(endOfStream){ + if (endOfStream) { int encodeInputIndex = encoder.dequeueInputBuffer(TIMEOUT_USEC); encoder.queueInputBuffer(encodeInputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); System.out.println("videoCliper audio encodeInput end"); @@ -309,12 +317,12 @@ private void startAudioCodec(MediaCodec decoder, MediaCodec encoder, MediaExtrac if (outputInfo.presentationTimeUs == 0 && !done) { continue; } - if (outputInfo.size != 0&&outputInfo.presentationTimeUs>0) { + if (outputInfo.size != 0 && outputInfo.presentationTimeUs > 0) { /*encodedData.position(outputInfo.offset); encodedData.limit(outputInfo.offset + outputInfo.size);*/ - if(!muxStarted){ - synchronized (lock){ - if(!muxStarted){ + if (!muxStarted) { + synchronized (lock) { + if (!muxStarted) { try { lock.wait(); } catch (InterruptedException e) { @@ -323,11 +331,11 @@ private void startAudioCodec(MediaCodec decoder, MediaCodec encoder, MediaExtrac } } } - if(outputInfo.presentationTimeUs>lastEncodeOutputTimeStamp){//为了避免有问题的数据 + if (outputInfo.presentationTimeUs > lastEncodeOutputTimeStamp) {//为了避免有问题的数据 encodeoutput++; - System.out.println("videoCliper audio encodeOutput"+encodeoutput+" dataSize"+outputInfo.size+" sampeTime"+outputInfo.presentationTimeUs); + System.out.println("videoCliper audio encodeOutput" + encodeoutput + " dataSize" + outputInfo.size + " sampeTime" + outputInfo.presentationTimeUs); mMediaMuxer.writeSampleData(muxAudioTrack, encodedData, outputInfo); - lastEncodeOutputTimeStamp=outputInfo.presentationTimeUs; + lastEncodeOutputTimeStamp = outputInfo.presentationTimeUs; } } @@ -343,10 +351,19 @@ private void startAudioCodec(MediaCodec decoder, MediaCodec encoder, MediaExtrac private void initVideoCodec() { //不对视频进行压缩 - int encodeW = videoWidth; - int encodeH = videoHeight; + VideoInfo info = new VideoInfo(); + info.width = videoWidth; + info.height = videoHeight; + info.rotation = videoRotation; + + MediaFormat mediaFormat; + if (info.rotation == 0 || info.rotation == 180) { + mediaFormat = MediaFormat.createVideoFormat("video/avc", info.width, info.height); + }else { + mediaFormat = MediaFormat.createVideoFormat("video/avc", info.height, info.width); + } //设置视频的编码参数 - MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", encodeW, encodeH); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 3000000); mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30); mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); @@ -356,16 +373,11 @@ private void initVideoCodec() { inputSurface.makeCurrent(); videoEncoder.start(); - - VideoInfo info = new VideoInfo(); - info.width = videoWidth; - info.height = videoHeight; - info.rotation = videoRotation; outputSurface = new OutputSurface(info); - outputSurface.isBeauty(isOpenBeauty); +// outputSurface.isBeauty(isOpenBeauty); if (mFilter != null) { - Log.e("hero","---gpuFilter 不为null哟----设置进outputSurface里面"); + Log.e("hero", "---gpuFilter 不为null哟----设置进outputSurface里面"); outputSurface.addGpuFilter(mFilter); } @@ -462,9 +474,9 @@ private void startVideoCodec(MediaCodec decoder, MediaCodec encoder, MediaExtrac if (outputInfo.size != 0) { encodedData.position(outputInfo.offset); encodedData.limit(outputInfo.offset + outputInfo.size); - if(!muxStarted){ - synchronized (lock){ - if(!muxStarted){ + if (!muxStarted) { + synchronized (lock) { + if (!muxStarted) { try { lock.wait(); } catch (InterruptedException e) { diff --git a/app/src/main/java/com/example/cj/videoeditor/mediacodec/VideoRunnable.java b/app/src/main/java/com/example/cj/videoeditor/mediacodec/VideoRunnable.java new file mode 100644 index 0000000..7a5b980 --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/mediacodec/VideoRunnable.java @@ -0,0 +1,388 @@ +package com.example.cj.videoeditor.mediacodec; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.os.Build; +import android.util.Log; + +import com.example.cj.videoeditor.bean.MediaDecode; +import com.example.cj.videoeditor.gpufilter.basefilter.GPUImageFilter; +import com.example.cj.videoeditor.gpufilter.helper.MagicFilterFactory; +import com.example.cj.videoeditor.gpufilter.helper.MagicFilterType; +import com.example.cj.videoeditor.media.VideoInfo; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by cj on 2017/6/30. + * desc 视频编解码线程 + */ +@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) +public class VideoRunnable extends Thread { + private static final String MIME_TYPE = "video/avc"; + private static final int bitRate = 3000000; //视频编码波特率 + private static final int frameRate = 30; //视频编码帧率 + private static final int frameInterval = 1; + private MediaMuxerRunnable mMediaMuxer; + private String inputFile; + private boolean isExit = false; + public int videoWidth; + public int videoHeight; + GPUImageFilter filter; + + private MediaFormat videoOutputFormat; + private MediaExtractor mExtractor; + + //处理多段视频 + private List mVideoInfos;//多段视频的信息 + private List mExtractors; + private List mMediaCodecInfos; + private VideoInfo mInfo; + + public VideoRunnable(List inputFiles, MagicFilterType filterType, MediaMuxerRunnable mediaMuxer) { + this.mMediaMuxer = mediaMuxer; + this.mVideoInfos = inputFiles; + this.filter = MagicFilterFactory.initFilters(filterType); + } + + @Override + public void run() { + try { + prepare(); + editVideo(videoOutputFormat); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void editVideo(MediaFormat outputFormat) throws IOException { + MediaCodec videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE); + videoEncoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + InputSurface inputSurface = new InputSurface(videoEncoder.createInputSurface()); + inputSurface.makeCurrent(); + videoEncoder.start();//编码器启动 + + OutputSurface outputSurface = new OutputSurface(mInfo); + if (filter != null) { + outputSurface.addGpuFilter(filter); + } + List decodeList = new ArrayList<>(); + List formatList = new ArrayList<>(); + try { + //初始化decoder,format,extractor + for (int i = 0; i < mVideoInfos.size(); i++) { + MediaExtractor extractor = mExtractors.get(i); + int trackCount = extractor.getTrackCount(); + for (int j = 0; j < trackCount; j++) { + MediaFormat format = extractor.getTrackFormat(j); + if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) { + MediaCodec videoDecoder = MediaCodec.createDecoderByType(MIME_TYPE); + decodeList.add(videoDecoder); + formatList.add(format); + extractor.selectTrack(j); + break; + } + } + } + + editVideoDataNew(mMediaCodecInfos, decodeList, formatList, outputSurface, inputSurface, videoEncoder); + } finally { + if (outputSurface != null) { + outputSurface.release(); + } + if (inputSurface != null) { + inputSurface.release(); + } + if (videoEncoder != null) { + videoEncoder.stop(); + videoEncoder.release(); + } + } + } + + //重构分离数据 解码数据 编码数据的流程 + private void editVideoDataNew(List mediaCodecInfos, List decodeList, + List formatList, OutputSurface outputSurface, + InputSurface inputSurface, MediaCodec videoEncoder) { + long start = System.currentTimeMillis(); + final int TIMEOUT_USEC = 0; + /** + * 1、初始化第一个解码器 + * */ + MediaCodec decoder = decodeList.get(0); + decoder.configure(formatList.get(0), outputSurface.getSurface(), null, 0); + decoder.start(); + ByteBuffer[] inputBuffers = decoder.getInputBuffers(); + + /** + * 2、初始化第一个分离器 + * */ + MediaDecode mediaCodecInfo = mediaCodecInfos.get(0); + + /** 3、初始化编码器*/ + ByteBuffer[] encoderOutputBuffers = videoEncoder.getOutputBuffers(); + /** + * 4、初始化解码器和编码器的bufferInfo + * */ + MediaCodec.BufferInfo encodeOutputInfo = new MediaCodec.BufferInfo(); + MediaCodec.BufferInfo decodeOutputInfo = new MediaCodec.BufferInfo(); + + /** + * 5、初始化while循环 while循环判断结束的标准在于 所有编码器中的数据 都已经添加到了分离器 + * */ + boolean outputDone = false;//是否整体编解码完毕 + boolean isNextStep = false;//是否新加了一段视频 + boolean isFirstDecodeInputFrame = true;//是否是解码输入的第一帧 + boolean isFirstDecodeOutputFrame = true;//是否是第一段视频的第一帧 + boolean inputDone = false;//是否输入结束 + boolean isChangeVideo = false;//是否切换视频 + + int curVideoIndex = 0;//当前解码视频的位置 + + long decodeInputTimeStamp = 0; + long lastInputTime = 0; + long encodeInputTimeStamp = 0; + long lastVideoTime = 0; + + while (!outputDone) { + /** + * 6、循环的第一步 从分离器中取数据 写入到解码器中 + * */ + if (!inputDone) { + int inputIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC); + if (inputIndex >= 0) { + /**说明解码器有可用的ByteBuffer*/ + ByteBuffer inputBuffer = inputBuffers[inputIndex]; + inputBuffer.clear(); + /**从分离器中取数据*/ + int readSampleData = mediaCodecInfo.extractor.readSampleData(inputBuffer, 0); + if (readSampleData < 0) { + /**说明该分离器中 没有数据了 发送一个解码流结束的标志位*/ + Log.e("send", "-----发送end--flag"); + inputDone = true; + decoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + } else { + /** + * 重写输入数据的时间戳 + * 关键点在于 如果是下一段视频的数据 + * 那么 + 30000 + * */ + if (isNextStep) { + //说明是新增了一段视频 + decodeInputTimeStamp += 30000; + isNextStep = false; + } else { + if (isFirstDecodeInputFrame) { + decodeInputTimeStamp = 0; + isFirstDecodeInputFrame = false; + } else { + decodeInputTimeStamp += mediaCodecInfo.extractor.getSampleTime() - lastInputTime; + } + } + lastInputTime = mediaCodecInfo.extractor.getSampleTime(); + /**将分离器的数据 插入解码器中*/ + decoder.queueInputBuffer(inputIndex, 0, readSampleData, decodeInputTimeStamp, 0); + mediaCodecInfo.extractor.advance(); + } + } + } + + /** + * 7、循环的第二步,轮询取出解码器解码完成的数据 并且加入到编码器中 + * */ + boolean decodeOutputDone = false; + boolean encodeDone = false; + while (!decodeOutputDone || !encodeDone) { + /**说明解码器的输出output有数据*/ + int outputIndex = decoder.dequeueOutputBuffer(decodeOutputInfo, TIMEOUT_USEC); + if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { + /**没有可用的解码器output*/ + decodeOutputDone = true; + } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + + } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + MediaFormat newFormat = decoder.getOutputFormat(); + } else if (outputIndex < 0) { + } else { + /** + * 8、判断本次是否有数据 以及本次数据是否需要传入编码器 + * */ + boolean doRender = (decodeOutputInfo.size != 0); + /** + * 9、根据当前解码出来的数据的时间戳 判断 是否需要写入编码器 + * */ + boolean isUseful = true; + if (decodeOutputInfo.presentationTimeUs <= 0) { + doRender = false; + } + Log.e("videoo", "---解码器解码出来的数据=+==" + decodeOutputInfo.presentationTimeUs + "----cutPoint==" + mediaCodecInfo.cutPoint + "--cutDuration==" + mediaCodecInfo.cutDuration); + if (mediaCodecInfo.cutDuration > 0 && decodeOutputInfo.presentationTimeUs / 1000 < mediaCodecInfo.cutPoint) { + /**还没有到剪切点,属于被放弃的数据*/ + isUseful = false; + Log.e("videoo", "---被抛弃的数据==头=="); + } + if (mediaCodecInfo.cutDuration > 0 && decodeOutputInfo.presentationTimeUs / 1000 > (mediaCodecInfo.cutPoint + mediaCodecInfo.cutDuration)) { + /**当前数据已经超过了剪切点 属于多余的数据*/ + isUseful = false; + Log.e("videoo", "---被抛弃的数据==尾=="); + } + decoder.releaseOutputBuffer(outputIndex, doRender && isUseful); + if (doRender && isUseful) { + /** + * 是有效数据 让他写到编码器中 + * 并且对时间戳 进行重写 + * */ + outputSurface.awaitNewImage(); + outputSurface.drawImage(); + if (isFirstDecodeOutputFrame) { + /**如果是第一个视频的话,有可能时间戳不是从0 开始的 所以需要初始化*/ + isFirstDecodeOutputFrame = false; + } else { + /** + * 如果是更换了一个视频源 就+30000us + */ + if (isChangeVideo) { + Log.e("videoo", "---更换了一个视频源===+30000"); + isChangeVideo = false; + encodeInputTimeStamp = (encodeInputTimeStamp + 30000); + } else { + encodeInputTimeStamp = (encodeInputTimeStamp + (decodeOutputInfo.presentationTimeUs - lastVideoTime)); + } + } + Log.e("videooo", "---在编码画面帧的时候,重置时间戳===" + encodeInputTimeStamp); + inputSurface.setPresentationTime(encodeInputTimeStamp * 1000); + inputSurface.swapBuffers(); + } + lastVideoTime = decodeOutputInfo.presentationTimeUs; + /**判断解码器是否解码完毕了*/ + if ((decodeOutputInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + /** + * 解码器解码完成了,说明该分离器的数据写入完成了 并且都已经解码完成了 + * 更换分离器和解码器或者结束编解码 + * */ + curVideoIndex++; + Log.e("send", "------收到了结束的标志位---当前是第几个video-"+curVideoIndex+"----videoList的size=="+mediaCodecInfos.size()); + if (curVideoIndex < mediaCodecInfos.size()) { + /**说明还有需要解码的*/ + /** + * 1)、更换分离器 + * 2)、更换解码器 + * */ + mediaCodecInfo.extractor.release(); + mediaCodecInfo = mediaCodecInfos.get(curVideoIndex); + + decoder.stop(); + decoder.release(); + decoder = decodeList.get(curVideoIndex); + decoder.configure(formatList.get(curVideoIndex), outputSurface.getSurface(), null, 0); + decoder.start(); + inputBuffers = decoder.getInputBuffers(); + outputSurface.onVideoSizeChanged(mVideoInfos.get(curVideoIndex)); + inputDone = false;//解码器继续写入数据 + isChangeVideo = true; + isNextStep = true; + Log.e("videoo", "---更换分离器 and 解码器---=="); + } else { + /**没有数据了 就给编码器发送一个结束的标志位*/ + videoEncoder.signalEndOfInputStream(); + inputDone = true; + Log.e("videoo", "---所有视频都解码完成了 告诉编码器 可以结束了---=="); + } + }else { + Log.e("send", "------没有完成 继续 go on"); + } + } + /** + * 10、从编码器中取数据 重写时间戳 添加到混合器中 + * 如果发生了更换视频源 那么 就先把编码器的所有数据读取出来 + * 然后再向解码器输入数据 以确保编码器时间戳的正确性 + * */ + while (!encodeDone) { + int encodeOutputState = videoEncoder.dequeueOutputBuffer(encodeOutputInfo, TIMEOUT_USEC); + if (encodeOutputState == MediaCodec.INFO_TRY_AGAIN_LATER) { + /** 说明没有可用的编码器 */ + encodeDone = true; + } else if (encodeOutputState == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + encoderOutputBuffers = videoEncoder.getOutputBuffers(); + } else if (encodeOutputState == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + /**初始化混合器的MediaFormat*/ + MediaFormat newFormat = videoEncoder.getOutputFormat(); + mMediaMuxer.addMeidaFormat(MediaMuxerRunnable.MEDIA_TRACK_VIDEO, newFormat); + Log.e("videoo", "---添加MediaFormat"); + } else if (encodeOutputState < 0) { + } else { + /**判断编码器是否完成了编码*/ + outputDone = (encodeOutputInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; + if (outputDone) { + break; + } + if ((encodeOutputInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + encodeOutputInfo.size = 0; + } + ByteBuffer encoderOutputBuffer = encoderOutputBuffers[encodeOutputState]; + if (encodeOutputInfo.size != 0) { + Log.e("videoo", "--写入混合器的数据----presentationTime===" + encodeOutputInfo.presentationTimeUs + "===size===" + encodeOutputInfo.size + "----flags==" + encodeOutputInfo.flags); + mMediaMuxer.addMuxerData(MediaMuxerRunnable.MEDIA_TRACK_VIDEO, encoderOutputBuffer, encodeOutputInfo); + } + videoEncoder.releaseOutputBuffer(encodeOutputState, false); + } + } + } + } + //释放最后一个decoder + decoder.stop(); + decoder.release(); + mMediaMuxer.videoIsOver(); + long end = System.currentTimeMillis(); + Log.e("timee", "---视频编码完成---视频编码耗时-==" + (end - start)); + } + + private void prepare() throws IOException { + mExtractors = new ArrayList<>(); + mMediaCodecInfos = new ArrayList<>(); + + //初始化所以的分离器 + int videoDuration = 0; + for (int i = 0; i < mVideoInfos.size(); i++) { + MediaExtractor temp = new MediaExtractor(); + VideoInfo videoInfo = mVideoInfos.get(i); + temp.setDataSource(videoInfo.path); + mExtractors.add(temp); + //多个视频剪切,根据视频所在位置 对本视频剪切点进行调整 + MediaDecode decode = new MediaDecode(); + decode.path = videoInfo.path; + decode.extractor = temp; + decode.cutPoint = videoInfo.cutPoint + videoDuration; + decode.cutDuration = videoInfo.cutDuration; + decode.duration = videoInfo.duration; + videoDuration += videoInfo.duration; + mMediaCodecInfos.add(decode); + } + + mExtractor = mExtractors.get(0);//先拿到第一个分离器 + int trackCount = mExtractor.getTrackCount(); + for (int i = 0; i < trackCount; i++) {//根据第一个视频信息来确定编码信息 + MediaFormat trackFormat = mExtractor.getTrackFormat(i); + String mime = trackFormat.getString(MediaFormat.KEY_MIME); + if (mime.startsWith("video/")) { + mInfo = mVideoInfos.get(0); + if (mInfo.rotation == 0 || mInfo.rotation == 180) { + videoOutputFormat = MediaFormat.createVideoFormat(MIME_TYPE, mInfo.width, mInfo.height); + } else { + videoOutputFormat = MediaFormat.createVideoFormat(MIME_TYPE, mInfo.height, mInfo.width); + } + videoOutputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, + android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + videoOutputFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); + videoOutputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); + videoOutputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, frameInterval); + break; + } + } + } +} diff --git a/app/src/main/java/com/example/cj/videoeditor/widget/TitleView.java b/app/src/main/java/com/example/cj/videoeditor/widget/TitleView.java new file mode 100644 index 0000000..f6d8a46 --- /dev/null +++ b/app/src/main/java/com/example/cj/videoeditor/widget/TitleView.java @@ -0,0 +1,307 @@ +package com.example.cj.videoeditor.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.TextView; + +import com.example.cj.videoeditor.R; + + +/** + * Created by guohuan on 2016/2/20. + * 标题栏控件 + */ +@SuppressWarnings("unused") +public class TitleView extends FrameLayout { + + // 左边返回键 + private ImageButton btn_title_bar_left; + // 右边btn键 + private ImageButton btn_title_bar_right; + + // 标题栏 + private TextView tv_title_bar_title; + + // 右边文字 + private TextView tv_title_bar_right_text; + // 左边文字 + private TextView tv_title_bar_left; + // 右边个数文字 + private TextView tv_title_bar_right_num; + private View title_root; + + public TitleView(Context context) { + super(context); + initView(context); + } + + public TitleView(Context context, AttributeSet attrs) { + super(context, attrs); + initView(context); + resolveAttr(context, attrs); + } + + public TitleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context); + resolveAttr(context, attrs); + } + + private void initView(Context ctx) { + View.inflate(ctx, R.layout.view_titlebar, this); + title_root = findViewById(R.id.title_root); + btn_title_bar_left = (ImageButton) findViewById(R.id.btn_title_bar_left); + btn_title_bar_right = (ImageButton) findViewById(R.id.btn_title_bar_right); + tv_title_bar_title = (TextView) findViewById(R.id.tv_title_bar_title); + tv_title_bar_right_text = (TextView) findViewById(R.id.tv_title_bar_right_text); + tv_title_bar_left = (TextView) findViewById(R.id.tv_title_bar_left); + tv_title_bar_right_num = (TextView) findViewById(R.id.tv_title_bar_right_num); + } + + // 解析属性 + private void resolveAttr(Context context, AttributeSet attrs) { + TypedArray typedArray = attrs == null ? null : context.obtainStyledAttributes(attrs, R.styleable.TitleView); + if (typedArray != null) { + String title = typedArray.getString(R.styleable.TitleView_title_text);// 标题 + if (!TextUtils.isEmpty(title)) { + tv_title_bar_title.setText(title); + } + String right_str = typedArray.getString(R.styleable.TitleView_right_text); //右侧文字 + if (!TextUtils.isEmpty(right_str)) { + tv_title_bar_right_text.setText(right_str); + tv_title_bar_right_text.setVisibility(View.VISIBLE); + } + String right_num = typedArray.getString(R.styleable.TitleView_right_num_text); //右侧文字 + if (!TextUtils.isEmpty(right_num)) { + tv_title_bar_right_num.setText(right_num); + tv_title_bar_right_num.setVisibility(View.VISIBLE); + } + String left_str = typedArray.getString(R.styleable.TitleView_left_text); //左侧文字 + if (!TextUtils.isEmpty(left_str)) { + tv_title_bar_left.setText(left_str); + tv_title_bar_left.setVisibility(View.VISIBLE); + btn_title_bar_left.setVisibility(View.INVISIBLE); + } + + Drawable right_pic = typedArray.getDrawable(R.styleable.TitleView_right_pic); + if (right_pic != null) { + btn_title_bar_right.setImageDrawable(right_pic); + btn_title_bar_right.setVisibility(View.VISIBLE); + } + Drawable left_pic = typedArray.getDrawable(R.styleable.TitleView_left_pic); + if (left_pic != null) { + btn_title_bar_left.setImageDrawable(left_pic); + btn_title_bar_left.setVisibility(View.VISIBLE); + } + //取消标题栏背景设置 + /*Drawable title_bg = typedArray.getDrawable(R.styleable.TitleView_title_background); + title_root.setBackground(title_bg);*/ + } + } + + // 设置右边图片 + public void setBtnRight(Drawable drawable) { + btn_title_bar_right.setImageDrawable(drawable); + tv_title_bar_right_text.setVisibility(View.INVISIBLE); + btn_title_bar_right.setVisibility(View.VISIBLE); + } + + public void setBtnRight(Bitmap bitmap) { + btn_title_bar_right.setImageBitmap(bitmap); + tv_title_bar_right_text.setVisibility(View.INVISIBLE); + btn_title_bar_right.setVisibility(View.VISIBLE); + } + + public void setBtnRight(int id) { + btn_title_bar_right.setImageResource(id); + tv_title_bar_right_text.setVisibility(View.INVISIBLE); + btn_title_bar_right.setVisibility(View.VISIBLE); + } + + // 设置右边文字 + public void setTvRight(String str) { + tv_title_bar_right_text.setText(str); + tv_title_bar_right_text.setVisibility(View.VISIBLE); + btn_title_bar_right.setVisibility(View.INVISIBLE); + } + + public void setTvRight(int id) { + tv_title_bar_right_text.setText(id); + tv_title_bar_right_text.setVisibility(View.VISIBLE); + btn_title_bar_right.setVisibility(View.INVISIBLE); + } + + // 设置右边个数文字 + public void setTvRightNum(String str) { + tv_title_bar_right_num.setText(str); + tv_title_bar_right_num.setVisibility(View.VISIBLE); + tv_title_bar_right_text.setVisibility(View.VISIBLE); + btn_title_bar_right.setVisibility(View.INVISIBLE); + } + + /** + * 设置右边的个数文字是否显示 + * + * @param visibility + */ + public void setTvRightNumVisibile(int visibility) { + tv_title_bar_right_num.setVisibility(visibility); + } + + // 设置左边图标 + public void setBtnLeft(int id) { + btn_title_bar_left.setImageResource(id); + } + + // 设置左边图标 + public void setBtnLeft(Drawable drawable) { + btn_title_bar_left.setImageDrawable(drawable); + } + + // 设置左边图标是否显示 + public void setBtnLeftVisible(int vis) { + btn_title_bar_left.setVisibility(vis); + } + + // 设置标题 + public void setTitle(String title) { + tv_title_bar_title.setText(title); + } + + public void setTitle(int id) { + tv_title_bar_title.setText(id); + } + + public void setTitleColor(int colorId) { + tv_title_bar_title.setTextColor(colorId); + } + + /** + * 设置左边点击事件 + * + * @param listener + */ + public void setBtnLeftOnClick(OnClickListener listener) { + btn_title_bar_left.setOnClickListener(listener); + } + + /** + * 设置右边btn点击事件 + * + * @param listener + */ + public void setBtnRightOnClick(OnClickListener listener) { + btn_title_bar_right.setOnClickListener(listener); + } + + /** + * 设置右边tv 点击事件 + * + * @param listener + */ + public void setTvRightOnClick(OnClickListener listener) { + tv_title_bar_right_text.setOnClickListener(listener); + } + + /** + * 设置左侧 文字点击事件 + * + * @param listener + */ + public void setTvLeftOnclick(OnClickListener listener) { + tv_title_bar_left.setOnClickListener(listener); + } + + /** + * 设置标题栏点击事件 + * + * @param listener + */ + public void setOnTitleClick(OnClickListener listener) { + tv_title_bar_title.setBackgroundResource(R.drawable.selector_title_bar_btn); + tv_title_bar_title.setOnClickListener(listener); + } + + /** + * 设置右边按钮是否可点击 默认可以 + */ + public void setBtnRightClickable(boolean b) { + btn_title_bar_right.setClickable(b); + } + + /** + * 设置右边文字是否可点击 默认可以 + */ + public void setTvRightClickable(boolean b) { + tv_title_bar_right_text.setClickable(b); + } + + /** + * 获取右边textView 对象 + */ + public TextView getRightTV() { + return tv_title_bar_right_text; + } + + + /** + * @return 获取右侧按钮 + */ + public ImageButton getRightBtn() { + return btn_title_bar_right; + } + + /** + * 获取标题栏textView + */ + public TextView getTitleTV() { + return tv_title_bar_title; + } + + + /** + * 设置右边textView是否可用 + * + * @param enabled + */ + public void setTvRightEnabled(boolean enabled) { + tv_title_bar_right_text.setEnabled(enabled); + } + + + public View getLeftBtn() { + return btn_title_bar_left; + } + + /** + * 设置右边textView是否激活 + * + * @param activated + */ + public void setTvRightActivated(boolean activated) { + tv_title_bar_right_text.setActivated(activated); + } + + public void hideRightBtn() { + btn_title_bar_right.setVisibility(INVISIBLE); + } + + public void hideBackBtn() { + if (btn_title_bar_left != null) { + btn_title_bar_left.setVisibility(GONE); + } + } + + public void showBackBtn() { + if (btn_title_bar_left != null) { + btn_title_bar_left.setVisibility(VISIBLE); + } + } +} diff --git a/app/src/main/java/com/example/cj/videoeditor/widget/VideoPreviewView.java b/app/src/main/java/com/example/cj/videoeditor/widget/VideoPreviewView.java index 33b2b7d..90b96c2 100644 --- a/app/src/main/java/com/example/cj/videoeditor/widget/VideoPreviewView.java +++ b/app/src/main/java/com/example/cj/videoeditor/widget/VideoPreviewView.java @@ -56,6 +56,12 @@ private void init(Context context) { public void setVideoPath(List paths){ mMediaPlayer.setDataSource(paths); } + /** + * 是否设置水印 + * */ + public void clearWaterMark(){ + mDrawer.clearWaterMark(); + } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { mDrawer.onSurfaceCreated(gl,config); @@ -172,6 +178,12 @@ public void seekTo(int time){ public int getVideoDuration(){ return mMediaPlayer.getCurVideoDuration(); } + /** + * 获取当前播放的视频的列表 + * */ + public List getVideoInfo(){ + return mMediaPlayer.getVideoInfo(); + } /** * 切换美颜状态 diff --git a/app/src/main/res/drawable/app_back_selector.xml b/app/src/main/res/drawable/app_back_selector.xml new file mode 100644 index 0000000..7d13559 --- /dev/null +++ b/app/src/main/res/drawable/app_back_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_circle_yellow.xml b/app/src/main/res/drawable/bg_circle_yellow.xml new file mode 100644 index 0000000..890d461 --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_yellow.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_btn_image_choose.xml b/app/src/main/res/drawable/selector_btn_image_choose.xml new file mode 100644 index 0000000..3617776 --- /dev/null +++ b/app/src/main/res/drawable/selector_btn_image_choose.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_title_bar_btn.xml b/app/src/main/res/drawable/selector_title_bar_btn.xml new file mode 100644 index 0000000..6578072 --- /dev/null +++ b/app/src/main/res/drawable/selector_title_bar_btn.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/setting_text_color_selector.xml b/app/src/main/res/drawable/setting_text_color_selector.xml new file mode 100644 index 0000000..f1e19c8 --- /dev/null +++ b/app/src/main/res/drawable/setting_text_color_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_connect.xml b/app/src/main/res/layout/activity_connect.xml new file mode 100644 index 0000000..d123977 --- /dev/null +++ b/app/src/main/res/layout/activity_connect.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 29b4783..bb7823e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -2,6 +2,7 @@ - +