├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── chenxf │ │ └── videocomposer │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── chenxf │ │ │ └── videocomposer │ │ │ ├── MainActivity.java │ │ │ └── VideoComposer.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── chenxf │ └── videocomposer │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties └── settings.gradle /README.md: -------------------------------------------------------------------------------- 1 | # VideoComposerDemo 2 | 3 | 将2个视频文件,拼接为一个文件,拼接前提是,2个文件的编码信息一样,包括video codec(H264的话,包括profile/level) 4 | video width/height, audio codec, audio sample rate, channel number 5 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion "25.0.3" 6 | defaultConfig { 7 | applicationId "com.chenxf.videocomposer" 8 | minSdkVersion 19 9 | targetSdkVersion 26 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:26.0.0-alpha1' 28 | testCompile 'junit:junit:4.12' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/chenxf/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/chenxf/videocomposer/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.chenxf.videocomposer; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.chenxf.videocomposer", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/chenxf/videocomposer/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.chenxf.videocomposer; 2 | 3 | import android.os.Handler; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | import android.view.View; 8 | import android.widget.Button; 9 | import android.widget.Toast; 10 | 11 | import java.util.ArrayList; 12 | 13 | /** 14 | * 将2个视频文件,拼接为一个文件,拼接前提是,2个文件的编码信息一样,包括video codec(H264的话,包括profile/level) 15 | * video width/height, audio codec, audio sample rate, channel number 16 | */ 17 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 18 | 19 | private static final String TAG = "MainActivity"; 20 | private Button mButton; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | setContentView(R.layout.activity_main); 26 | mButton = (Button) findViewById(R.id.button); 27 | mButton.setOnClickListener(this); 28 | } 29 | 30 | @Override 31 | protected void onResume() { 32 | super.onResume(); 33 | 34 | } 35 | 36 | private void startCompose() { 37 | new Thread(new Runnable() { 38 | @Override 39 | public void run() { 40 | ArrayList videoList = new ArrayList<>(); 41 | //待合成的2个视频文件 42 | videoList.add("/storage/emulated/0/DCIM/test1.mp4"); 43 | videoList.add("/storage/emulated/0/DCIM/test2.mp4"); 44 | VideoComposer composer = new VideoComposer(videoList, "/storage/emulated/0/DCIM/out.mp4"); 45 | final boolean result = composer.joinVideo(); 46 | 47 | mButton.post(new Runnable() { 48 | @Override 49 | public void run() { 50 | Toast.makeText(MainActivity.this, "合成结果 " + result, Toast.LENGTH_LONG); 51 | } 52 | }); 53 | Log.i(TAG, "compose result: " + result); 54 | } 55 | }).start(); 56 | } 57 | 58 | @Override 59 | public void onClick(View view) { 60 | if(view.getId() == R.id.button) { 61 | startCompose(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/chenxf/videocomposer/VideoComposer.java: -------------------------------------------------------------------------------- 1 | package com.chenxf.videocomposer; 2 | 3 | import android.annotation.TargetApi; 4 | import android.media.MediaCodec; 5 | import android.media.MediaCodec.BufferInfo; 6 | import android.media.MediaExtractor; 7 | import android.media.MediaFormat; 8 | import android.media.MediaMuxer; 9 | import android.util.Log; 10 | 11 | import java.io.IOException; 12 | import java.nio.ByteBuffer; 13 | import java.util.ArrayList; 14 | import java.util.Iterator; 15 | 16 | @TargetApi(18) 17 | public class VideoComposer { 18 | private final String TAG = "VideoComposer"; 19 | private ArrayList mVideoList; 20 | private String mOutFilename; 21 | 22 | private MediaMuxer mMuxer; 23 | private ByteBuffer mReadBuf; 24 | private int mOutAudioTrackIndex; 25 | private int mOutVideoTrackIndex; 26 | private MediaFormat mAudioFormat; 27 | private MediaFormat mVideoFormat; 28 | 29 | public VideoComposer(ArrayList videoList, String outFilename) { 30 | mVideoList = videoList; 31 | this.mOutFilename = outFilename; 32 | mReadBuf = ByteBuffer.allocate(1048576); 33 | } 34 | 35 | public boolean joinVideo() { 36 | boolean getAudioFormat = false; 37 | boolean getVideoFormat = false; 38 | Iterator videoIterator = mVideoList.iterator(); 39 | 40 | //--------step 1 MediaExtractor拿到多媒体信息,用于MediaMuxer创建文件 41 | while(videoIterator.hasNext()) { 42 | String videoPath = (String)videoIterator.next(); 43 | MediaExtractor extractor = new MediaExtractor(); 44 | 45 | try { 46 | extractor.setDataSource(videoPath); 47 | } catch (Exception ex) { 48 | ex.printStackTrace(); 49 | } 50 | 51 | int trackIndex; 52 | if(!getVideoFormat) { 53 | trackIndex = this.selectTrack(extractor, "video/"); 54 | if(trackIndex < 0) { 55 | Log.e(TAG, "No video track found in " + videoPath); 56 | } else { 57 | extractor.selectTrack(trackIndex); 58 | mVideoFormat = extractor.getTrackFormat(trackIndex); 59 | getVideoFormat = true; 60 | } 61 | } 62 | 63 | if(!getAudioFormat) { 64 | trackIndex = this.selectTrack(extractor, "audio/"); 65 | if(trackIndex < 0) { 66 | Log.e(TAG, "No audio track found in " + videoPath); 67 | } else { 68 | extractor.selectTrack(trackIndex); 69 | mAudioFormat = extractor.getTrackFormat(trackIndex); 70 | getAudioFormat = true; 71 | } 72 | } 73 | 74 | extractor.release(); 75 | if(getVideoFormat && getAudioFormat) { 76 | break; 77 | } 78 | } 79 | 80 | try { 81 | mMuxer = new MediaMuxer(this.mOutFilename, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); 82 | } catch (IOException e) { 83 | e.printStackTrace(); 84 | } 85 | if(getVideoFormat) { 86 | mOutVideoTrackIndex = mMuxer.addTrack(mVideoFormat); 87 | } 88 | if(getAudioFormat) { 89 | mOutAudioTrackIndex = mMuxer.addTrack(mAudioFormat); 90 | } 91 | mMuxer.start(); 92 | //--------step 1 end---------------------------// 93 | 94 | 95 | //--------step 2 遍历文件,MediaExtractor读取帧数据,MediaMuxer写入帧数据,并记录帧信息 96 | long ptsOffset = 0L; 97 | Iterator trackIndex = mVideoList.iterator(); 98 | while(trackIndex.hasNext()) { 99 | String videoPath = (String)trackIndex.next(); 100 | boolean hasVideo = true; 101 | boolean hasAudio = true; 102 | MediaExtractor videoExtractor = new MediaExtractor(); 103 | 104 | try { 105 | videoExtractor.setDataSource(videoPath); 106 | } catch (Exception var27) { 107 | var27.printStackTrace(); 108 | } 109 | 110 | int inVideoTrackIndex = this.selectTrack(videoExtractor, "video/"); 111 | if(inVideoTrackIndex < 0) { 112 | hasVideo = false; 113 | } 114 | 115 | videoExtractor.selectTrack(inVideoTrackIndex); 116 | MediaExtractor audioExtractor = new MediaExtractor(); 117 | 118 | try { 119 | audioExtractor.setDataSource(videoPath); 120 | } catch (Exception e) { 121 | e.printStackTrace(); 122 | } 123 | 124 | int inAudioTrackIndex = this.selectTrack(audioExtractor, "audio/"); 125 | if(inAudioTrackIndex < 0) { 126 | hasAudio = false; 127 | } 128 | 129 | audioExtractor.selectTrack(inAudioTrackIndex); 130 | boolean bMediaDone = false; 131 | long presentationTimeUs = 0L; 132 | long audioPts = 0L; 133 | long videoPts = 0L; 134 | 135 | while(!bMediaDone) { 136 | if(!hasVideo && !hasAudio) { 137 | break; 138 | } 139 | 140 | int outTrackIndex; 141 | MediaExtractor extractor; 142 | int currenttrackIndex; 143 | if((!hasVideo || audioPts - videoPts <= 50000L) && hasAudio) { 144 | currenttrackIndex = inAudioTrackIndex; 145 | outTrackIndex = mOutAudioTrackIndex; 146 | extractor = audioExtractor; 147 | } else { 148 | currenttrackIndex = inVideoTrackIndex; 149 | outTrackIndex = mOutVideoTrackIndex; 150 | extractor = videoExtractor; 151 | } 152 | 153 | mReadBuf.rewind(); 154 | int chunkSize = extractor.readSampleData(mReadBuf, 0);//读取帧数据 155 | if(chunkSize < 0) { 156 | if(currenttrackIndex == inVideoTrackIndex) { 157 | hasVideo = false; 158 | } else if(currenttrackIndex == inAudioTrackIndex) { 159 | hasAudio = false; 160 | } 161 | } else { 162 | if(extractor.getSampleTrackIndex() != currenttrackIndex) { 163 | Log.e(TAG, "WEIRD: got sample from track " + extractor.getSampleTrackIndex() + ", expected " + currenttrackIndex); 164 | } 165 | 166 | presentationTimeUs = extractor.getSampleTime();//读取帧的pts 167 | if(currenttrackIndex == inVideoTrackIndex) { 168 | videoPts = presentationTimeUs; 169 | } else { 170 | audioPts = presentationTimeUs; 171 | } 172 | 173 | BufferInfo info = new BufferInfo(); 174 | info.offset = 0; 175 | info.size = chunkSize; 176 | info.presentationTimeUs = ptsOffset + presentationTimeUs;//pts重新计算 177 | if((extractor.getSampleFlags() & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { 178 | info.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME; 179 | } 180 | 181 | mReadBuf.rewind(); 182 | Log.i(TAG, String.format("write sample track %d, size %d, pts %d flag %d", new Object[]{Integer.valueOf(outTrackIndex), Integer.valueOf(info.size), Long.valueOf(info.presentationTimeUs), Integer.valueOf(info.flags)})); 183 | mMuxer.writeSampleData(outTrackIndex, mReadBuf, info);//写入文件 184 | extractor.advance(); 185 | } 186 | } 187 | 188 | //记录当前文件的最后一个pts,作为下一个文件的pts offset 189 | ptsOffset += videoPts > audioPts ? videoPts : audioPts; 190 | ptsOffset += 10000L;//前一个文件的最后一帧与后一个文件的第一帧,差10ms,只是估计值,不准确,但能用 191 | 192 | Log.i(TAG, "finish one file, ptsOffset " + ptsOffset); 193 | 194 | videoExtractor.release(); 195 | audioExtractor.release(); 196 | } 197 | 198 | if(mMuxer != null) { 199 | try { 200 | mMuxer.stop(); 201 | mMuxer.release(); 202 | } catch (Exception e) { 203 | Log.e(TAG, "Muxer close error. No data was written"); 204 | } 205 | 206 | mMuxer = null; 207 | } 208 | 209 | Log.i(TAG, "video join finished"); 210 | return true; 211 | } 212 | 213 | private int selectTrack(MediaExtractor extractor, String mimePrefix) { 214 | int numTracks = extractor.getTrackCount(); 215 | 216 | for(int i = 0; i < numTracks; ++i) { 217 | MediaFormat format = extractor.getTrackFormat(i); 218 | String mime = format.getString("mime"); 219 | if(mime.startsWith(mimePrefix)) { 220 | return i; 221 | } 222 | } 223 | 224 | return -1; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 17 | 18 |