diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ea78bcc..15edc11 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,8 +4,8 @@
-
-
+
+
停止本地录制");
+ }
+ // 停止音视频编码线程
+ if(mH264Consumer != null){
+ mH264Consumer.setTmpuMuxer(null);
+ }
+ if(mAacConsumer != null){
+ mAacConsumer.setTmpuMuxer(null);
+ }
+ if(mH264Consumer != null){
+ mH264Consumer.exit();
+ try {
+ Thread t2 = mH264Consumer;
+ mH264Consumer = null;
+ if(t2 != null){
+ t2.interrupt();
+ t2.join();
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
}
}
- try {
- mWeakCameraView.get().setVideoEncoder(null);
- } catch (final Exception e) {
- // ignore
+ if(mAacConsumer != null){
+ mAacConsumer.exit();
+ try {
+ Thread t1 = mAacConsumer;
+ mAacConsumer = null;
+ if(t1 != null){
+ t1.interrupt();
+ t1.join();
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
}
- if (muxer != null) {
- muxer.stopRecording();
+ // 停止捕获视频数据
+ if (mUVCCamera != null) {
+ mUVCCamera.stopCapture();
mUVCCamera.setFrameCallback(null, 0);
- // you should not wait here
- callOnStopRecording();
}
+ mWeakCameraView.get().setVideoEncoder(null);
+ // you should not wait here
+ callOnStopRecording();
}
+ // 停止录制视频
+// public void handleStopRecording2() {
+// if (DEBUG) Log.v(TAG_THREAD, "handleStopRecording:mMuxer=" + mMuxer);
+// final MediaMuxerWrapper muxer;
+// synchronized (mSync) {
+// muxer = mMuxer;
+// mMuxer = null;
+// mVideoEncoder = null;
+// if (mUVCCamera != null) {
+// mUVCCamera.stopCapture();
+// }
+// }
+// try {
+// mWeakCameraView.get().setVideoEncoder(null);
+// } catch (final Exception e) {
+// // ignore
+// }
+// if (muxer != null) {
+// muxer.stopRecording();
+// mUVCCamera.setFrameCallback(null, 0);
+// // you should not wait here
+// callOnStopRecording();
+// }
+// }
+
private final IFrameCallback mIFrameCallback = new IFrameCallback() {
@Override
public void onFrame(final ByteBuffer frame) {
- final MediaVideoBufferEncoder videoEncoder;
- synchronized (mSync) {
- videoEncoder = mVideoEncoder;
- }
- if (videoEncoder != null) {
- videoEncoder.frameAvailableSoon();
- videoEncoder.encode(frame);
+// final MediaVideoBufferEncoder videoEncoder;
+// synchronized (mSync) {
+// videoEncoder = mVideoEncoder;
+// }
+// if (videoEncoder != null) {
+// videoEncoder.frameAvailableSoon();
+// videoEncoder.encode(frame);
+// }
+ int len = frame.capacity();
+ byte[] yuv = new byte[len];
+ frame.get(yuv);
+ if(mH264Consumer != null){
+ mH264Consumer.setRawYuv(yuv,640,480);
}
}
};
diff --git a/libusbcamera/src/main/java/com/serenegiant/usb/encoder/MediaEncoder.java b/libusbcamera/src/main/java/com/serenegiant/usb/encoder/MediaEncoder.java
index b651f76..633255e 100644
--- a/libusbcamera/src/main/java/com/serenegiant/usb/encoder/MediaEncoder.java
+++ b/libusbcamera/src/main/java/com/serenegiant/usb/encoder/MediaEncoder.java
@@ -1,26 +1,3 @@
-/*
- * UVCCamera
- * library and sample to access to UVC web camera on non-rooted Android device
- *
- * Copyright (c) 2014-2017 saki t_saki@serenegiant.com
- *
- * 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.
- *
- * All files in the folder are under this Apache License, Version 2.0.
- * Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder
- * may have a different license, see the respective files.
- */
-
package com.serenegiant.usb.encoder;
import android.media.MediaCodec;
@@ -30,11 +7,14 @@ import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
+import com.jiangdg.usbcamera.FileUtils;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
+import java.nio.Buffer;
import java.nio.ByteBuffer;
public abstract class MediaEncoder implements Runnable {
@@ -43,9 +23,12 @@ public abstract class MediaEncoder implements Runnable {
public static final int TYPE_AUDIO = 0; // 音频数据
public static final int TYPE_VIDEO = 1; // 视频数据
- protected static final int TIMEOUT_USEC = 10000; // 10[msec]
+ protected static final int TIMEOUT_USEC = 10000; // 10毫秒
protected static final int MSG_FRAME_AVAILABLE = 1;
protected static final int MSG_STOP_RECORDING = 9;
+ private long lastPush;
+ private long millisPerframe;
+ private boolean isExit;
public interface MediaEncoderListener {
void onPrepared(MediaEncoder encoder);
@@ -169,6 +152,10 @@ public abstract class MediaEncoder implements Runnable {
final boolean isRunning = true;
boolean localRequestStop;
boolean localRequestDrain;
+ boolean localIsNotExit;
+ // 创建h264
+ FileUtils.createfile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/test222.h264");
+
while (isRunning) {
synchronized (mSync) {
localRequestStop = mRequestStop;
@@ -198,11 +185,11 @@ public abstract class MediaEncoder implements Runnable {
}
}
} // end of while
- if (DEBUG) Log.d(TAG, "Encoder thread exiting");
synchronized (mSync) {
mRequestStop = true;
mIsCapturing = false;
}
+ FileUtils.releaseFile();
}
/*
@@ -217,6 +204,7 @@ public abstract class MediaEncoder implements Runnable {
synchronized (mSync) {
mIsCapturing = true;
mRequestStop = false;
+ isExit = false;
mSync.notifyAll();
}
}
@@ -231,6 +219,7 @@ public abstract class MediaEncoder implements Runnable {
return;
}
mRequestStop = true; // for rejecting newer frame
+ isExit = true;
mSync.notifyAll();
// We can not know when the encoding and writing finish.
// so we return immediately after request to avoid delay of caller thread
@@ -288,7 +277,6 @@ public abstract class MediaEncoder implements Runnable {
*/
@SuppressWarnings("deprecation")
protected void encode(final byte[] buffer, final int length, final long presentationTimeUs) {
-// if (DEBUG) Log.v(TAG, "encode:buffer=" + buffer);
if (!mIsCapturing) return;
int ix = 0, sz;
final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
@@ -323,6 +311,46 @@ public abstract class MediaEncoder implements Runnable {
}
}
+ protected void encode(ByteBuffer yuvBuffer,int len){
+ if (!mIsCapturing) return;
+ try {
+ if (lastPush == 0) {
+ lastPush = System.currentTimeMillis();
+ }
+ long time = System.currentTimeMillis() - lastPush;
+ if (time >= 0) {
+ time = millisPerframe - time;
+ if (time > 0)
+ Thread.sleep(time / 2);
+ }
+ final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
+ int bufferIndex = -1;
+ try{
+ bufferIndex = mMediaCodec.dequeueInputBuffer(0);
+ }catch (IllegalStateException e){
+ e.printStackTrace();
+ }
+ if (bufferIndex >= 0) {
+ ByteBuffer mBuffer;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mBuffer = mMediaCodec.getInputBuffer(bufferIndex);
+ } else {
+ mBuffer = inputBuffers[bufferIndex];
+ }
+ byte[] yuvData = new byte[yuvBuffer.capacity()];
+ yuvBuffer.get(yuvData);
+
+ mBuffer.clear();
+ mBuffer.put(yuvData);
+ mBuffer.clear();
+ mMediaCodec.queueInputBuffer(bufferIndex, 0, yuvData.length, System.nanoTime() / 1000, MediaCodec.BUFFER_FLAG_KEY_FRAME);
+ }
+ lastPush = System.currentTimeMillis();
+ } catch (InterruptedException ex) {
+ ex.printStackTrace();
+ }
+ }
+
/**
* Method to set ByteBuffer to the MediaCodec encoder
* @param buffer null means EOS
@@ -367,155 +395,147 @@ public abstract class MediaEncoder implements Runnable {
}
}
- byte[] mPpsSps = new byte[0];
- byte[] h264 = new byte[640 * 480 * 3 / 2];
ByteBuffer mBuffer = ByteBuffer.allocate(10240);
- long timeStamp = System.currentTimeMillis();
/**
* drain encoded data and write them to muxer
*/
@SuppressWarnings("deprecation")
protected void drain() {
- if (mMediaCodec == null) return;
+ if (mMediaCodec == null)
+ return;
ByteBuffer[] encoderOutputBuffers = mMediaCodec.getOutputBuffers();
int encoderStatus, count = 0;
final MediaMuxerWrapper muxer = mWeakMuxer.get();
if (muxer == null) {
-// throw new NullPointerException("muxer is unexpectedly null");
Log.w(TAG, "muxer is unexpectedly null");
return;
}
+ byte[] mPpsSps = new byte[0];
+ byte[] h264 = new byte[640 * 480];
-
-LOOP: while (mIsCapturing) {
- // get encoded data with maximum timeout duration of TIMEOUT_USEC(=10[msec])
+ while (mIsCapturing) {
encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
- // wait 5 counts(=TIMEOUT_USEC x 5 = 50msec) until data/EOS come
+ // 等待 TIMEOUT_USEC x 5 = 50毫秒
+ // 如果还没有数据,终止循环
if (!mIsEOS) {
if (++count > 5)
- break LOOP; // out of while
+ break;
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
- if (DEBUG) Log.v(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
- // this shoud not come when encoding
encoderOutputBuffers = mMediaCodec.getOutputBuffers();
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
- if (DEBUG) Log.v(TAG, "INFO_OUTPUT_FORMAT_CHANGED");
- // this status indicate the output format of codec is changed
- // this should come only once before actual encoded data
- // but this status never come on Android4.3 or less
- // and in that case, you should treat when MediaCodec.BUFFER_FLAG_CODEC_CONFIG come.
- if (mMuxerStarted) { // second time request is error
+ if (mMuxerStarted) {
throw new RuntimeException("format changed twice");
}
- // get output format from codec and pass them to muxer
- // getOutputFormat should be called after INFO_OUTPUT_FORMAT_CHANGED otherwise crash.
- final MediaFormat format = mMediaCodec.getOutputFormat(); // API >= 16
+ final MediaFormat format = mMediaCodec.getOutputFormat();
mTrackIndex = muxer.addTrack(format);
mMuxerStarted = true;
if (!muxer.start()) {
- // we should wait until muxer is ready
synchronized (muxer) {
while (!muxer.isStarted())
try {
muxer.wait(100);
} catch (final InterruptedException e) {
- break LOOP;
+ break;
}
}
}
} else if (encoderStatus < 0) {
- // unexpected status
if (DEBUG) Log.w(TAG, "drain:unexpected result from encoder#dequeueOutputBuffer: " + encoderStatus);
} else {
- final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
+ ByteBuffer encodedData;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ encodedData = mMediaCodec.getOutputBuffer(encoderStatus);
+ } else {
+ encodedData = encoderOutputBuffers[encoderStatus];
+ }
+ encodedData.position(mBufferInfo.offset);
+ encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
+
+// final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null) {
- // this never should come...may be a MediaCodec internal error
throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
}
+ // BUFFER_FLAG_CODEC_CONFIG标志
+ // BufferInfo清零
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
- // You shoud set output format to muxer here when you target Android4.3 or less
- // but MediaCodec#getOutputFormat can not call here(because INFO_OUTPUT_FORMAT_CHANGED don't come yet)
- // therefor we should expand and prepare output format from buffer data.
- // This sample is for API>=18(>=Android 4.3), just ignore this flag here
if (DEBUG) Log.d(TAG, "drain:BUFFER_FLAG_CODEC_CONFIG");
mBufferInfo.size = 0;
}
-
+ // BUFFER_FLAG_END_OF_STREAM标志
+ // 流结束,终止循环
+ if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ mMuxerStarted = mIsCapturing = false;
+ break;
+ }
+ // 有效编码数据流
if (mBufferInfo.size != 0) {
- // encoded data is ready, clear waiting counter
count = 0;
- if (!mMuxerStarted) {
- // muxer is not ready...this will prrograming failure.
- throw new RuntimeException("drain:muxer hasn't started");
- }
- // write encoded data to muxer(need to adjust presentationTimeUs.
- mBufferInfo.presentationTimeUs = getPTSUs();
- muxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
+ if (!mMuxerStarted) {
+ throw new RuntimeException("drain:muxer hasn't started");
+ }
+ // 写入音频流或视频流到混合器
+ mBufferInfo.presentationTimeUs = getPTSUs();
+ muxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
prevOutputPTSUs = mBufferInfo.presentationTimeUs;
// 推流,获取h.264数据流
- // 根据mBufferInfo.size来判断音视频
- // > 1000,视频;< 1000,音频
- synchronized (this){
- if(mBufferInfo.size > 1000) {
- int type = encodedData.get(4) & 0x07;
- if(type == 7 || type == 8) {
- byte[] outData = new byte[mBufferInfo.size];
- encodedData.get(outData);
- mPpsSps = outData;
- }else if(type == 5) {
- System.arraycopy(mPpsSps,0,h264,0,mPpsSps.length);
- if(mBufferInfo.size > h264.length) {
- continue;
- }
- encodedData.get(h264,mPpsSps.length,mBufferInfo.size);
- if(mListener != null) {
- mListener.onEncodeResult(h264, 0,mPpsSps.length + mBufferInfo.size,
- mBufferInfo.presentationTimeUs / 1000,TYPE_VIDEO);
- }
+ // mTrackIndex=0 视频;mTrackIndex=1 音频
+ if(mTrackIndex == 0) {
+ encodedData.position(mBufferInfo.offset);
+ encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
+ boolean sync = false;
+ if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// sps
+ sync = (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
+ if (!sync) {
+ byte[] temp = new byte[mBufferInfo.size];
+ encodedData.get(temp);
+ mPpsSps = temp;
+ mMediaCodec.releaseOutputBuffer(encoderStatus, false);
+ continue;
} else {
- if(mBufferInfo.size > h264.length){
- continue ;
- }
- encodedData.get(h264,0,mBufferInfo.size);
- if(System.currentTimeMillis() - timeStamp >= 3000) {
- timeStamp = System.currentTimeMillis();
- if(Build.VERSION.SDK_INT >= 23) {
- Bundle params = new Bundle();
- params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
- mMediaCodec.setParameters(params);
- }
- }
- if(mListener != null) {
- mListener.onEncodeResult(h264, 0,mBufferInfo.size,
- mBufferInfo.presentationTimeUs / 1000,TYPE_VIDEO);
- }
+ mPpsSps = new byte[0];
}
-
+ }
+ sync |= (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
+ int len = mPpsSps.length + mBufferInfo.size;
+ if (len > h264.length) {
+ h264 = new byte[len];
+ }
+ if (sync) {
+ System.arraycopy(mPpsSps, 0, h264, 0, mPpsSps.length);
+ encodedData.get(h264, mPpsSps.length, mBufferInfo.size);
+ if(mListener != null) {
+ mListener.onEncodeResult(h264, 0,mPpsSps.length + mBufferInfo.size,
+ mBufferInfo.presentationTimeUs / 1000,TYPE_VIDEO);
+ }
+ // 保存数据流到文件
+ FileUtils.putFileStream(h264, 0,mPpsSps.length + mBufferInfo.size);
} else {
- mBuffer.clear();
- encodedData.get(mBuffer.array(), 7, mBufferInfo.size);
- encodedData.clear();
- mBuffer.position(7 + mBufferInfo.size);
- addADTStoPacket(mBuffer.array(), mBufferInfo.size + 7);
- mBuffer.flip();
- if(mListener != null){
- mListener.onEncodeResult(mBuffer.array(),0, mBufferInfo.size + 7, mBufferInfo.presentationTimeUs / 1000,TYPE_AUDIO);
+ encodedData.get(h264, 0, mBufferInfo.size);
+ if(mListener != null) {
+ mListener.onEncodeResult(h264, 0,mBufferInfo.size,
+ mBufferInfo.presentationTimeUs / 1000,TYPE_VIDEO);
}
+ FileUtils.putFileStream(h264, 0,mBufferInfo.size);
+ }
+ } else if(mTrackIndex == 1){
+ mBuffer.clear();
+ encodedData.get(mBuffer.array(), 7, mBufferInfo.size);
+ encodedData.clear();
+ mBuffer.position(7 + mBufferInfo.size);
+ addADTStoPacket(mBuffer.array(), mBufferInfo.size + 7);
+ mBuffer.flip();
+ if(mListener != null){
+ mListener.onEncodeResult(mBuffer.array(),0, mBufferInfo.size + 7,
+ mBufferInfo.presentationTimeUs / 1000,TYPE_AUDIO);
}
}
-
}
- // return buffer to encoder
+ // 释放输出缓存,将其还给编码器
mMediaCodec.releaseOutputBuffer(encoderStatus, false);
- if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
- // when EOS come.
- mMuxerStarted = mIsCapturing = false;
- break; // out of while
- }
}
}
}
@@ -541,18 +561,11 @@ LOOP: while (mIsCapturing) {
return mSamplingRateIndex;
}
- /**
- * previous presentationTimeUs for writing
- */
+
private long prevOutputPTSUs = 0;
- /**
- * get next encoding presentationTimeUs
- * @return
- */
+
protected long getPTSUs() {
long result = System.nanoTime() / 1000L;
- // presentationTimeUs should be monotonic
- // otherwise muxer fail to write
if (result < prevOutputPTSUs)
result = (prevOutputPTSUs - result) + result;
return result;
diff --git a/libusbcamera/src/main/java/com/serenegiant/usb/encoder/MediaVideoBufferEncoder.java b/libusbcamera/src/main/java/com/serenegiant/usb/encoder/MediaVideoBufferEncoder.java
index 5cef494..40d26bf 100644
--- a/libusbcamera/src/main/java/com/serenegiant/usb/encoder/MediaVideoBufferEncoder.java
+++ b/libusbcamera/src/main/java/com/serenegiant/usb/encoder/MediaVideoBufferEncoder.java
@@ -27,24 +27,18 @@ import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
+import android.os.Build;
+import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
import java.nio.ByteBuffer;
-/**
- * This class receives video images as ByteBuffer(strongly recommend direct ByteBuffer) as NV21(YUV420SP)
- * and encode them to h.264.
- * If you use this directly with IFrameCallback, you should know UVCCamera and it backend native libraries
- * never execute color space conversion. This means that color tone of resulted movie will be different
- * from that you expected/can see on screen.
- */
public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncoder {
private static final boolean DEBUG = true; // TODO set false on release
private static final String TAG = "MediaVideoBufferEncoder";
private static final String MIME_TYPE = "video/avc";
- // parameters for recording
private static final int FRAME_RATE = 15;
private static final float BPP = 0.50f;
@@ -59,11 +53,11 @@ public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncod
}
public void encode(final ByteBuffer buffer) {
-// if (DEBUG) Log.v(TAG, "encode:buffer=" + buffer);
synchronized (mSync) {
if (!mIsCapturing || mRequestStop) return;
}
- encode(buffer, buffer.capacity(), getPTSUs());
+// encode(buffer, buffer.capacity(), getPTSUs());
+ encode(buffer, buffer.capacity());
}
@Override
@@ -89,6 +83,12 @@ public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncod
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
+
+ Bundle params = new Bundle();
+ params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ mMediaCodec.setParameters(params);
+ }
if (DEBUG) Log.i(TAG, "prepare finishing");
if (mListener != null) {
try {
@@ -105,11 +105,7 @@ public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncod
return bitrate;
}
- /**
- * select the first codec that match a specific MIME type
- * @param mimeType
- * @return null if no codec matched
- */
+ // 选择第一个与制定MIME类型匹配的编码器
@SuppressWarnings("deprecation")
protected final MediaCodecInfo selectVideoCodec(final String mimeType) {
if (DEBUG) Log.v(TAG, "selectVideoCodec:");
@@ -138,10 +134,7 @@ public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncod
return null;
}
- /**
- * select color format available on specific codec and we can use.
- * @return 0 if no colorFormat is matched
- */
+ // 选择编码器支持的格式
protected static final int selectColorFormat(final MediaCodecInfo codecInfo, final String mimeType) {
if (DEBUG) Log.i(TAG, "selectColorFormat: ");
int result = 0;
@@ -166,9 +159,7 @@ public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncod
return result;
}
- /**
- * color formats that we can use in this class
- */
+ // YUV颜色格式
protected static int[] recognizedFormats;
static {
recognizedFormats = new int[] {
diff --git a/libusbcamera/src/main/java/com/serenegiant/usb/encoder/biz/AACEncodeConsumer.java b/libusbcamera/src/main/java/com/serenegiant/usb/encoder/biz/AACEncodeConsumer.java
new file mode 100644
index 0000000..f6cac2c
--- /dev/null
+++ b/libusbcamera/src/main/java/com/serenegiant/usb/encoder/biz/AACEncodeConsumer.java
@@ -0,0 +1,365 @@
+package com.serenegiant.usb.encoder.biz;
+
+import android.annotation.TargetApi;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+import android.media.MediaRecorder;
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**将PCM编码为AAC
+ *
+ * Created by jianddongguo on 2017/7/21.
+ */
+
+public class AACEncodeConsumer extends Thread{
+ private static final boolean DEBUG = false;
+ private static final String TAG = "TMPU";
+ private static final String MIME_TYPE = "audio/mp4a-latm";
+ private static final long TIMES_OUT = 1000;
+ private static final int SAMPLE_RATE = 8000; // 采样率
+ private static final int BIT_RATE = 16000; // 比特率
+ private static final int BUFFER_SIZE = 1920; // 最小缓存
+ private int outChannel = 1;
+ private int bitRateForLame = 32;
+ private int qaulityDegree = 7;
+ private int bufferSizeInBytes;
+
+ private AudioRecord mAudioRecord; // 音频采集
+ private MediaCodec mAudioEncoder; // 音频编码
+ private OnAACEncodeResultListener listener;
+ private int mSamplingRateIndex = 0;//ADTS
+ private boolean isEncoderStart = false;
+ private boolean isRecMp3 = false;
+ private boolean isExit = false;
+ private long prevPresentationTimes = 0;
+ private WeakReference mMuxerRef;
+ private MediaFormat newFormat;
+
+ /**
+ * There are 13 supported frequencies by ADTS.
+ **/
+ public static final int[] AUDIO_SAMPLING_RATES = { 96000, // 0
+ 88200, // 1
+ 64000, // 2
+ 48000, // 3
+ 44100, // 4
+ 32000, // 5
+ 24000, // 6
+ 22050, // 7
+ 16000, // 8
+ 12000, // 9
+ 11025, // 10
+ 8000, // 11
+ 7350, // 12
+ -1, // 13
+ -1, // 14
+ -1, // 15
+ };
+
+ private FileOutputStream fops;
+
+ // 编码流结果回调接口
+ public interface OnAACEncodeResultListener{
+ void onEncodeResult(byte[] data, int offset,
+ int length, long timestamp);
+ }
+
+ public AACEncodeConsumer(){
+ for (int i=0;i < AUDIO_SAMPLING_RATES.length; i++) {
+ if (AUDIO_SAMPLING_RATES[i] == SAMPLE_RATE) {
+ mSamplingRateIndex = i;
+ break;
+ }
+ }
+ }
+
+ public void setOnAACEncodeResultListener(OnAACEncodeResultListener listener){
+ this.listener = listener;
+ }
+
+ public void exit(){
+ isExit = true;
+ }
+
+ public synchronized void setTmpuMuxer(Mp4MediaMuxer mMuxer){
+ this.mMuxerRef = new WeakReference<>(mMuxer);
+ Mp4MediaMuxer muxer = mMuxerRef.get();
+ if (muxer != null && newFormat != null) {
+ muxer.addTrack(newFormat, false);
+ }
+ }
+
+ @Override
+ public void run() {
+ // 开启音频采集、编码
+ if(! isEncoderStart){
+ initAudioRecord();
+ initMediaCodec();
+ }
+ // 初始化音频文件参数
+ byte[] mp3Buffer = new byte[1024];
+
+ // 这里有问题,当本地录制结束后,没有写入
+ while(! isExit){
+ byte[] audioBuffer = new byte[2048];
+ // 采集音频
+ int readBytes = mAudioRecord.read(audioBuffer,0,BUFFER_SIZE);
+
+ if(DEBUG)
+ Log.i(TAG,"采集音频readBytes = "+readBytes);
+ // 编码音频
+ if(readBytes > 0){
+ encodeBytes(audioBuffer,readBytes);
+ }
+ }
+ // 停止音频采集、编码
+ stopMediaCodec();
+ stopAudioRecord();
+ }
+
+
+ @TargetApi(21)
+ private void encodeBytes(byte[] audioBuf, int readBytes) {
+ ByteBuffer[] inputBuffers = mAudioEncoder.getInputBuffers();
+ ByteBuffer[] outputBuffers = mAudioEncoder.getOutputBuffers();
+ //返回编码器的一个输入缓存区句柄,-1表示当前没有可用的输入缓存区
+ int inputBufferIndex = mAudioEncoder.dequeueInputBuffer(TIMES_OUT);
+ if(inputBufferIndex >= 0){
+ // 绑定一个被空的、可写的输入缓存区inputBuffer到客户端
+ ByteBuffer inputBuffer = null;
+ if(!isLollipop()){
+ inputBuffer = inputBuffers[inputBufferIndex];
+ }else{
+ inputBuffer = mAudioEncoder.getInputBuffer(inputBufferIndex);
+ }
+ // 向输入缓存区写入有效原始数据,并提交到编码器中进行编码处理
+ if(audioBuf==null || readBytes<=0){
+ mAudioEncoder.queueInputBuffer(inputBufferIndex,0,0,getPTSUs(),MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ }else{
+ inputBuffer.clear();
+ inputBuffer.put(audioBuf);
+ mAudioEncoder.queueInputBuffer(inputBufferIndex,0,readBytes,getPTSUs(),0);
+ }
+ }
+
+ // 返回一个输出缓存区句柄,当为-1时表示当前没有可用的输出缓存区
+ // mBufferInfo参数包含被编码好的数据,timesOut参数为超时等待的时间
+ MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
+ int outputBufferIndex = -1;
+ do{
+ outputBufferIndex = mAudioEncoder.dequeueOutputBuffer(mBufferInfo,TIMES_OUT);
+ if(outputBufferIndex == MediaCodec. INFO_TRY_AGAIN_LATER){
+ if(DEBUG)
+ Log.i(TAG,"获得编码器输出缓存区超时");
+ }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){
+ // 如果API小于21,APP需要重新绑定编码器的输入缓存区;
+ // 如果API大于21,则无需处理INFO_OUTPUT_BUFFERS_CHANGED
+ if(!isLollipop()){
+ outputBuffers = mAudioEncoder.getOutputBuffers();
+ }
+ }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
+ // 编码器输出缓存区格式改变,通常在存储数据之前且只会改变一次
+ // 这里设置混合器视频轨道,如果音频已经添加则启动混合器(保证音视频同步)
+ if(DEBUG)
+ Log.i(TAG,"编码器输出缓存区格式改变,添加视频轨道到混合器");
+ synchronized (AACEncodeConsumer.this) {
+ newFormat = mAudioEncoder.getOutputFormat();
+ if(mMuxerRef != null){
+ Mp4MediaMuxer muxer = mMuxerRef.get();
+ if (muxer != null) {
+ muxer.addTrack(newFormat, false);
+ }
+ }
+ }
+ }else{
+ // 当flag属性置为BUFFER_FLAG_CODEC_CONFIG后,说明输出缓存区的数据已经被消费了
+ if((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0){
+ if(DEBUG)
+ Log.i(TAG,"编码数据被消费,BufferInfo的size属性置0");
+ mBufferInfo.size = 0;
+ }
+ // 数据流结束标志,结束本次循环
+ if((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
+ if(DEBUG)
+ Log.i(TAG,"数据流结束,退出循环");
+ break;
+ }
+ // 获取一个只读的输出缓存区inputBuffer ,它包含被编码好的数据
+ ByteBuffer mBuffer = ByteBuffer.allocate(10240);
+ ByteBuffer outputBuffer = null;
+ if(!isLollipop()){
+ outputBuffer = outputBuffers[outputBufferIndex];
+ }else{
+ outputBuffer = mAudioEncoder.getOutputBuffer(outputBufferIndex);
+ }
+ if(mBufferInfo.size != 0){
+ // 获取输出缓存区失败,抛出异常
+ if(outputBuffer == null){
+ throw new RuntimeException("encodecOutputBuffer"+outputBufferIndex+"was null");
+ }
+ // 添加视频流到混合器
+ if(mMuxerRef != null){
+ Mp4MediaMuxer muxer = mMuxerRef.get();
+ if (muxer != null) {
+ muxer.pumpStream(outputBuffer, mBufferInfo, false);
+ }
+ }
+ // AAC流添加ADTS头,缓存到mBuffer
+ mBuffer.clear();
+ outputBuffer.get(mBuffer.array(), 7, mBufferInfo.size);
+ outputBuffer.clear();
+ mBuffer.position(7 + mBufferInfo.size);
+ addADTStoPacket(mBuffer.array(), mBufferInfo.size + 7);
+ mBuffer.flip();
+ // 将AAC回调给MainModelImpl进行push
+ if(listener != null){
+ Log.i(TAG,"----->得到aac数据流<-----");
+ listener.onEncodeResult(mBuffer.array(),0, mBufferInfo.size + 7, mBufferInfo.presentationTimeUs / 1000);
+ }
+ }
+ // 处理结束,释放输出缓存区资源
+ mAudioEncoder.releaseOutputBuffer(outputBufferIndex,false);
+ }
+ }while (outputBufferIndex >= 0);
+ }
+
+ private void initAudioRecord(){
+ if(DEBUG)
+ Log.d(TAG,"AACEncodeConsumer-->开始采集音频");
+ // 设置进程优先级
+ Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO);
+ int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO,
+ AudioFormat.ENCODING_PCM_16BIT);
+ mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE,
+ AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
+ mAudioRecord.startRecording();
+ }
+
+ private void initMediaCodec(){
+ if(DEBUG)
+ Log.d(TAG,"AACEncodeConsumer-->开始编码音频");
+ MediaCodecInfo mCodecInfo = selectSupportCodec(MIME_TYPE);
+ if(mCodecInfo == null){
+ Log.e(TAG,"编码器不支持"+MIME_TYPE+"类型");
+ return;
+ }
+ try{
+ mAudioEncoder = MediaCodec.createByCodecName(mCodecInfo.getName());
+ }catch(IOException e){
+ Log.e(TAG,"创建编码器失败"+e.getMessage());
+ e.printStackTrace();
+ }
+ MediaFormat format = new MediaFormat();
+ format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
+ format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
+ format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
+ format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
+ format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
+ format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, BUFFER_SIZE);
+ mAudioEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+ mAudioEncoder.start();
+ isEncoderStart = true;
+ }
+
+ private void stopAudioRecord() {
+ if(DEBUG)
+ Log.d(TAG,"AACEncodeConsumer-->停止采集音频");
+ if(mAudioRecord != null){
+ mAudioRecord.stop();
+ mAudioRecord.release();
+ mAudioRecord = null;
+ }
+ }
+
+ private void stopMediaCodec() {
+ if(DEBUG)
+ Log.d(TAG,"AACEncodeConsumer-->停止编码音频");
+ if(mAudioEncoder != null){
+ mAudioEncoder.stop();
+ mAudioEncoder.release();
+ mAudioEncoder = null;
+ }
+ isEncoderStart = false;
+ }
+
+ // API>=21
+ private boolean isLollipop(){
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+ }
+
+ // API<=19
+ private boolean isKITKAT(){
+ return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT;
+ }
+
+ private long getPTSUs(){
+ long result = System.nanoTime()/1000;
+ if(result < prevPresentationTimes){
+ result = (prevPresentationTimes - result ) + result;
+ }
+ return result;
+ }
+
+ /**
+ * 遍历所有编解码器,返回第一个与指定MIME类型匹配的编码器
+ * 判断是否有支持指定mime类型的编码器
+ * */
+ private MediaCodecInfo selectSupportCodec(String mimeType){
+ int numCodecs = MediaCodecList.getCodecCount();
+ for (int i = 0; i < numCodecs; i++) {
+ MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
+ // 判断是否为编码器,否则直接进入下一次循环
+ if (!codecInfo.isEncoder()) {
+ continue;
+ }
+ // 如果是编码器,判断是否支持Mime类型
+ String[] types = codecInfo.getSupportedTypes();
+ for (int j = 0; j < types.length; j++) {
+ if (types[j].equalsIgnoreCase(mimeType)) {
+ return codecInfo;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void addADTStoPacket(byte[] packet, int packetLen) {
+ packet[0] = (byte) 0xFF;
+ packet[1] = (byte) 0xF1;
+ packet[2] = (byte) (((2 - 1) << 6) + (mSamplingRateIndex << 2) + (1 >> 2));
+ packet[3] = (byte) (((1 & 3) << 6) + (packetLen >> 11));
+ packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
+ packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
+ packet[6] = (byte) 0xFC;
+ }
+
+
+ private short[] transferByte2Short(byte[] data,int readBytes){
+ // byte[] 转 short[],数组长度缩减一半
+ int shortLen = readBytes / 2;
+ // 将byte[]数组装如ByteBuffer缓冲区
+ ByteBuffer byteBuffer = ByteBuffer.wrap(data, 0, readBytes);
+ // 将ByteBuffer转成小端并获取shortBuffer
+ ShortBuffer shortBuffer = byteBuffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
+ short[] shortData = new short[shortLen];
+ shortBuffer.get(shortData, 0, shortLen);
+ return shortData;
+ }
+}
diff --git a/libusbcamera/src/main/java/com/serenegiant/usb/encoder/biz/H264EncodeConsumer.java b/libusbcamera/src/main/java/com/serenegiant/usb/encoder/biz/H264EncodeConsumer.java
new file mode 100644
index 0000000..c649642
--- /dev/null
+++ b/libusbcamera/src/main/java/com/serenegiant/usb/encoder/biz/H264EncodeConsumer.java
@@ -0,0 +1,371 @@
+package com.serenegiant.usb.encoder.biz;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.nio.ByteBuffer;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+
+import com.jiangdg.usbcamera.FileUtils;
+
+import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar;
+import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar;
+import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
+
+/** 对YUV视频流进行编码
+ * Created by jiangdongguo on 2017/5/6.
+ */
+
+@SuppressWarnings("deprecation")
+public class H264EncodeConsumer extends Thread {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "H264EncodeConsumer";
+ private static final String MIME_TYPE = "video/avc";
+ // 间隔1s插入一帧关键帧
+ private static final int FRAME_INTERVAL = 1;
+ // 绑定编码器缓存区超时时间为10s
+ private static final int TIMES_OUT = 10000;
+ // 硬编码器
+ private MediaCodec mMediaCodec;
+ private int mColorFormat;
+ private boolean isExit = false;
+ private boolean isEncoderStart = false;
+
+ private MediaFormat mFormat;
+ private static String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test2.h264";
+ private BufferedOutputStream outputStream;
+ final int millisPerframe = 1000 / 20;
+ long lastPush = 0;
+ private OnH264EncodeResultListener listener;
+ private int mWidth ;
+ private int mHeight ;
+ private MediaFormat newFormat;
+ private WeakReference mMuxerRef;
+ private boolean isAddKeyFrame = false;
+
+ public interface OnH264EncodeResultListener{
+ void onEncodeResult(byte[] data, int offset,
+ int length, long timestamp);
+ }
+
+ public void setOnH264EncodeResultListener(OnH264EncodeResultListener listener) {
+ this.listener = listener;
+ }
+
+ public synchronized void setTmpuMuxer(Mp4MediaMuxer mMuxer){
+ this.mMuxerRef = new WeakReference<>(mMuxer);
+ Mp4MediaMuxer muxer = mMuxerRef.get();
+ if (muxer != null && newFormat != null) {
+ muxer.addTrack(newFormat, true);
+ }
+ }
+
+ private ByteBuffer[] inputBuffers;
+ private ByteBuffer[] outputBuffers;
+
+ public void setRawYuv(byte[] yuvData,int width,int height){
+ if (! isEncoderStart)
+ return;
+ // 根据编码器支持转换颜色空间格式
+ // 即 nv21 ---> YUV420sp(21)
+ // nv21 ---> YUV420p (19)
+ mWidth = width;
+ mHeight = height;
+ try {
+ if (lastPush == 0) {
+ lastPush = System.currentTimeMillis();
+ }
+ long time = System.currentTimeMillis() - lastPush;
+ if (time >= 0) {
+ time = millisPerframe - time;
+ if (time > 0)
+ Thread.sleep(time / 2);
+ }
+ // 将数据写入编码器
+ feedMediaCodecData(yuvData);
+
+ if (time > 0)
+ Thread.sleep(time / 2);
+ lastPush = System.currentTimeMillis();
+ } catch (InterruptedException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ private void feedMediaCodecData(byte[] data){
+ if (! isEncoderStart)
+ return;
+ int bufferIndex = -1;
+ try{
+ bufferIndex = mMediaCodec.dequeueInputBuffer(0);
+ }catch (IllegalStateException e){
+ e.printStackTrace();
+ }
+ if (bufferIndex >= 0) {
+ ByteBuffer buffer;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ buffer = mMediaCodec.getInputBuffer(bufferIndex);
+ } else {
+ buffer = inputBuffers[bufferIndex];
+ }
+ buffer.clear();
+ buffer.put(data);
+ buffer.clear();
+ mMediaCodec.queueInputBuffer(bufferIndex, 0, data.length, System.nanoTime() / 1000, MediaCodec.BUFFER_FLAG_KEY_FRAME);
+ }
+ }
+
+ public void exit(){
+ isExit = true;
+ }
+
+ @Override
+ public void run() {
+ if(!isEncoderStart){
+ startMediaCodec();
+ }
+ // 休眠200ms,等待音频线程开启
+ // 否则视频第一秒会卡住
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException e1) {
+ e1.printStackTrace();
+ }
+
+ // 如果编码器没有启动或者没有图像数据,线程阻塞等待
+ while(!isExit){
+ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+ int outputBufferIndex = 0;
+ byte[] mPpsSps = new byte[0];
+ byte[] h264 = new byte[mWidth * mHeight];
+ do {
+ outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
+ if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ // no output available yet
+ } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+ // not expected for an encoder
+ outputBuffers = mMediaCodec.getOutputBuffers();
+ } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ synchronized (H264EncodeConsumer.this) {
+ newFormat = mMediaCodec.getOutputFormat();
+ if(mMuxerRef != null){
+ Mp4MediaMuxer muxer = mMuxerRef.get();
+ if (muxer != null) {
+ muxer.addTrack(newFormat, true);
+ }
+ }
+ }
+ } else if (outputBufferIndex < 0) {
+ // let's ignore it
+ } else {
+ ByteBuffer outputBuffer;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
+ } else {
+ outputBuffer = outputBuffers[outputBufferIndex];
+ }
+ outputBuffer.position(bufferInfo.offset);
+ outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
+
+ boolean sync = false;
+ if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// sps
+ sync = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
+ if (!sync) {
+ byte[] temp = new byte[bufferInfo.size];
+ outputBuffer.get(temp);
+ mPpsSps = temp;
+ mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
+ continue;
+ } else {
+ mPpsSps = new byte[0];
+ }
+ }
+ sync |= (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
+ int len = mPpsSps.length + bufferInfo.size;
+ if (len > h264.length) {
+ h264 = new byte[len];
+ }
+ if (sync) {
+ System.arraycopy(mPpsSps, 0, h264, 0, mPpsSps.length);
+ outputBuffer.get(h264, mPpsSps.length, bufferInfo.size);
+ if(listener != null){
+ listener.onEncodeResult(h264, 0,mPpsSps.length + bufferInfo.size, bufferInfo.presentationTimeUs / 1000);
+ }
+
+ // 添加视频流到混合器
+ if(mMuxerRef != null){
+ Mp4MediaMuxer muxer = mMuxerRef.get();
+ if (muxer != null) {
+ muxer.pumpStream(outputBuffer, bufferInfo, true);
+ }
+ isAddKeyFrame = true;
+ }
+ if(DEBUG)
+ Log.i(TAG,"关键帧 h264.length = "+h264.length+";mPpsSps.length="+mPpsSps.length
+ + " bufferInfo.size = " + bufferInfo.size);
+ } else {
+ outputBuffer.get(h264, 0, bufferInfo.size);
+ if(listener != null){
+ listener.onEncodeResult(h264, 0,bufferInfo.size, bufferInfo.presentationTimeUs / 1000);
+ }
+ // 添加视频流到混合器
+ if(isAddKeyFrame && mMuxerRef != null){
+ Mp4MediaMuxer muxer = mMuxerRef.get();
+ if (muxer != null) {
+ muxer.pumpStream(outputBuffer, bufferInfo, true);
+ }
+ }
+ if(DEBUG)
+ Log.i(TAG,"普通帧 h264.length = "+h264.length+ " bufferInfo.size = " + bufferInfo.size);
+ }
+ mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
+ }
+ } while (!isExit && isEncoderStart);
+ }
+ stopMediaCodec();
+ }
+
+ private void startMediaCodec() {
+ final MediaCodecInfo videoCodecInfo = selectVideoCodec(MIME_TYPE);
+ if (videoCodecInfo == null) {
+ Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE);
+ return;
+ }
+
+ final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, 640, 480);
+ format.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat);
+ format.setInteger(MediaFormat.KEY_BIT_RATE, calcBitRate());
+ format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
+ format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
+
+ try {
+ mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+ mMediaCodec.start();
+
+
+
+ isEncoderStart = true;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + 1) {
+ inputBuffers = outputBuffers = null;
+ } else {
+ inputBuffers = mMediaCodec.getInputBuffers();
+ outputBuffers = mMediaCodec.getOutputBuffers();
+ }
+
+ Bundle params = new Bundle();
+ params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ mMediaCodec.setParameters(params);
+ }
+ }
+
+ private void stopMediaCodec(){
+ isEncoderStart = false;
+ if(mMediaCodec != null){
+ mMediaCodec.stop();
+ mMediaCodec.release();
+ Log.d(TAG,"关闭视频编码器");
+ }
+ }
+
+ private static final int FRAME_RATE = 15;
+ private static final float BPP = 0.50f;
+ private int calcBitRate() {
+ final int bitrate = (int)(BPP * FRAME_RATE * 640 * 480);
+ Log.i(TAG, String.format("bitrate=%5.2f[Mbps]", bitrate / 1024f / 1024f));
+ return bitrate;
+ }
+
+ /**
+ * select the first codec that match a specific MIME type
+ * @param mimeType
+ * @return null if no codec matched
+ */
+ @SuppressWarnings("deprecation")
+ protected final MediaCodecInfo selectVideoCodec(final String mimeType) {
+
+ // get the list of available codecs
+ final int numCodecs = MediaCodecList.getCodecCount();
+ for (int i = 0; i < numCodecs; i++) {
+ final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
+
+ if (!codecInfo.isEncoder()) { // skipp decoder
+ continue;
+ }
+ // select first codec that match a specific MIME type and color format
+ final String[] types = codecInfo.getSupportedTypes();
+ for (int j = 0; j < types.length; j++) {
+ if (types[j].equalsIgnoreCase(mimeType)) {
+ final int format = selectColorFormat(codecInfo, mimeType);
+ if (format > 0) {
+ mColorFormat = format;
+ return codecInfo;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * select color format available on specific codec and we can use.
+ * @return 0 if no colorFormat is matched
+ */
+ protected static final int selectColorFormat(final MediaCodecInfo codecInfo, final String mimeType) {
+ int result = 0;
+ final MediaCodecInfo.CodecCapabilities caps;
+ try {
+ Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
+ caps = codecInfo.getCapabilitiesForType(mimeType);
+ } finally {
+ Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
+ }
+ int colorFormat;
+ for (int i = 0; i < caps.colorFormats.length; i++) {
+ colorFormat = caps.colorFormats[i];
+ if (isRecognizedViewoFormat(colorFormat)) {
+ if (result == 0)
+ result = colorFormat;
+ break;
+ }
+ }
+ if (result == 0)
+ Log.e(TAG, "couldn't find a good color format for " + codecInfo.getName() + " / " + mimeType);
+ return result;
+ }
+
+ /**
+ * color formats that we can use in this class
+ */
+ protected static int[] recognizedFormats;
+ static {
+ recognizedFormats = new int[] {
+// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar,
+ MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
+ MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
+// MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface,
+ };
+ }
+
+ private static final boolean isRecognizedViewoFormat(final int colorFormat) {
+ final int n = recognizedFormats != null ? recognizedFormats.length : 0;
+ for (int i = 0; i < n; i++) {
+ if (recognizedFormats[i] == colorFormat) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/libusbcamera/src/main/java/com/serenegiant/usb/encoder/biz/Mp4MediaMuxer.java b/libusbcamera/src/main/java/com/serenegiant/usb/encoder/biz/Mp4MediaMuxer.java
new file mode 100644
index 0000000..e964530
--- /dev/null
+++ b/libusbcamera/src/main/java/com/serenegiant/usb/encoder/biz/Mp4MediaMuxer.java
@@ -0,0 +1,148 @@
+package com.serenegiant.usb.encoder.biz;
+
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.os.Build;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**Mp4封装混合器
+ *
+ * Created by jianddongguo on 2017/7/28.
+ */
+
+public class Mp4MediaMuxer {
+ private static final boolean VERBOSE = false;
+ private static final String TAG = Mp4MediaMuxer.class.getSimpleName();
+ private final String mFilePath;
+ private MediaMuxer mMuxer;
+ private final long durationMillis;
+ private int index = 0;
+ private int mVideoTrackIndex = -1;
+ private int mAudioTrackIndex = -1;
+ private long mBeginMillis;
+ private MediaFormat mVideoFormat;
+ private MediaFormat mAudioFormat;
+
+ // 文件路径;文件时长
+ public Mp4MediaMuxer(String path, long durationMillis) {
+ mFilePath = path;
+ this.durationMillis = durationMillis;
+ Object mux = null;
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ mux = new MediaMuxer(path + "-" + index++ + ".mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ mMuxer = (MediaMuxer) mux;
+ }
+ }
+
+ public synchronized void addTrack(MediaFormat format, boolean isVideo) {
+ // now that we have the Magic Goodies, start the muxer
+ if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1)
+ throw new RuntimeException("already add all tracks");
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ int track = mMuxer.addTrack(format);
+ if (VERBOSE)
+ Log.i(TAG, String.format("addTrack %s result %d", isVideo ? "video" : "audio", track));
+ if (isVideo) {
+ mVideoFormat = format;
+ mVideoTrackIndex = track;
+ if (mAudioTrackIndex != -1) {
+ if (VERBOSE)
+ Log.i(TAG, "both audio and video added,and muxer is started");
+ mMuxer.start();
+ mBeginMillis = System.currentTimeMillis();
+ }
+ } else {
+ mAudioFormat = format;
+ mAudioTrackIndex = track;
+ if (mVideoTrackIndex != -1) {
+ mMuxer.start();
+ mBeginMillis = System.currentTimeMillis();
+ }
+ }
+ }
+ }
+
+ public synchronized void pumpStream(ByteBuffer outputBuffer, MediaCodec.BufferInfo bufferInfo, boolean isVideo) {
+ if (mAudioTrackIndex == -1 || mVideoTrackIndex == -1) {
+// Log.i(TAG, String.format("pumpStream [%s] but muxer is not start.ignore..", isVideo ? "video" : "audio"));
+ return;
+ }
+ if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
+ // The codec config data was pulled out and fed to the muxer when we got
+ // the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
+ } else if (bufferInfo.size != 0) {
+ if (isVideo && mVideoTrackIndex == -1) {
+ throw new RuntimeException("muxer hasn't started");
+ }
+
+ // adjust the ByteBuffer values to match BufferInfo (not needed?)
+ outputBuffer.position(bufferInfo.offset);
+ outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ mMuxer.writeSampleData(isVideo ? mVideoTrackIndex : mAudioTrackIndex, outputBuffer, bufferInfo);
+ }
+// if (VERBOSE)
+// Log.d(TAG, String.format("sent %s [" + bufferInfo.size + "] with timestamp:[%d] to muxer", isVideo ? "video" : "audio", bufferInfo.presentationTimeUs / 1000));
+ }
+
+ if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+// if (VERBOSE)
+// Log.i(TAG, "BUFFER_FLAG_END_OF_STREAM received");
+ }
+
+ if (System.currentTimeMillis() - mBeginMillis >= durationMillis) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+// if (VERBOSE)
+// Log.i(TAG, String.format("record file reach expiration.create new file:" + index));
+ mMuxer.stop();
+ mMuxer.release();
+ mMuxer = null;
+ mVideoTrackIndex = mAudioTrackIndex = -1;
+ try {
+ mMuxer = new MediaMuxer(mFilePath + "-" + ++index + ".mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+ addTrack(mVideoFormat, true);
+ addTrack(mAudioFormat, false);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ public synchronized void release() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ if (mMuxer != null) {
+ if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1) {
+ if (VERBOSE)
+ Log.i(TAG, String.format("muxer is started. now it will be stoped."));
+ try {
+ mMuxer.stop();
+ mMuxer.release();
+ } catch (IllegalStateException ex) {
+ ex.printStackTrace();
+ }
+
+ if (System.currentTimeMillis() - mBeginMillis <= 1500){
+ new File(mFilePath + "-" + index + ".mp4").delete();
+ }
+ mAudioTrackIndex = mVideoTrackIndex = -1;
+ }else{
+ if (VERBOSE)
+ Log.i(TAG, String.format("muxer is failed to be stoped."));
+ }
+ }
+ }
+ }
+}