本篇文章將介紹本身總結的短視頻錄製的相關內容,主要分爲三個部分:git
先上效果圖github
錄製過程bash
錄製結果app
錄製流程大體如上圖所示。ide
新建外部紋理工具
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mTextureId = GLUtils.createTextureObject(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
mSurfaceTexture = new SurfaceTexture(mTextureId);
...
}
複製代碼
新建了外部紋理以後,傳入 Camerapost
mCamera.setPreviewTexture(mSurfaceTexture);
mCamera.startPreview();
複製代碼
GLSurfaceView 渲染時,請求 SurfaceTexture 更新,獲取最新的內容ui
@Override
public void onDrawFrame(GL10 gl) {
if (mFilter == null) {
return;
}
float matrix[] = new float[16];
if (mSurfaceTexture != null) {
//請求刷新最新內容
mSurfaceTexture.updateTexImage();
}
mSurfaceTexture.getTransformMatrix(matrix);
if (mFrameListener != null) {
//通知MediaCodec刷新畫面
mFrameListener.onFrameAvailable(new VideoFrameData(mFilter,
matrix, mSurfaceTexture.getTimestamp(), mTextureId));
}
mFilter.init();
if (mOldFilter != null) {
mOldFilter.release();
mOldFilter = null;
}
mSurfaceTexture.getTransformMatrix(mMatrix);
//繪製預覽內容
mFilter.draw(mTextureId, mMatrix);
}
複製代碼
mFilter 中包含 OpenGL 相關的着色器程序this
着色器代碼以下:google
/**
* 默認代碼
*/
private static final String FRAGMENT_CODE =
"#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;\n" +
"varying vec2 vTextureCoord;\n" +
"uniform samplerExternalOES uTexture;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(uTexture, vTextureCoord);\n" +
"}\n";
/**
* 默認代碼
*/
private static final String VERTEX_CODE =
"uniform mat4 uTexMatrix;\n" +
"attribute vec2 aPosition;\n" +
"attribute vec4 aTextureCoord;\n" +
"varying vec2 vTextureCoord;\n" +
"void main() {\n" +
" gl_Position = vec4(aPosition,0.0,1.0);\n" +
" vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" +
"}\n";
複製代碼
外部紋理和普通紋理不一樣,須要在片斷着色器代碼頭部聲明拓展。
#extension GL_OES_EGL_image_external : require
複製代碼
着色器代碼比較簡單,不包含濾鏡相關的內容,直接使用相機的紋理繪製一個矩形。
內容錄製編碼使用 MediaCodec + MediaMuxer 的組合來實現。MediaCodec 在初始化時,咱們能夠從中獲取一個 Surface,用來往裏面填充內容。
MediaFormat format = MediaFormat.createVideoFormat(C.VideoParams.MIME_TYPE,
configuration.getVideoWidth(),
configuration.getVideoHeight());
//設置參數
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, C.VideoParams.BIT_RATE);
format.setInteger(MediaFormat.KEY_FRAME_RATE, C.VideoParams.SAMPLE_RATE);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, C.VideoParams.I_FRAME_INTERVAL);
MediaCodec encoder = MediaCodec.createEncoderByType(C.VideoParams.MIME_TYPE);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
inputSurface = encoder.createInputSurface();
複製代碼
獲取 inputSurface 以後,咱們新建一個 EGLSurface,到這裏編碼器的初始化就完成了,當有新的內容時,通知編碼器來刷新。以前咱們獲取了GLSurfaceView 的 GL 上下文,當收到新內容通知時,咱們把 GL 環境切到編碼器的線程,而後繪製,最後調用 swapBuffers 方法把繪製的內容填充到inputSurface 中,這就是所謂的離屏渲染(聽着很高大上,後面講解短視頻後期製做時也會用到這個)。
這裏不使用 EOS 紋理也是能夠的,咱們能夠經過 Camera 的setPreviewCallback 方法監聽相機的每一幀數據,而後將 YUV 數據轉換成ARGB 數據,再轉成紋理交給 OpenGL 渲染便可。
最後新建 MediaMuxer
muxer = new MediaMuxer(configuration.getFileName(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
複製代碼
此部份內容參考 grafika 實現
視頻變速相對來講比較容易,在編碼以後,咱們從 MediaCodec 的緩衝區中獲取本次編碼內容的 ByteBuffer 和 BufferInfo ,前者是編碼後的內容,後者是本次內容的信息,包括時間戳,大小等。咱們經過改變視頻的時間戳,就能夠達到視頻變速的要求。好比要加快視頻的速度,那麼只須要將視頻的時間戳間隔縮小必定的倍數便可。放慢操做和這個相反,只須要把時間戳間隔放大必定的倍數便可。
音頻的錄製咱們須要使用到 AudioRecord 這個大殺器,大體流程圖以下。
音頻錄製比較簡單,參考官方文檔便可。這裏須要開啓兩條線程,由於目前使用的編碼是同步模式,若是是在一條線程裏處理數據,會致使麥克風的數據丟失。
關鍵代碼以下:
初始化AudioRecord
指定單聲道模式,採樣率爲 44100,每一個採樣點 16 比特
int bufferSize = AudioRecord.getMinBufferSize(
configuration.getSampleRate(), C.AudioParams.CHANNEL,
C.AudioParams.BITS_PER_SAMPLE);
recorder = new AudioRecord(
MediaRecorder.AudioSource.MIC, configuration.getSampleRate(),
C.AudioParams.CHANNEL, C.AudioParams.BITS_PER_SAMPLE, bufferSize);
複製代碼
初始化MediaCodec
MediaFormat audioFormat = MediaFormat.createAudioFormat(C.AudioParams.MIME_TYPE,
C.AudioParams.SAMPLE_RATE, C.AudioParams.CHANNEL_COUNT);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, C.AudioParams.CHANNEL);
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, C.AudioParams.BIT_RATE);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, C.AudioParams.CHANNEL_COUNT);
audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1024 * 4);
encoder = MediaCodec.createEncoderByType(C.AudioParams.MIME_TYPE);
encoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
bufferInfo = new MediaCodec.BufferInfo();
mStream = new BufferedOutputStream(new FileOutputStream(configuration.getFileName()));
複製代碼
音頻編碼
讀取音頻數據
byte[] buffer = new byte[configuration.getSamplePerFrame()];
int bytes = recorder.read(buffer, 0, buffer.length);
if (bytes > 0) {
encode(buffer, bytes);
}
複製代碼
塞進MediaCodec緩衝區
private void onEncode(byte[] data, int length) {
final ByteBuffer[] inputBuffers = encoder.getInputBuffers();
while (true) {
final int inputBufferIndex = encoder.dequeueInputBuffer(BUFFER_TIME_OUT);
if (inputBufferIndex >= 0) {
final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.position(0);
if (data != null) {
inputBuffer.put(data, 0, length);
}
if (length <= 0) {
encoder.queueInputBuffer(inputBufferIndex, 0, 0,
getTimeUs(), MediaCodec.BUFFER_FLAG_END_OF_STREAM);
break;
} else {
encoder.queueInputBuffer(inputBufferIndex, 0, length,
getTimeUs(), 0);
}
break;
}
}
}
複製代碼
取出編碼後的數據並寫入文件
private void drain() {
bufferInfo = new MediaCodec.BufferInfo();
ByteBuffer[] encoderOutputBuffers = encoder.getOutputBuffers();
int encoderStatus = encoder.dequeueOutputBuffer(bufferInfo, C.BUFFER_TIME_OUT);
while (encoderStatus >= 0) {
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
int outSize = bufferInfo.size;
encodedData.position(bufferInfo.offset);
encodedData.limit(bufferInfo.offset + bufferInfo.size);
byte[] data = new byte[outSize + 7];
addADTSHeader(data, outSize + 7);
encodedData.get(data, 7, outSize);
try {
mStream.write(data, 0, data.length);
} catch (IOException e) {
LogUtil.e(e);
}
if (duration >= configuration.getMaxDuration()) {
stop();
}
encoder.releaseOutputBuffer(encoderStatus, false);
encoderStatus = encoder.dequeueOutputBuffer(bufferInfo, C.BUFFER_TIME_OUT);
}
}
複製代碼
aac文件對內容格式有要求,須要在每一幀的內容頭部添加內容,代碼以下:
private void addADTSHeader(byte[] packet, int length) {
int profile = 2; // AAC LC
int freqIdx = 4; // 44.1KHz
int chanCfg = 1; // CPE
// fill in A D T S data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (length >> 11));
packet[4] = (byte) ((length & 0x7FF) >> 3);
packet[5] = (byte) (((length & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
複製代碼
一開始調研短視頻方案的時候,對於音頻變速這方面,想了不少個方案:
最終我選擇了第三個方案,前兩個方案的死因以下:
第三個方案須要使用一個第三方庫——SoundTouch,它能夠改變音頻的音調和速度。SoundTouch 由 C++ 實現,所以咱們須要用 NDK 工具把它集成到工程當中。集成的方法參照官方文檔便可。官方的例子中主要給出了處理 wav 文件的方法,接下來我介紹一下如何使用這個庫實時處理 pcm 數據(經過實時處理PCM 數據,咱們還能夠弄個變聲功能噢)。
新建類—— SoundTouch
public class SoundTouch {
private native final void setTempo(long handle, float tempo);
private native final void setPitchSemiTones(long handle, float pitch);
private native final void putBytes(long handle, byte[] input, int offset, int length);
private native final int getBytes(long handle, byte[] output, int length);
private native final static long newInstance();
private native final void deleteInstance(long handle);
private native final void flush(long handle);
private long handle = 0;
public SoundTouch() {
handle = newInstance();
}
public void putBytes(byte[] input) {
this.putBytes(handle, input, 0, input.length);
}
public int getBytes(byte[] output) {
return this.getBytes(handle, output, output.length);
}
public void close() {
deleteInstance(handle);
handle = 0;
}
public void flush() {
this.flush(handle);
}
public void setTempo(float tempo) {
setTempo(handle, tempo);
}
public void setPitchSemiTones(float pitch) {
setPitchSemiTones(handle, pitch);
}
static {
System.loadLibrary("soundtouch");
}
}
複製代碼
主要有四個方法
新建對應的 cpp 文件,關鍵代碼以下:
void Java_com_netease_soundtouch_SoundTouch_setTempo(JNIEnv *env, jobject thiz, jlong handle, jfloat tempo)
{
SoundTouch *ptr = (SoundTouch *)handle;
ptr->setTempo(tempo);
}
void Java_com_netease_soundtouch_SoundTouch_setPitchSemiTones(JNIEnv *env, jobject thiz, jlong handle, jfloat pitch)
{
SoundTouch *ptr = (SoundTouch *)handle;
ptr->setPitchSemiTones(pitch);
}
void Java_com_netease_soundtouch_SoundTouch_putBytes(JNIEnv *env, jobject thiz, jlong handle, jbyteArray input, jint offset, jint length)
{
SoundTouch *soundTouch = (SoundTouch *)handle;
jbyte *data;
data = env->GetByteArrayElements(input, JNI_FALSE);
soundTouch->putSamples((SAMPLETYPE *)data, length/2);
env->ReleaseByteArrayElements(input, data, 0);
}
jint Java_com_netease_soundtouch_SoundTouch_getBytes(JNIEnv *env, jobject thiz, jlong handle, jbyteArray output, jint length)
{
int receiveSamples = 0;
int maxReceiveSamples = length/2;
SoundTouch *soundTouch = (SoundTouch *)handle;
jbyte *data;
data = env->GetByteArrayElements(output, JNI_FALSE);
receiveSamples = soundTouch->receiveSamples((SAMPLETYPE *)data,
maxReceiveSamples);
env->ReleaseByteArrayElements(output, data, 0);
return receiveSamples;
}
複製代碼
處理 pcm 數據
//在將pcm導入MediaCodec以前,先由SoundTouch處理一遍
private void encode(final byte[] data, final int length) {
encodeHandler.post(new Runnable() {
@Override
public void run() {
if (soundTouch != null) {
soundTouch.putBytes(data);
while (true) {
//若是是用MediaMuxer來生成音頻,咱們每次只能寫入一幀數據,那麼這裏緩衝區就不能用4096,只能用1024
byte[] modified = new byte[4096];
int count = soundTouch.getBytes(modified);
if (count > 0) {
onEncode(modified, count * 2);
drain();
} else {
break;
}
}
} else {
onEncode(data, length);
drain();
}
}
});
}
複製代碼
錄製完視頻和音頻以後,咱們須要將音頻和視頻進行合成,這一步直接使用FFMPEG 工具便可,命令行以下:
ffmpeg -y -i audioFile -ss 0 -t duration -i videoFile -acodec copy -vcodec copy output
其中,audioFile 爲咱們的 aac 文件的路徑,videoFile 爲 mp4 文件的路徑,output 爲最終生成的 mp4 文件的路徑,duration 爲音頻文件的長度,使用MediaExtractor 獲取便可。
ffmpeg 不會自動幫咱們建立文件,在合成以前,須要先建立output文件
執行完這個命令後,音頻和視頻就合成完畢了,15秒的視頻,合成一次大概只須要100ms左右。咱們只須要在每小段視頻錄製完畢時合成一次便可,對用戶來講沒什麼影響。視頻的碼率越高,合成所須要的時間越久。
多段視頻拼接使用 ffmpeg 便可,無需從新解碼,咱們在點擊 app 中的下一步按鈕時進行視頻的拼接。關鍵代碼以下:
public static VideoCommand mergeVideo(List<String> videos, String output) {
String appDir = StorageUtil.getExternalStoragePath() + File.separator;
String fileName = "ffmpeg_concat.txt";
FileUtils.writeTxtToFile(videos, appDir, fileName);
VideoCommand cmd = new VideoCommand();
cmd.append("ffmpeg").append("-y").append("-f").append("concat").append("-safe")
.append("0").append("-i").append(appDir + fileName)
.append("-c").append("copy").append(output);
return cmd;
}
複製代碼
命令行爲:
ffmpeg -y -f concat -safe 0 -i concatFile -c copy output
其中,concatFile 是一個 txt 文件,內容爲咱們要拼接的文件的路徑列表,output 爲最終輸出的 mp4 文件。
整個短視頻的錄製方案大概就是如此,關於視頻錄製方面,由於沒有具體線上項目實踐過,因此可能會存在機型不兼容的狀況,你們若是有更好的方案,歡迎在評論區提出來噢,一塊兒探討下。有些地方講解不對或者以爲不清楚的,歡迎你們在評論區指出。後面會發關於短視頻後期處理的文章,敬請關注!