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