jiangdongguo
7 years ago
10 changed files with 1279 additions and 218 deletions
@ -0,0 +1,60 @@ |
|||
package com.jiangdg.usbcamera; |
|||
|
|||
import android.os.Environment; |
|||
|
|||
import java.io.BufferedOutputStream; |
|||
import java.io.File; |
|||
import java.io.FileOutputStream; |
|||
import java.io.IOException; |
|||
|
|||
/** 创建文件 |
|||
* |
|||
* Created by jiangdongguo on 2017/10/18. |
|||
*/ |
|||
|
|||
public class FileUtils { |
|||
|
|||
private static BufferedOutputStream outputStream; |
|||
public static String ROOT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator; |
|||
|
|||
public static void createfile(String path){ |
|||
File file = new File(path); |
|||
if(file.exists()){ |
|||
file.delete(); |
|||
} |
|||
try { |
|||
outputStream = new BufferedOutputStream(new FileOutputStream(file)); |
|||
} catch (Exception e){ |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
|
|||
public static void releaseFile(){ |
|||
try { |
|||
outputStream.flush(); |
|||
outputStream.close(); |
|||
} catch (IOException e) { |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
|
|||
public static void putFileStream(byte[] data,int offset,int length){ |
|||
if(outputStream != null) { |
|||
try { |
|||
outputStream.write(data,offset,length); |
|||
} catch (IOException e) { |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static void putFileStream(byte[] data){ |
|||
if(outputStream != null) { |
|||
try { |
|||
outputStream.write(data); |
|||
} catch (IOException e) { |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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<Mp4MediaMuxer> 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; |
|||
} |
|||
} |
@ -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<Mp4MediaMuxer> 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; |
|||
} |
|||
} |
@ -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.")); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue