|
|
@@ -0,0 +1,359 @@
|
|
|
+package com.sheep.gamegroup.util;
|
|
|
+
|
|
|
+import android.media.MediaCodec;
|
|
|
+import android.media.MediaExtractor;
|
|
|
+import android.media.MediaFormat;
|
|
|
+import android.media.MediaMuxer;
|
|
|
+import android.util.Log;
|
|
|
+
|
|
|
+import com.sheep.gamegroup.absBase.AbsObserver;
|
|
|
+import com.sheep.gamegroup.model.entity.Video;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.nio.ByteBuffer;
|
|
|
+import java.util.Locale;
|
|
|
+
|
|
|
+import io.reactivex.Observable;
|
|
|
+import io.reactivex.ObservableEmitter;
|
|
|
+import io.reactivex.ObservableOnSubscribe;
|
|
|
+import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
|
+import io.reactivex.schedulers.Schedulers;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Created by realicing on 2018/11/30.
|
|
|
+ * realicing@sina.com
|
|
|
+ */
|
|
|
+public class MediaHandleUtil {
|
|
|
+ public static final String TAG = "MediaHandleUtil";
|
|
|
+ private String url;
|
|
|
+ private long clipPoint;
|
|
|
+ private long clipDuration;
|
|
|
+
|
|
|
+ public MediaHandleUtil(String url, long clipPoint, long clipDuration) {
|
|
|
+ this.url = url;
|
|
|
+ this.clipPoint = clipPoint;
|
|
|
+ this.clipDuration = clipDuration;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void tryCutVideo(AbsObserver<Video> absObserver) {
|
|
|
+ Observable.create(new ObservableOnSubscribe<Video>() {
|
|
|
+ @Override
|
|
|
+ public void subscribe(ObservableEmitter<Video> emitter) throws Exception {
|
|
|
+ long time = System.currentTimeMillis();
|
|
|
+ Video video = cutVideo(url, clipPoint, clipDuration);
|
|
|
+ if (video == null) {
|
|
|
+ emitter.onError(new Throwable("剪切失败"));
|
|
|
+ } else {
|
|
|
+ emitter.onNext(video);
|
|
|
+ }
|
|
|
+ long curTime = System.currentTimeMillis();
|
|
|
+ LogUtil.println(TAG, time, curTime, curTime - time, (curTime - time) / 1000);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
+ .observeOn(AndroidSchedulers.mainThread())
|
|
|
+ .subscribe(absObserver);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取可以剪切出的视频的地址
|
|
|
+ *
|
|
|
+ * @param url 源文本地址
|
|
|
+ * @param clipPoint 开始位置
|
|
|
+ * @param clipDuration 要剪切的长度
|
|
|
+ * @return 返回可以剪切出来的文件的地址
|
|
|
+ */
|
|
|
+ public static String getCutVideoFilePath(String url, long clipPoint, long clipDuration) {
|
|
|
+ if (clipPoint < 0) {
|
|
|
+ Log.e(TAG, "clipPoint is error! but reset clipPoint " + clipPoint + "0 ");
|
|
|
+ clipPoint = 0;
|
|
|
+ }
|
|
|
+ return String.format(Locale.CHINA, "%s_%d_%d.%s", url.substring(0, url.lastIndexOf(".")), clipPoint, clipDuration, FileUtil.getExtensionName(url));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 剪切视频
|
|
|
+ *
|
|
|
+ * @param url 源文本地址
|
|
|
+ * @param clipPoint 开始位置
|
|
|
+ * @param clipDuration 要剪切的长度
|
|
|
+ * @return 返回剪切出来的文件的地址
|
|
|
+ */
|
|
|
+ public static Video cutVideo(String url, long clipPoint, long clipDuration) {
|
|
|
+ Video video = new Video();
|
|
|
+ if (clipPoint < 0) {
|
|
|
+ Log.e(TAG, "clipPoint is error! but reset clipPoint " + clipPoint + "0 ");
|
|
|
+ clipPoint = 0;
|
|
|
+ }
|
|
|
+ LogUtil.println(TAG, url, clipPoint, clipDuration);
|
|
|
+ String outFilePath = getCutVideoFilePath(url, clipPoint, clipDuration);
|
|
|
+ video.setFilePath(outFilePath);
|
|
|
+ int videoTrackIndex = -1;
|
|
|
+ int audioTrackIndex = -1;
|
|
|
+ int videoMaxInputSize = 0;
|
|
|
+ int audioMaxInputSize = 0;
|
|
|
+ int sourceVTrack = 0;
|
|
|
+ int sourceATrack = 0;
|
|
|
+ long videoDuration, audioDuration;
|
|
|
+ //创建分离器
|
|
|
+ MediaExtractor mediaExtractor = new MediaExtractor();
|
|
|
+ MediaMuxer mediaMuxer = null;
|
|
|
+ MediaFormat mediaFormat;
|
|
|
+
|
|
|
+ try {
|
|
|
+ //设置文件路径
|
|
|
+ mediaExtractor.setDataSource(url);
|
|
|
+ //创建合成器
|
|
|
+ mediaMuxer = new MediaMuxer(outFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
|
|
+ String mime;
|
|
|
+ //获取每个轨道的信息
|
|
|
+ for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {
|
|
|
+ mediaFormat = mediaExtractor.getTrackFormat(i);
|
|
|
+ mime = mediaFormat.getString(MediaFormat.KEY_MIME);
|
|
|
+ if (mime.startsWith("video/")) {
|
|
|
+ sourceVTrack = i;
|
|
|
+ int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
|
|
|
+ int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
|
|
|
+ videoMaxInputSize = mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
|
|
|
+ videoDuration = mediaFormat.getLong(MediaFormat.KEY_DURATION);
|
|
|
+ //检测剪辑点和剪辑时长是否正确
|
|
|
+ if (clipPoint >= videoDuration) {
|
|
|
+ Log.e(TAG, "clip point is error! but reset clipPoint " + clipPoint + " 0 ");
|
|
|
+ clipPoint = 0;
|
|
|
+ }
|
|
|
+ if ((clipDuration + clipPoint) > videoDuration) {
|
|
|
+ if (clipPoint == 0) {
|
|
|
+ Log.e(TAG, "clip duration is error!"
|
|
|
+ + ";duration is " + videoDuration
|
|
|
+ + ";clipPoint is " + clipPoint
|
|
|
+ + ";clipDuration is " + clipDuration
|
|
|
+ + ";totalDuration is " + (clipDuration + clipPoint)
|
|
|
+ );
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Log.e(TAG, "clip duration is error! but reset clipDuration " + clipDuration + " " + (videoDuration - clipPoint));
|
|
|
+ clipDuration = videoDuration - clipPoint;
|
|
|
+ }
|
|
|
+ Log.d(TAG, "width and height is " + width + " " + height
|
|
|
+ + ";maxInputSize is " + videoMaxInputSize
|
|
|
+ + ";duration is " + videoDuration
|
|
|
+ + ";clipPoint is " + clipPoint
|
|
|
+ + ";clipDuration is " + clipDuration
|
|
|
+ + ";totalDuration is " + (clipDuration + clipPoint)
|
|
|
+ );
|
|
|
+ video.setWidth(width);
|
|
|
+ video.setHeight(height);
|
|
|
+ video.setDuration(clipDuration / 1000);//微秒转换为毫秒
|
|
|
+ //向合成器添加视频轨
|
|
|
+ videoTrackIndex = mediaMuxer.addTrack(mediaFormat);
|
|
|
+ } else if (mime.startsWith("audio/")) {
|
|
|
+ sourceATrack = i;
|
|
|
+ int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
|
|
+ int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
|
|
+ audioMaxInputSize = mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
|
|
|
+ audioDuration = mediaFormat.getLong(MediaFormat.KEY_DURATION);
|
|
|
+ Log.d(TAG, "sampleRate is " + sampleRate
|
|
|
+ + ";channelCount is " + channelCount
|
|
|
+ + ";audioMaxInputSize is " + audioMaxInputSize
|
|
|
+ + ";audioDuration is " + audioDuration
|
|
|
+ );
|
|
|
+ //添加音轨
|
|
|
+ audioTrackIndex = mediaMuxer.addTrack(mediaFormat);
|
|
|
+ }
|
|
|
+ Log.d(TAG, "file mime is " + mime);
|
|
|
+ }
|
|
|
+ //分配缓冲
|
|
|
+ ByteBuffer inputBuffer = ByteBuffer.allocate(videoMaxInputSize);
|
|
|
+ //根据官方文档的解释MediaMuxer的start一定要在addTrack之后
|
|
|
+ mediaMuxer.start();
|
|
|
+ //视频处理部分
|
|
|
+ mediaExtractor.selectTrack(sourceVTrack);
|
|
|
+ MediaCodec.BufferInfo videoInfo = new MediaCodec.BufferInfo();
|
|
|
+ videoInfo.presentationTimeUs = 0;
|
|
|
+ long videoSampleTime;
|
|
|
+ //获取源视频相邻帧之间的时间间隔。(1)
|
|
|
+ {
|
|
|
+ mediaExtractor.readSampleData(inputBuffer, 0);
|
|
|
+ //skip first I frame
|
|
|
+ if (mediaExtractor.getSampleFlags() == MediaExtractor.SAMPLE_FLAG_SYNC)
|
|
|
+ mediaExtractor.advance();
|
|
|
+ mediaExtractor.readSampleData(inputBuffer, 0);
|
|
|
+ long firstVideoPTS = mediaExtractor.getSampleTime();
|
|
|
+ mediaExtractor.advance();
|
|
|
+ mediaExtractor.readSampleData(inputBuffer, 0);
|
|
|
+ long SecondVideoPTS = mediaExtractor.getSampleTime();
|
|
|
+ videoSampleTime = Math.abs(SecondVideoPTS - firstVideoPTS);
|
|
|
+ Log.d(TAG, "videoSampleTime is " + videoSampleTime);
|
|
|
+ }
|
|
|
+ //选择起点
|
|
|
+ mediaExtractor.seekTo(clipPoint, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
|
|
|
+ while (true) {
|
|
|
+ int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
|
|
|
+ if (sampleSize < 0) {
|
|
|
+ //这里一定要释放选择的轨道,不然另一个轨道就无法选中了
|
|
|
+ mediaExtractor.unselectTrack(sourceVTrack);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ int trackIndex = mediaExtractor.getSampleTrackIndex();
|
|
|
+ //获取时间戳
|
|
|
+ long presentationTimeUs = mediaExtractor.getSampleTime();
|
|
|
+ //获取帧类型,只能识别是否为I帧
|
|
|
+ int sampleFlag = mediaExtractor.getSampleFlags();
|
|
|
+ Log.d(TAG, "trackIndex is " + trackIndex
|
|
|
+ + ";presentationTimeUs is " + presentationTimeUs
|
|
|
+ + ";sampleFlag is " + sampleFlag
|
|
|
+ + ";sampleSize is " + sampleSize);
|
|
|
+ //剪辑时间到了就跳出
|
|
|
+ if ((clipDuration != 0) && (presentationTimeUs > (clipPoint + clipDuration))) {
|
|
|
+ mediaExtractor.unselectTrack(sourceVTrack);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ mediaExtractor.advance();
|
|
|
+ videoInfo.offset = 0;
|
|
|
+ videoInfo.size = sampleSize;
|
|
|
+ videoInfo.flags = sampleFlag;
|
|
|
+ mediaMuxer.writeSampleData(videoTrackIndex, inputBuffer, videoInfo);
|
|
|
+ videoInfo.presentationTimeUs += videoSampleTime;//presentationTimeUs;
|
|
|
+ }
|
|
|
+ //音频部分
|
|
|
+ mediaExtractor.selectTrack(sourceATrack);
|
|
|
+ MediaCodec.BufferInfo audioInfo = new MediaCodec.BufferInfo();
|
|
|
+ audioInfo.presentationTimeUs = 0;
|
|
|
+ long audioSampleTime;
|
|
|
+ //获取音频帧时长
|
|
|
+ {
|
|
|
+ mediaExtractor.readSampleData(inputBuffer, 0);
|
|
|
+ //skip first sample
|
|
|
+ if (mediaExtractor.getSampleTime() == 0)
|
|
|
+ mediaExtractor.advance();
|
|
|
+ mediaExtractor.readSampleData(inputBuffer, 0);
|
|
|
+ long firstAudioPTS = mediaExtractor.getSampleTime();
|
|
|
+ mediaExtractor.advance();
|
|
|
+ mediaExtractor.readSampleData(inputBuffer, 0);
|
|
|
+ long SecondAudioPTS = mediaExtractor.getSampleTime();
|
|
|
+ audioSampleTime = Math.abs(SecondAudioPTS - firstAudioPTS);
|
|
|
+ Log.d(TAG, "AudioSampleTime is " + audioSampleTime);
|
|
|
+ }
|
|
|
+ mediaExtractor.seekTo(clipPoint, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
|
|
|
+ while (true) {
|
|
|
+ int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
|
|
|
+ if (sampleSize < 0) {
|
|
|
+ mediaExtractor.unselectTrack(sourceATrack);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ int trackIndex = mediaExtractor.getSampleTrackIndex();
|
|
|
+ long presentationTimeUs = mediaExtractor.getSampleTime();
|
|
|
+ Log.d(TAG, "trackIndex is " + trackIndex
|
|
|
+ + ";presentationTimeUs is " + presentationTimeUs);
|
|
|
+ if ((clipDuration != 0) && (presentationTimeUs > (clipPoint + clipDuration))) {
|
|
|
+ mediaExtractor.unselectTrack(sourceATrack);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ mediaExtractor.advance();
|
|
|
+ audioInfo.offset = 0;
|
|
|
+ audioInfo.size = sampleSize;
|
|
|
+ mediaMuxer.writeSampleData(audioTrackIndex, inputBuffer, audioInfo);
|
|
|
+ audioInfo.presentationTimeUs += audioSampleTime;//presentationTimeUs;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ Log.e(TAG, " read error " + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ //全部写完后释放MediaMuxer和MediaExtractor
|
|
|
+ if (mediaMuxer != null) {
|
|
|
+ mediaMuxer.stop();
|
|
|
+ mediaMuxer.release();
|
|
|
+ }
|
|
|
+ mediaExtractor.release();
|
|
|
+ }
|
|
|
+ return video;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void mixVideAndAudio(File outVideoPath, File outAudioPath, File outMergeVideoPath) throws Exception {
|
|
|
+ //分别初始化视频、音频的Extractor
|
|
|
+ MediaExtractor videoExtractor = new MediaExtractor();
|
|
|
+ videoExtractor.setDataSource(outVideoPath.getAbsolutePath());
|
|
|
+ MediaExtractor audioExtractor = new MediaExtractor();
|
|
|
+ audioExtractor.setDataSource(outAudioPath.getAbsolutePath());
|
|
|
+ int videoTrack = getTrack(videoExtractor, "video/");
|
|
|
+ int audioTrack = getTrack(audioExtractor, "audio/");
|
|
|
+ videoExtractor.selectTrack(videoTrack);
|
|
|
+ audioExtractor.selectTrack(audioTrack);
|
|
|
+ MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo();
|
|
|
+ MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();
|
|
|
+ //写入新的视频
|
|
|
+ if (outMergeVideoPath.exists() && outMergeVideoPath.createNewFile())
|
|
|
+ ;
|
|
|
+ ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024);
|
|
|
+
|
|
|
+ MediaMuxer mediaMuxer = new MediaMuxer(outMergeVideoPath.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
|
|
+ int writeVideoTrackIndex = mediaMuxer.addTrack(videoExtractor.getTrackFormat(videoTrack));
|
|
|
+ int writeAudioTrackIndex = mediaMuxer.addTrack(audioExtractor.getTrackFormat(audioTrack));
|
|
|
+ mediaMuxer.start();
|
|
|
+ long videoSampleTime = getSampleTime(videoExtractor, byteBuffer, videoTrack);
|
|
|
+ while (true) {
|
|
|
+ int data = videoExtractor.readSampleData(byteBuffer, 0);
|
|
|
+ if (data < 0) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ videoBufferInfo.size = data;
|
|
|
+ videoBufferInfo.offset = 0;
|
|
|
+ videoBufferInfo.flags = videoExtractor.getSampleFlags();
|
|
|
+ videoBufferInfo.presentationTimeUs += videoSampleTime;
|
|
|
+ mediaMuxer.writeSampleData(writeVideoTrackIndex, byteBuffer, videoBufferInfo);
|
|
|
+ videoExtractor.advance();
|
|
|
+ }
|
|
|
+ long audioSampleTime = getSampleTime(audioExtractor, byteBuffer, audioTrack);
|
|
|
+ while (true) {
|
|
|
+ int data = audioExtractor.readSampleData(byteBuffer, 0);
|
|
|
+ if (data < 0) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ audioBufferInfo.size = data;
|
|
|
+ audioBufferInfo.offset = 0;
|
|
|
+ audioBufferInfo.flags = audioExtractor.getSampleFlags();
|
|
|
+ audioBufferInfo.presentationTimeUs += audioSampleTime;
|
|
|
+ mediaMuxer.writeSampleData(writeAudioTrackIndex, byteBuffer, audioBufferInfo);
|
|
|
+ audioExtractor.advance();
|
|
|
+ }
|
|
|
+ Log.i("video", "合并完成");
|
|
|
+ mediaMuxer.stop();
|
|
|
+ mediaMuxer.release();
|
|
|
+ videoExtractor.release();
|
|
|
+ audioExtractor.release();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static long getSampleTime(MediaExtractor mediaExtractor, ByteBuffer byteBuffer, int videoTrack) {
|
|
|
+ mediaExtractor.readSampleData(byteBuffer, 0);
|
|
|
+ //跳过I帧,要P帧(视频是由个别I帧和很多P帧组成)h264编码中有IBP帧 I为关键帧。
|
|
|
+ if (mediaExtractor.getSampleFlags() == MediaExtractor.SAMPLE_FLAG_SYNC) {
|
|
|
+ mediaExtractor.advance();
|
|
|
+ }
|
|
|
+ mediaExtractor.readSampleData(byteBuffer, 0);
|
|
|
+
|
|
|
+ // 得到第一帧的PTS
|
|
|
+ long firstVideoPTS = mediaExtractor.getSampleTime();
|
|
|
+ //下一帧
|
|
|
+ mediaExtractor.advance();
|
|
|
+ mediaExtractor.readSampleData(byteBuffer, 0);
|
|
|
+ long secondVideoPTS = mediaExtractor.getSampleTime();
|
|
|
+ long sampleTime = Math.abs(secondVideoPTS - firstVideoPTS);
|
|
|
+
|
|
|
+ // 重新切换此信道,不然上面跳过了3帧,造成前面的帧数模糊
|
|
|
+ mediaExtractor.unselectTrack(videoTrack);
|
|
|
+ mediaExtractor.selectTrack(videoTrack);
|
|
|
+ return sampleTime;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static int getTrack(MediaExtractor extractor, String willFormat) {
|
|
|
+ //获得信道数
|
|
|
+ int trackCount = extractor.getTrackCount();
|
|
|
+ for (int i = 0; i < trackCount; i++) {
|
|
|
+ MediaFormat trackFormat = extractor.getTrackFormat(i);
|
|
|
+ String format = trackFormat.getString(MediaFormat.KEY_MIME);
|
|
|
+ if (format.startsWith(willFormat)) {
|
|
|
+ return i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+}
|